diff --git a/decode_test.go b/decode_test.go index bc3ce55..5ee3e6b 100644 --- a/decode_test.go +++ b/decode_test.go @@ -1423,3 +1423,57 @@ func (b *ctxBoth) UnmarshalTOML(data any) error { b.plainCalled = true return nil } + +func TestEmbeddedMapRule(t *testing.T) { + t.Run("only the first embedded map takes the leftover keys", func(t *testing.T) { + type ExtraMap map[string]any + type MoreMap map[string]any + type Config struct { + Port int + ExtraMap + MoreMap + } + var cfg Config + in := []byte("port = 8080\nlang = \"cs\"\nregion = \"EU\"\n") + if err := Unmarshal(in, &cfg); err != nil { + t.Fatal(err) + } + if cfg.Port != 8080 { + t.Errorf("port = %d", cfg.Port) + } + if cfg.ExtraMap["lang"] != "cs" || cfg.ExtraMap["region"] != "EU" { + t.Errorf("extra = %v, want the leftover keys", cfg.ExtraMap) + } + if len(cfg.MoreMap) != 0 { + t.Errorf("more = %v, want empty: only the first embedded map is the filler", cfg.MoreMap) + } + }) + t.Run("a tagged embedded map is an ordinary field", func(t *testing.T) { + type Config struct { + Extra map[string]any `toml:"extra"` + } + var cfg Config + if err := Unmarshal([]byte("extra = {a = 1}\n"), &cfg); err != nil { + t.Fatal(err) + } + if cfg.Extra["a"] != int64(1) { + t.Errorf("extra = %v", cfg.Extra) + } + }) +} + +func TestDecodeMergesIntoNonEmptyMap(t *testing.T) { + dst := map[string]any{"keep": "me", "port": 1} + if err := Unmarshal([]byte("port = 8080\nlang = \"cs\"\n"), &dst); err != nil { + t.Fatal(err) + } + if dst["keep"] != "me" { + t.Errorf("keep = %v, want the pre-existing key kept", dst["keep"]) + } + if dst["port"] != int64(8080) { + t.Errorf("port = %v, want the document's value to win", dst["port"]) + } + if dst["lang"] != "cs" { + t.Errorf("lang = %v, want the key added", dst["lang"]) + } +} diff --git a/docs/API.md b/docs/API.md index fad99e6..f6744fd 100644 --- a/docs/API.md +++ b/docs/API.md @@ -202,7 +202,8 @@ table with `Number`, which keeps the literal; see - `*struct`, matched per the field rules below - `*map[string]any` or `*map[string]T`, keys become map keys and values decode - into `T` recursively + into `T` recursively; a map that already holds entries is merged into, the + document's values replacing same-named keys and the rest left standing - `*OrderedMap`, the keys fill in the order the document wrote them; see [Ordered tables](#ordered-tables) - `*any`, receives the whole parsed tree unchanged @@ -220,7 +221,9 @@ For a struct destination, a TOML key matches a field as follows: into the embedded struct and matches its own fields against the same keys, mirroring how the encoder flattens it. A nil embedded pointer struct is allocated on demand. An untagged embedded map receives the keys no field - claims. + claims; when a struct embeds several untagged maps, the first one + declared takes all of them and the rest stay untouched, so the rule stays + predictable. 4. The key itself is lower-cased before lookup, so the match is case-insensitive on both sides: `DATABASEURL` matches a field named `DatabaseUrl`.