diff --git a/dupkeys_test.go b/dupkeys_test.go new file mode 100644 index 0000000..14f9eb1 --- /dev/null +++ b/dupkeys_test.go @@ -0,0 +1,56 @@ +package yaml_test + +import ( + "testing" + + "github.com/oasdiff/yaml" +) + +type dupSpec struct { + Info struct { + Title string `json:"title"` + Version string `json:"version"` + } `json:"info"` +} + +const dupSrc = "info:\n title: a\n title: b\n version: \"1\"\n" + +func TestDecodeOpts_DuplicateKeys(t *testing.T) { + var strict dupSpec + if _, err := yaml.Unmarshal([]byte(dupSrc), &strict, yaml.DecodeOpts{}); err == nil { + t.Fatal("default should reject a repeated key") + } + + var lenient dupSpec + if _, err := yaml.Unmarshal([]byte(dupSrc), &lenient, + yaml.DecodeOpts{AllowDuplicateKeys: true}); err != nil { + t.Fatalf("AllowDuplicateKeys should accept: %v", err) + } + // The last occurrence wins, as encoding/json does. Asserting the value, + // not just the absence of an error. + if lenient.Info.Title != "b" { + t.Fatalf("title = %q, want %q", lenient.Info.Title, "b") + } + if lenient.Info.Version != "1" { + t.Fatalf("sibling key clobbered: version = %q", lenient.Info.Version) + } +} + +// The option must survive the JSON round trip this package performs, and must +// compose with origin tracking rather than disabling it. +func TestDecodeOpts_DuplicateKeysWithOrigin(t *testing.T) { + var v dupSpec + tree, err := yaml.Unmarshal([]byte(dupSrc), &v, yaml.DecodeOpts{ + AllowDuplicateKeys: true, + Origin: yaml.OriginOpt{Enabled: true, File: "spec.yaml"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if v.Info.Title != "b" { + t.Fatalf("title = %q, want %q", v.Info.Title, "b") + } + if tree == nil { + t.Fatal("origin tree should still be produced") + } +} diff --git a/go.mod b/go.mod index b094f80..8eb2f7b 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/oasdiff/yaml go 1.25 -require github.com/oasdiff/yaml3 v0.0.14 +require github.com/oasdiff/yaml3 v0.0.15-0.20260802195239-79d189934084 diff --git a/go.sum b/go.sum index 9e6d2c6..c5a4812 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw= -github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg= +github.com/oasdiff/yaml3 v0.0.15-0.20260802195239-79d189934084 h1:0FFmaKhYkUtCiwh81RlE2WAmd8ga69YnnTOzJXHIKTE= +github.com/oasdiff/yaml3 v0.0.15-0.20260802195239-79d189934084/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/yaml.go b/yaml.go index 3ec5366..a767c8f 100644 --- a/yaml.go +++ b/yaml.go @@ -59,6 +59,13 @@ type DecodeOpts struct { // real-world specs that use date-shaped strings as keys. Explicit // "!!timestamp" tags in the source still resolve to time.Time. DisableTimestamps bool + // AllowDuplicateKeys makes a repeated mapping key take its last value + // instead of being an error. That is what encoding/json does with a + // repeated JSON object name, so it is the setting to use when the same + // types are decoded from both YAML and JSON and a document should not + // load or fail purely on account of its format. Default false: repeated + // keys are an error, which is YAML's rule. + AllowDuplicateKeys bool } // OriginTree holds __origin__ data extracted from a YAML-decoded map tree. @@ -87,6 +94,7 @@ type OriginTree struct { func Unmarshal(y []byte, o interface{}, decode DecodeOpts, opts ...JSONOpt) (*OriginTree, error) { dec := yaml.NewDecoder(bytes.NewReader(y)) dec.Origin(decode.Origin.Enabled, decode.Origin.File) + dec.AllowDuplicateKeys(decode.AllowDuplicateKeys) if decode.DisableTimestamps { dec.DisableTimestamps(true) }