Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions goyaml.v3/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ func (e *Encoder) DefaultSeqIndent() {
func yaml_emitter_process_line_comment(emitter *yaml_emitter_t) bool {
return yaml_emitter_process_line_comment_linebreak(emitter, false)
}

func (e *Encoder) SetWidth(width int) {
yaml_emitter_set_width(&e.encoder.emitter, width)
}
14 changes: 14 additions & 0 deletions goyaml.v3/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,17 @@ a:
t.Errorf("expected error, got none")
}
}

func (s *S) TestSetWidth(c *C) {
longString := "this is very long line with spaces and it must be longer than 80 so we will repeat that it must be longer that 80"

var buf bytes.Buffer
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
enc.SetWidth(80)
err := enc.Encode(map[string]interface{}{"a": longString})
c.Assert(err, Equals, nil)
err = enc.Close()
c.Assert(err, Equals, nil)
c.Assert(buf.String(), Equals, "a: this is very long line with spaces and it must be longer than 80 so we will repeat\n that it must be longer that 80\n")
}
11 changes: 9 additions & 2 deletions goyaml.v3/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ func Unmarshal(in []byte, out interface{}) (err error) {

// A Decoder reads and decodes YAML values from an input stream.
type Decoder struct {
parser *parser
knownFields bool
parser *parser
knownFields bool
noUniqueKeys bool
}

// NewDecoder returns a new decoder that reads from r.
Expand All @@ -111,6 +112,11 @@ func (dec *Decoder) KnownFields(enable bool) {
dec.knownFields = enable
}

// UniqueKeys ensures that the keys in the yaml document are unique.
func (dec *Decoder) UniqueKeys(enable bool) {
dec.noUniqueKeys = !enable
}

// Decode reads the next YAML-encoded value from its input
// and stores it in the value pointed to by v.
//
Expand All @@ -119,6 +125,7 @@ func (dec *Decoder) KnownFields(enable bool) {
func (dec *Decoder) Decode(v interface{}) (err error) {
d := newDecoder()
d.knownFields = dec.knownFields
d.uniqueKeys = !dec.noUniqueKeys
defer handleErr(&err)
node := dec.parser.parse()
if node == nil {
Expand Down
71 changes: 59 additions & 12 deletions yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import (
"reflect"
"strconv"

"sigs.k8s.io/yaml/goyaml.v2"
yamlv2 "sigs.k8s.io/yaml/goyaml.v2"
yamlv3 "sigs.k8s.io/yaml/goyaml.v3"
)

// Marshal marshals obj into JSON using stdlib json.Marshal, and then converts JSON to YAML using JSONToYAML (see that method for more reference)
Expand All @@ -37,6 +38,40 @@ func Marshal(obj interface{}) ([]byte, error) {
return JSONToYAML(jsonBytes)
}

// yamlv3Unmarshal is a YAML unmarshaler that does not error on duplicate keys or on unknown fields.
// This function replicates the behavior of the Unmarshal function in the goyaml v2 library.
func yamlv3Unmarshal(input []byte, output interface{}) error {
decoder := yamlv3.NewDecoder(bytes.NewReader(input))
decoder.KnownFields(false)
decoder.UniqueKeys(false)
return decoder.Decode(output)
}

// yamlv3UnmarshalStrict is like Unmarshal except that any fields that are found in the data that
// do not have corresponding struct members, or mapping keys that are duplicates, will result in an error.
// This function replicates the behavior of the UnmarshalStrict function in the goyaml v2 library.
func yamlv3UnmarshalStrict(input []byte, output interface{}) error {
decoder := yamlv3.NewDecoder(bytes.NewReader(input))
decoder.KnownFields(true)
decoder.UniqueKeys(true)
return decoder.Decode(output)
}

// yamlv3Marshal is a YAML marshaller that uses compact sequence indentation an indentation of 2 spaces.
// It also sets the width to 80 characters.
// This function replicates the behavior of the Marshal function in the goyaml v2 library.
func yamlv3Marshal(input interface{}) ([]byte, error) {
buf := bytes.Buffer{}
encoder := yamlv3.NewEncoder(&buf)
encoder.CompactSeqIndent()
encoder.SetIndent(2)
encoder.SetWidth(80)
if err := encoder.Encode(input); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

// JSONOpt is a decoding option for decoding from JSON format.
type JSONOpt func(*json.Decoder) *json.Decoder

Expand All @@ -53,15 +88,15 @@ type JSONOpt func(*json.Decoder) *json.Decoder
// - YAML non-string keys, e.g. ints, bools and floats, are converted to strings implicitly during the YAML to JSON conversion process.
// - There are no compatibility guarantees for returned error values.
func Unmarshal(yamlBytes []byte, obj interface{}, opts ...JSONOpt) error {
return unmarshal(yamlBytes, obj, yaml.Unmarshal, opts...)
return unmarshal(yamlBytes, obj, yamlv3Unmarshal, opts...)
}

// UnmarshalStrict is similar to Unmarshal (please read its documentation for reference), with the following exceptions:
//
// - Duplicate fields in an object yield an error. This is according to the YAML specification.
// - If obj, or any of its recursive children, is a struct, presence of fields in the serialized data unknown to the struct will yield an error.
func UnmarshalStrict(yamlBytes []byte, obj interface{}, opts ...JSONOpt) error {
return unmarshal(yamlBytes, obj, yaml.UnmarshalStrict, append(opts, DisallowUnknownFields)...)
return unmarshal(yamlBytes, obj, yamlv3UnmarshalStrict, append(opts, DisallowUnknownFields)...)
}

// unmarshal unmarshals the given YAML byte stream into the given interface,
Expand Down Expand Up @@ -111,13 +146,13 @@ func JSONToYAML(j []byte) ([]byte, error) {
// etc.) when unmarshalling to interface{}, it just picks float64
// universally. go-yaml does go through the effort of picking the right
// number type, so we can preserve number type throughout this process.
err := yaml.Unmarshal(j, &jsonObj)
err := yamlv3Unmarshal(j, &jsonObj)
if err != nil {
return nil, err
}

// Marshal this object into YAML.
yamlBytes, err := yaml.Marshal(jsonObj)
yamlBytes, err := yamlv3Marshal(jsonObj)
if err != nil {
return nil, err
}
Expand All @@ -144,13 +179,13 @@ func JSONToYAML(j []byte) ([]byte, error) {
// - Unlike Unmarshal, all integers, up to 64 bits, are preserved during this round-trip.
// - There are no compatibility guarantees for returned error values.
func YAMLToJSON(y []byte) ([]byte, error) {
return yamlToJSONTarget(y, nil, yaml.Unmarshal)
return yamlToJSONTarget(y, nil, yamlv3Unmarshal)
}

// YAMLToJSONStrict is like YAMLToJSON but enables strict YAML decoding,
// returning an error on any duplicate field names.
func YAMLToJSONStrict(y []byte) ([]byte, error) {
return yamlToJSONTarget(y, nil, yaml.UnmarshalStrict)
return yamlToJSONTarget(y, nil, yamlv3UnmarshalStrict)
}

func yamlToJSONTarget(yamlBytes []byte, jsonTarget *reflect.Value, unmarshalFn func([]byte, interface{}) error) ([]byte, error) {
Expand Down Expand Up @@ -196,6 +231,18 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
}
}

// Transform map[string]interface{} into map[interface{}]interface{},
// such that the switch statement below can handle it.
// TODO: we should modify the logic in the switch statement to handle
// map[string]interface{} directly, instead of doing this conversion.
if stringMap, ok := yamlObj.(map[string]interface{}); ok {
interfaceMap := make(map[interface{}]interface{})
for k, v := range stringMap {
interfaceMap[k] = v
}
yamlObj = interfaceMap
}

// If yamlObj is a number or a boolean, check if jsonTarget is a string -
// if so, coerce. Else return normal.
// If yamlObj is a map or array, find the field that each key is
Expand Down Expand Up @@ -227,7 +274,7 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
// Stolen from go-yaml to use the same conversion to string as
// the go-yaml library uses to convert float to string when
// Marshaling.
s := strconv.FormatFloat(typedKey, 'g', -1, 32)
s := strconv.FormatFloat(typedKey, 'g', -1, 64)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Will this result in visibly different output? Any changes have the potential to break consumers so we need to be extremely careful about changes that break byte-for-byte round-tripping" - @liggitt

#100 (comment)

Copy link
Member Author

@inteon inteon Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaik, this should just result in float64 values also being supported.
I added a few additional test cases for float marshalling and unmarshalling.

switch s {
case "+Inf":
s = ".inf"
Expand Down Expand Up @@ -339,7 +386,7 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
case int64:
s = strconv.FormatInt(typedVal, 10)
case float64:
s = strconv.FormatFloat(typedVal, 'g', -1, 32)
s = strconv.FormatFloat(typedVal, 'g', -1, 64)
case uint64:
s = strconv.FormatUint(typedVal, 10)
case bool:
Expand Down Expand Up @@ -370,13 +417,13 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
// Big int/int64/uint64 do not lose precision as in the json-yaml roundtripping case.
//
// string, bool and any other types are unchanged.
func JSONObjectToYAMLObject(j map[string]interface{}) yaml.MapSlice {
func JSONObjectToYAMLObject(j map[string]interface{}) yamlv2.MapSlice {
if len(j) == 0 {
return nil
}
ret := make(yaml.MapSlice, 0, len(j))
ret := make(yamlv2.MapSlice, 0, len(j))
for k, v := range j {
ret = append(ret, yaml.MapItem{Key: k, Value: jsonToYAMLValue(v)})
ret = append(ret, yamlv2.MapItem{Key: k, Value: jsonToYAMLValue(v)})
}
return ret
}
Expand Down
26 changes: 12 additions & 14 deletions yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,13 @@ func testYAMLToJSON(t *testing.T, f testYAMLToJSONFunc, tests map[string]yamlToJ
type MarshalTest struct {
A string
B int64
// Would like to test float64, but it's not supported in go-yaml.
// (See https://github.com/go-yaml/yaml/issues/83.)
C float32
C float64
}

func TestMarshal(t *testing.T) {
f32String := strconv.FormatFloat(math.MaxFloat32, 'g', -1, 32)
s := MarshalTest{"a", math.MaxInt64, math.MaxFloat32}
e := []byte(fmt.Sprintf("A: a\nB: %d\nC: %s\n", math.MaxInt64, f32String))
f64String := strconv.FormatFloat(math.MaxFloat64, 'g', -1, 64)
s := MarshalTest{"a", math.MaxInt64, math.MaxFloat64}
e := []byte(fmt.Sprintf("A: a\nB: %d\nC: %s\n", math.MaxInt64, f64String))

y, err := Marshal(s)
if err != nil {
Expand Down Expand Up @@ -296,12 +294,12 @@ func TestUnmarshal(t *testing.T) {
"tagged casematched boolean key (yes)": {
encoded: []byte("Yes: test"),
decodeInto: new(UnmarshalTaggedStruct),
decoded: UnmarshalTaggedStruct{TrueLower: "test"},
decoded: UnmarshalTaggedStruct{YesUpper: "test"}, // In yamlv2, this incorrectly set the TrueLower field instead
},
"tagged non-casematched boolean key (yes)": {
encoded: []byte("yes: test"),
decodeInto: new(UnmarshalTaggedStruct),
decoded: UnmarshalTaggedStruct{TrueLower: "test"},
decoded: UnmarshalTaggedStruct{YesLower: "test"}, // In yamlv2, this incorrectly set the TrueLower field instead
},
"tagged integer key": {
encoded: []byte("3: test"),
Expand Down Expand Up @@ -343,7 +341,7 @@ func TestUnmarshal(t *testing.T) {
"boolean value (no) into string field": {
encoded: []byte("a: no"),
decodeInto: new(UnmarshalStruct),
decoded: UnmarshalStruct{A: "false"},
decoded: UnmarshalStruct{A: "no"}, // In yamlv2, this incorrectly set the value to "false"
},

// decode into complex fields
Expand Down Expand Up @@ -390,7 +388,7 @@ func TestUnmarshal(t *testing.T) {
encoded: []byte("Yes:"),
decodeInto: new(map[string]struct{}),
decoded: map[string]struct{}{
"true": {},
"Yes": {}, // In yamlv2, this incorrectly set the key to "true"
},
},
"string map: decode integer key": {
Expand Down Expand Up @@ -639,8 +637,8 @@ func TestYAMLToJSON(t *testing.T) {
},
"boolean value (no)": {
yaml: "t: no\n",
json: `{"t":false}`,
yamlReverseOverwrite: strPtr("t: false\n"),
json: `{"t":"no"}`, // In yamlv2, this was interpreted as the boolean false
yamlReverseOverwrite: strPtr("t: \"no\"\n"),
},
"integer value (2^53 + 1)": {
yaml: "t: 9007199254740993\n",
Expand Down Expand Up @@ -668,8 +666,8 @@ func TestYAMLToJSON(t *testing.T) {
},
"boolean key (no)": {
yaml: "no: a",
json: `{"false":"a"}`,
yamlReverseOverwrite: strPtr("\"false\": a\n"),
json: `{"no":"a"}`, // In yamlv2, this was incorrectly converted to "false"
yamlReverseOverwrite: strPtr("\"no\": a\n"),
},
"integer key": {
yaml: "1: a",
Expand Down