diff --git a/pkg/yqlib/encoder.go b/pkg/yqlib/encoder.go index e9fae6af..e0ecaccf 100644 --- a/pkg/yqlib/encoder.go +++ b/pkg/yqlib/encoder.go @@ -1,10 +1,6 @@ package yqlib import ( - "bytes" - "encoding/json" - "errors" - "fmt" "io" yaml "gopkg.in/yaml.v3" @@ -17,162 +13,17 @@ type Encoder interface { CanHandleAliases() bool } -// orderedMap allows to marshal and unmarshal JSON and YAML values keeping the -// order of keys and values in a map or an object. -type orderedMap struct { - // if this is an object, kv != nil. If this is not an object, kv == nil. - kv []orderedMapKV - altVal interface{} -} +func mapKeysToStrings(node *yaml.Node) { -type orderedMapKV struct { - K string - V orderedMap -} - -func (o *orderedMap) UnmarshalJSON(data []byte) error { - switch data[0] { - case '{': - // initialise so that even if the object is empty it is not nil - o.kv = []orderedMapKV{} - - // create decoder - dec := json.NewDecoder(bytes.NewReader(data)) - _, err := dec.Token() // open object - if err != nil { - return err - } - - // cycle through k/v - var tok json.Token - for tok, err = dec.Token(); err == nil; tok, err = dec.Token() { - // we can expect two types: string or Delim. Delim automatically means - // that it is the closing bracket of the object, whereas string means - // that there is another key. - if _, ok := tok.(json.Delim); ok { - break + if node.Kind == yaml.MappingNode { + for index, child := range node.Content { + if index%2 == 0 { // its a map key + child.Tag = "!!str" } - kv := orderedMapKV{ - K: tok.(string), - } - if err := dec.Decode(&kv.V); err != nil { - return err - } - o.kv = append(o.kv, kv) } - // unexpected error - if err != nil && !errors.Is(err, io.EOF) { - return err - } - return nil - case '[': - var res []*orderedMap - if err := json.Unmarshal(data, &res); err != nil { - return err - } - o.altVal = res - o.kv = nil - return nil } - return json.Unmarshal(data, &o.altVal) -} - -func (o orderedMap) MarshalJSON() ([]byte, error) { - buf := new(bytes.Buffer) - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) // do not escape html chars e.g. &, <, > - if o.kv == nil { - if err := enc.Encode(o.altVal); err != nil { - return nil, err - } - return buf.Bytes(), nil - } - buf.WriteByte('{') - for idx, el := range o.kv { - if err := enc.Encode(el.K); err != nil { - return nil, err - } - buf.WriteByte(':') - if err := enc.Encode(el.V); err != nil { - return nil, err - } - if idx != len(o.kv)-1 { - buf.WriteByte(',') - } - } - buf.WriteByte('}') - return buf.Bytes(), nil -} - -func (o *orderedMap) UnmarshalYAML(node *yaml.Node) error { - switch node.Kind { - case yaml.DocumentNode: - if len(node.Content) == 0 { - return nil - } - return o.UnmarshalYAML(node.Content[0]) - case yaml.AliasNode: - return o.UnmarshalYAML(node.Alias) - case yaml.ScalarNode: - return node.Decode(&o.altVal) - case yaml.MappingNode: - // set kv to non-nil - o.kv = []orderedMapKV{} - for i := 0; i < len(node.Content); i += 2 { - var key string - var val orderedMap - if err := node.Content[i].Decode(&key); err != nil { - return err - } - if err := node.Content[i+1].Decode(&val); err != nil { - return err - } - o.kv = append(o.kv, orderedMapKV{ - K: key, - V: val, - }) - } - return nil - case yaml.SequenceNode: - // note that this has to be a pointer, so that nulls can be represented. - var res []*orderedMap - if err := node.Decode(&res); err != nil { - return err - } - o.altVal = res - o.kv = nil - return nil - case 0: - // null - o.kv = nil - o.altVal = nil - return nil - default: - return fmt.Errorf("orderedMap: invalid yaml node") + for _, child := range node.Content { + mapKeysToStrings(child) } } - -func (o *orderedMap) MarshalYAML() (interface{}, error) { - // fast path: kv is nil, use altVal - if o.kv == nil { - return o.altVal, nil - } - content := make([]*yaml.Node, 0, len(o.kv)*2) - for _, val := range o.kv { - n := new(yaml.Node) - if err := n.Encode(val.V); err != nil { - return nil, err - } - content = append(content, &yaml.Node{ - Kind: yaml.ScalarNode, - Tag: "!!str", - Value: val.K, - }, n) - } - return &yaml.Node{ - Kind: yaml.MappingNode, - Tag: "!!map", - Content: content, - }, nil -} diff --git a/pkg/yqlib/encoder_json.go b/pkg/yqlib/encoder_json.go index 9f5b71ac..6039578f 100644 --- a/pkg/yqlib/encoder_json.go +++ b/pkg/yqlib/encoder_json.go @@ -14,21 +14,6 @@ type jsonEncoder struct { UnwrapScalar bool } -func mapKeysToStrings(node *yaml.Node) { - - if node.Kind == yaml.MappingNode { - for index, child := range node.Content { - if index%2 == 0 { // its a map key - child.Tag = "!!str" - } - } - } - - for _, child := range node.Content { - mapKeysToStrings(child) - } -} - func NewJSONEncoder(indent int, colorise bool, unwrapScalar bool) Encoder { var indentString = "" diff --git a/pkg/yqlib/ordered_map.go b/pkg/yqlib/ordered_map.go new file mode 100644 index 00000000..a783423b --- /dev/null +++ b/pkg/yqlib/ordered_map.go @@ -0,0 +1,14 @@ +package yqlib + +// orderedMap allows to marshal and unmarshal JSON and YAML values keeping the +// order of keys and values in a map or an object. +type orderedMap struct { + // if this is an object, kv != nil. If this is not an object, kv == nil. + kv []orderedMapKV + altVal interface{} +} + +type orderedMapKV struct { + K string + V orderedMap +} diff --git a/pkg/yqlib/ordered_map_json.go b/pkg/yqlib/ordered_map_json.go new file mode 100644 index 00000000..94a1d780 --- /dev/null +++ b/pkg/yqlib/ordered_map_json.go @@ -0,0 +1,83 @@ +package yqlib + +import ( + "bytes" + "encoding/json" + "errors" + "io" +) + +func (o *orderedMap) UnmarshalJSON(data []byte) error { + switch data[0] { + case '{': + // initialise so that even if the object is empty it is not nil + o.kv = []orderedMapKV{} + + // create decoder + dec := json.NewDecoder(bytes.NewReader(data)) + _, err := dec.Token() // open object + if err != nil { + return err + } + + // cycle through k/v + var tok json.Token + for tok, err = dec.Token(); err == nil; tok, err = dec.Token() { + // we can expect two types: string or Delim. Delim automatically means + // that it is the closing bracket of the object, whereas string means + // that there is another key. + if _, ok := tok.(json.Delim); ok { + break + } + kv := orderedMapKV{ + K: tok.(string), + } + if err := dec.Decode(&kv.V); err != nil { + return err + } + o.kv = append(o.kv, kv) + } + // unexpected error + if err != nil && !errors.Is(err, io.EOF) { + return err + } + return nil + case '[': + var res []*orderedMap + if err := json.Unmarshal(data, &res); err != nil { + return err + } + o.altVal = res + o.kv = nil + return nil + } + + return json.Unmarshal(data, &o.altVal) +} + +func (o orderedMap) MarshalJSON() ([]byte, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) // do not escape html chars e.g. &, <, > + if o.kv == nil { + if err := enc.Encode(o.altVal); err != nil { + return nil, err + } + return buf.Bytes(), nil + } + buf.WriteByte('{') + for idx, el := range o.kv { + if err := enc.Encode(el.K); err != nil { + return nil, err + } + buf.WriteByte(':') + if err := enc.Encode(el.V); err != nil { + return nil, err + } + if idx != len(o.kv)-1 { + buf.WriteByte(',') + } + } + buf.WriteByte('}') + return buf.Bytes(), nil +} diff --git a/pkg/yqlib/ordered_map_yaml.go b/pkg/yqlib/ordered_map_yaml.go new file mode 100644 index 00000000..b3c49442 --- /dev/null +++ b/pkg/yqlib/ordered_map_yaml.go @@ -0,0 +1,79 @@ +package yqlib + +import ( + "fmt" + + yaml "gopkg.in/yaml.v3" +) + +func (o *orderedMap) UnmarshalYAML(node *yaml.Node) error { + switch node.Kind { + case yaml.DocumentNode: + if len(node.Content) == 0 { + return nil + } + return o.UnmarshalYAML(node.Content[0]) + case yaml.AliasNode: + return o.UnmarshalYAML(node.Alias) + case yaml.ScalarNode: + return node.Decode(&o.altVal) + case yaml.MappingNode: + // set kv to non-nil + o.kv = []orderedMapKV{} + for i := 0; i < len(node.Content); i += 2 { + var key string + var val orderedMap + if err := node.Content[i].Decode(&key); err != nil { + return err + } + if err := node.Content[i+1].Decode(&val); err != nil { + return err + } + o.kv = append(o.kv, orderedMapKV{ + K: key, + V: val, + }) + } + return nil + case yaml.SequenceNode: + // note that this has to be a pointer, so that nulls can be represented. + var res []*orderedMap + if err := node.Decode(&res); err != nil { + return err + } + o.altVal = res + o.kv = nil + return nil + case 0: + // null + o.kv = nil + o.altVal = nil + return nil + default: + return fmt.Errorf("orderedMap: invalid yaml node") + } +} + +func (o *orderedMap) MarshalYAML() (interface{}, error) { + // fast path: kv is nil, use altVal + if o.kv == nil { + return o.altVal, nil + } + content := make([]*yaml.Node, 0, len(o.kv)*2) + for _, val := range o.kv { + n := new(yaml.Node) + if err := n.Encode(val.V); err != nil { + return nil, err + } + content = append(content, &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: val.K, + }, n) + } + return &yaml.Node{ + Kind: yaml.MappingNode, + Tag: "!!map", + Content: content, + }, nil +}