From a125495eec4b5d93111a010c31cd79c8a79c615d Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 7 Oct 2020 18:06:11 +0200 Subject: [PATCH] keep order of keys when json marshalling --- pkg/yqlib/encoder.go | 151 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/pkg/yqlib/encoder.go b/pkg/yqlib/encoder.go index 5694722c..0eb0dfcf 100644 --- a/pkg/yqlib/encoder.go +++ b/pkg/yqlib/encoder.go @@ -3,6 +3,7 @@ package yqlib import ( "bytes" "encoding/json" + "fmt" "io" yaml "gopkg.in/yaml.v3" @@ -87,7 +88,7 @@ func NewJsonEncoder(destination io.Writer, prettyPrint bool, indent int) Encoder } func (je *jsonEncoder) Encode(node *yaml.Node) error { - var dataBucket interface{} + var dataBucket orderedMap // firstly, convert all map keys to strings mapKeysToStrings(node) errorDecoding := node.Decode(&dataBucket) @@ -96,3 +97,151 @@ func (je *jsonEncoder) Encode(node *yaml.Node) error { } return je.encoder.Encode(dataBucket) } + +// 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 +} + +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 != io.EOF; 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 && err != io.EOF { + return err + } + return nil + case '[': + var arr []orderedMap + return json.Unmarshal(data, &arr) + } + + return json.Unmarshal(data, &o.altVal) +} + +func (o orderedMap) MarshalJSON() ([]byte, error) { + if o.kv == nil { + return json.Marshal(o.altVal) + } + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + buf.WriteByte('{') + for idx, el := range o.kv { + if err := enc.Encode(el.K); err != nil { + return nil, err + } + buf.WriteByte(':') + enc.Encode(el.V) + 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: + 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 +}