diff --git a/README.md b/README.md index 506432f..0b32dd6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://github.com/hjson/hjson-go/workflows/test/badge.svg)](https://github.com/hjson/hjson-go/actions) [![Go Pkg](https://img.shields.io/github/release/hjson/hjson-go.svg?style=flat-square&label=go-pkg)](https://github.com/hjson/hjson-go/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/hjson/hjson-go?style=flat-square)](https://goreportcard.com/report/github.com/hjson/hjson-go) -[![coverage](https://img.shields.io/badge/coverage-ok-brightgreen.svg?style=flat-square)](http://gocover.io/github.com/hjson/hjson-go/) +[![coverage](https://img.shields.io/badge/coverage-ok-brightgreen.svg?style=flat-square)](https://gocover.io/github.com/hjson/hjson-go/) [![godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/hjson/hjson-go/v4) ![Hjson Intro](https://hjson.github.io/hjson1.gif) @@ -29,9 +29,11 @@ The Go implementation of Hjson is based on [hjson-js](https://github.com/hjson/hjson-js). For other platforms see [hjson.github.io](https://hjson.github.io). +More documentation can be found at https://pkg.go.dev/github.com/hjson/hjson-go/v4 + # Install -Make sure you have a working Go environment. See the [install instructions](http://golang.org/doc/install.html). +Make sure you have a working Go environment. See the [install instructions](https://golang.org/doc/install.html). - In order to use Hjson from your own Go source code, just add an import line like the one here below. Before building your project, run `go mod tidy` in order to download the Hjson source files. The suffix `/v4` is required in the import path, unless you specifically want to use an older major version. ```go @@ -49,8 +51,6 @@ hjson can be used to convert JSON from/to Hjson. hjson will read the given JSON/Hjson input file or read from stdin. Options: - -allowMinusZero - Allow -0. -bracesSameLine Print braces on the same line. -c Output as JSON. @@ -75,12 +75,11 @@ Sample: package main import ( - "github.com/hjson/hjson-go/v4" - "fmt" + "github.com/hjson/hjson-go/v4" + "fmt" ) func main() { - // Now let's look at decoding Hjson data into Go // values. sampleText := []byte(` @@ -98,10 +97,13 @@ func main() { // can put the decoded data. var dat map[string]interface{} - // Decode and a check for errors. + // Decode with default options and check for errors. if err := hjson.Unmarshal(sampleText, &dat); err != nil { panic(err) } + // short for: + // options := hjson.DefaultDecoderOptions() + // err := hjson.UnmarshalWithOptions(sampleText, &dat, options) fmt.Println(dat) // In order to use the values in the decoded map, @@ -118,23 +120,24 @@ func main() { // To encode to Hjson with default options: sampleMap := map[string]int{"apple": 5, "lettuce": 7} hjson, _ := hjson.Marshal(sampleMap) - // this is short for: + // short for: // options := hjson.DefaultOptions() // hjson, _ := hjson.MarshalWithOptions(sampleMap, options) fmt.Println(string(hjson)) } ``` -If you prefer, you can also unmarshal to Go objects by converting to JSON: +## Unmarshal to Go structs + +If you prefer, you can also unmarshal to Go structs (including structs implementing the json.Unmarshaler interface or the encoding.TextUnmarshaler interface). The Go JSON package is used for this, so the same rules apply. Specifically for the "json" key in struct field tags. For more details about this type of unmarshalling, see the [documentation for json.Unmarshal()](https://pkg.go.dev/encoding/json#Unmarshal). ```go package main import ( - "github.com/hjson/hjson-go/v4" - "encoding/json" - "fmt" + "github.com/hjson/hjson-go/v4" + "fmt" ) type Sample struct { @@ -142,8 +145,12 @@ type Sample struct { Array []string } -func main() { +type SampleAlias struct { + Rett int `json:"rate"` + Ashtray []string `json:"array"` +} +func main() { sampleText := []byte(` { # specify rate in requests/second @@ -155,25 +162,72 @@ func main() { ] }`) - // read Hjson - var dat map[string]interface{} - hjson.Unmarshal(sampleText, &dat) - - // convert to JSON - b, _ := json.Marshal(dat) - // unmarshal var sample Sample - json.Unmarshal(b, &sample) + hjson.Unmarshal(sampleText, &sample) fmt.Println(sample.Rate) fmt.Println(sample.Array) + + // unmarshal using json tags on struct fields + var sampleAlias SampleAlias + hjson.Unmarshal(sampleText, &sampleAlias) + + fmt.Println(sampleAlias.Rett) + fmt.Println(sampleAlias.Ashtray) +} +``` + +## Comments on struct fields + +By using key `comment` in struct field tags you can specify comments to be written on one or more lines preceding the struct field in the Hjson output. + +```go + +package main + +import ( + "github.com/hjson/hjson-go/v4" + "fmt" +) + +type foo struct { + A string `json:"x" comment:"First comment"` + B int32 `comment:"Second comment\nLook ma, new lines"` + C string + D int32 +} + +func main() { + a := foo{A: "hi!", B: 3, C: "some text", D: 5} + buf, err := hjson.Marshal(a) + if err != nil { + fmt.Error(err) + } + + fmt.Println(string(buf)) +} +``` + +Output: + +``` +{ + # First comment + x: hi! + + # Second comment + # Look ma, new lines + B: 3 + + C: some text + D: 5 } ``` # API -[![godoc](https://godoc.org/github.com/hjson/hjson-go/v4?status.svg)](http://godoc.org/github.com/hjson/hjson-go/v4) +[![godoc](https://godoc.org/github.com/hjson/hjson-go/v4?status.svg)](https://godoc.org/github.com/hjson/hjson-go/v4) # History diff --git a/decode.go b/decode.go index 701eaa0..f6683e9 100644 --- a/decode.go +++ b/decode.go @@ -2,12 +2,34 @@ package hjson import ( "bytes" + "encoding/json" + "errors" "fmt" "reflect" "strings" ) +// DecoderOptions defines options for decoding Hjson. +type DecoderOptions struct { + // UseJSONNumber causes the Decoder to unmarshal a number into an interface{} as a + // json.Number instead of as a float64. + UseJSONNumber bool + // DisallowUnknownFields causes an error to be returned when the destination + // is a struct and the input contains object keys which do not match any + // non-ignored, exported fields in the destination. + DisallowUnknownFields bool +} + +// DefaultDecoderOptions returns the default decoding options. +func DefaultDecoderOptions() DecoderOptions { + return DecoderOptions{ + UseJSONNumber: false, + DisallowUnknownFields: false, + } +} + type hjsonParser struct { + DecoderOptions data []byte at int // The index of the current character ch byte // The current character @@ -294,7 +316,8 @@ func (p *hjsonParser) readTfnns() (interface{}, error) { } default: if chf == '-' || chf >= '0' && chf <= '9' { - if n, err := tryParseNumber(value.Bytes(), false); err == nil { + // Always use json.Number because we will marshal to JSON. + if n, err := tryParseNumber(value.Bytes(), false, true); err == nil { return n, nil } } @@ -448,33 +471,61 @@ func (p *hjsonParser) checkTrailing(v interface{}, err error) (interface{}, erro return v, nil } -// Unmarshal parses the Hjson-encoded data and stores the result +// Unmarshal parses the Hjson-encoded data using default options and stores the +// result in the value pointed to by v. +// +// See UnmarshalWithOptions. +// +func Unmarshal(data []byte, v interface{}) error { + return UnmarshalWithOptions(data, v, DefaultDecoderOptions()) +} + +// UnmarshalWithOptions parses the Hjson-encoded data and stores the result // in the value pointed to by v. // -// Unmarshal uses the inverse of the encodings that -// Marshal uses, allocating maps, slices, and pointers as necessary. +// Internally the Hjson input is converted to JSON, which is then used as input +// to the function json.Unmarshal(). // -func Unmarshal(data []byte, v interface{}) (err error) { - var value interface{} - parser := &hjsonParser{data, 0, ' '} +// For more details about the output from this function, see the documentation +// for json.Unmarshal(). +func UnmarshalWithOptions(data []byte, v interface{}, options DecoderOptions) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return fmt.Errorf("Cannot unmarshal into non-pointer %v", reflect.TypeOf(v)) + } + + parser := &hjsonParser{ + DecoderOptions: options, + data: data, + at: 0, + ch: ' ', + } parser.resetAt() - value, err = parser.rootValue() + value, err := parser.rootValue() if err != nil { return err } - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Ptr || rv.IsNil() { - return fmt.Errorf("non-pointer %v", reflect.TypeOf(v)) + // Convert to JSON so we can let json.Unmarshal() handle all destination + // types (including interfaces json.Unmarshaler and encoding.TextUnmarshaler) + // and merging. + buf, err := json.Marshal(value) + if err != nil { + return errors.New("Internal error") } - for rv.Kind() == reflect.Ptr { - rv = rv.Elem() + + dec := json.NewDecoder(bytes.NewBuffer(buf)) + if parser.UseJSONNumber { + dec.UseNumber() } - defer func() { - if e := recover(); e != nil { - err = fmt.Errorf("%v", e) - } - }() - rv.Set(reflect.ValueOf(value)) + if parser.DisallowUnknownFields { + dec.DisallowUnknownFields() + } + + err = dec.Decode(v) + if err != nil { + return err + } + return err } diff --git a/encode.go b/encode.go index 6d5317a..1642d2c 100644 --- a/encode.go +++ b/encode.go @@ -2,6 +2,7 @@ package hjson import ( "bytes" + "encoding" "encoding/json" "errors" "fmt" @@ -30,33 +31,35 @@ type EncoderOptions struct { IndentBy string // Base indentation string BaseIndentation string - // Allow the -0 value (unlike ES6) - AllowMinusZero bool - // Encode unknown values as 'null' - UnknownAsNull bool } // DefaultOptions returns the default encoding options. func DefaultOptions() EncoderOptions { - opt := EncoderOptions{} - opt.Eol = "\n" - opt.BracesSameLine = false - opt.EmitRootBraces = true - opt.QuoteAlways = false - opt.QuoteAmbiguousStrings = true - opt.IndentBy = " " - opt.BaseIndentation = "" - opt.AllowMinusZero = false - opt.UnknownAsNull = false - return opt + return EncoderOptions{ + Eol: "\n", + BracesSameLine: false, + EmitRootBraces: true, + QuoteAlways: false, + QuoteAmbiguousStrings: true, + IndentBy: " ", + BaseIndentation: "", + } } +// Start looking for circular references below this depth. +const depthLimit = 1024 + type hjsonEncoder struct { bytes.Buffer // output EncoderOptions - indent int + indent int + pDepth uint + parents map[uintptr]struct{} // Starts to be filled after pDepth has reached depthLimit + structTypeCache map[reflect.Type][]structFieldInfo } +var JSONNumberType = reflect.TypeOf(json.Number("")) + var needsEscape, needsQuotes, needsEscapeML, startsWithKeyword, needsEscapeName *regexp.Regexp func init() { @@ -175,7 +178,7 @@ func (s sortAlpha) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s sortAlpha) Less(i, j int) bool { - return s[i].String() < s[j].String() + return fmt.Sprintf("%v", s[i]) < fmt.Sprintf("%v", s[j]) } func (e *hjsonEncoder) writeIndent(indent int) { @@ -186,17 +189,29 @@ func (e *hjsonEncoder) writeIndent(indent int) { } } -func (e *hjsonEncoder) useMarshaler(value reflect.Value, separator string) error { +func (e *hjsonEncoder) useMarshalerJSON( + value reflect.Value, + noIndent bool, + separator string, + isRootObject bool, +) error { b, err := value.Interface().(json.Marshaler).MarshalJSON() if err != nil { return err } - e.WriteString(separator) - e.WriteString(string(b)) - return nil + + var jsonRoot interface{} + err = Unmarshal(b, &jsonRoot) + if err != nil { + return err + } + + // Output Hjson with our current options, instead of JSON. + return e.str(reflect.ValueOf(jsonRoot), noIndent, separator, isRootObject) } -var marshaler = reflect.TypeOf((*json.Marshaler)(nil)).Elem() +var marshalerJSON = reflect.TypeOf((*json.Marshaler)(nil)).Elem() +var marshalerText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, isRootObject bool) error { @@ -204,6 +219,22 @@ func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, kind := value.Kind() + switch kind { + case reflect.Ptr, reflect.Slice, reflect.Map: + if e.pDepth++; e.pDepth > depthLimit { + if e.parents == nil { + e.parents = map[uintptr]struct{}{} + } + p := value.Pointer() + if _, ok := e.parents[p]; ok { + return errors.New("Circular reference found, pointer of type " + value.Type().String()) + } + e.parents[p] = struct{}{} + defer delete(e.parents, p) + } + defer func() { e.pDepth-- }() + } + if kind == reflect.Interface || kind == reflect.Ptr { if value.IsNil() { e.WriteString(separator) @@ -213,13 +244,31 @@ func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, return e.str(value.Elem(), noIndent, separator, isRootObject) } - if value.Type().Implements(marshaler) { - return e.useMarshaler(value, separator) + if value.Type().Implements(marshalerJSON) { + return e.useMarshalerJSON(value, noIndent, separator, isRootObject) + } + + if value.Type().Implements(marshalerText) { + b, err := value.Interface().(encoding.TextMarshaler).MarshalText() + if err != nil { + return err + } + + return e.str(reflect.ValueOf(string(b)), noIndent, separator, isRootObject) } switch kind { case reflect.String: - e.quote(value.String(), separator, isRootObject) + if value.Type() == JSONNumberType { + n := value.String() + if n == "" { + n = "0" + } + // without quotes + e.WriteString(separator + n) + } else { + e.quote(value.String(), separator, isRootObject) + } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: e.WriteString(separator) @@ -236,7 +285,7 @@ func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, number := value.Float() if math.IsInf(number, 0) || math.IsNaN(number) { e.WriteString("null") - } else if !e.AllowMinusZero && number == -0 { + } else if number == -0 { e.WriteString("0") } else { // find shortest representation ('G' does not work) @@ -289,130 +338,71 @@ func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, e.indent = indent1 case reflect.Map: - - len := value.Len() - if len == 0 { - e.WriteString(separator) - e.WriteString("{}") - break - } - - indent1 := e.indent - if !isRootObject || e.EmitRootBraces { - if !noIndent && !e.BracesSameLine { - e.writeIndent(e.indent) - } else { - e.WriteString(separator) - } - - e.indent++ - e.WriteString("{") - } - + var fis []fieldInfo + useMarshalText := value.Type().Key().Implements(marshalerText) keys := value.MapKeys() sort.Sort(sortAlpha(keys)) - - // Join all of the member texts together, separated with newlines - for i := 0; i < len; i++ { - if i > 0 || !isRootObject || e.EmitRootBraces { - e.writeIndent(e.indent) - } - e.WriteString(e.quoteName(keys[i].String())) - e.WriteString(":") - if err := e.str(value.MapIndex(keys[i]), false, " ", false); err != nil { - return err + for _, key := range keys { + var name string + if useMarshalText { + keyBytes, err := key.Interface().(encoding.TextMarshaler).MarshalText() + if err != nil { + return err + } + name = string(keyBytes) + } else { + name = fmt.Sprintf("%v", key) } + fis = append(fis, fieldInfo{ + field: value.MapIndex(key), + name: name, + }) } - - if !isRootObject || e.EmitRootBraces { - e.writeIndent(indent1) - e.WriteString("}") - } - e.indent = indent1 + return e.writeFields(fis, noIndent, separator, isRootObject) case reflect.Struct: - - l := value.NumField() - if l == 0 { - e.WriteString(separator) - e.WriteString("{}") - break + // Struct field info is identical for all instances of the same type. + // Only the values on the fields can be different. + t := value.Type() + sfis, ok := e.structTypeCache[t] + if !ok { + sfis = getStructFieldInfo(t) + e.structTypeCache[t] = sfis } - indent1 := e.indent - if !isRootObject || e.EmitRootBraces { - if !noIndent && !e.BracesSameLine { - e.writeIndent(e.indent) - } else { - e.WriteString(separator) - } - - e.indent++ - e.WriteString("{") - } - - // Join all of the member texts together, separated with newlines - for i := 0; i < l; i++ { - curStructField := value.Type().Field(i) - curField := value.Field(i) - - name := curStructField.Name - jsonTag := curStructField.Tag.Get("json") - jsonComment := curStructField.Tag.Get("comment") - omitEmpty := false - if jsonTag == "-" { - continue - } - splits := strings.Split(jsonTag, ",") - if splits[0] != "" { - name = splits[0] - } - if len(splits) > 1 { - for _, opt := range splits[1:] { - if opt == "omitempty" { - omitEmpty = true + // Collect fields first, too see if any should be shown (considering + // "omitEmpty"). + var fis []fieldInfo + FieldLoop: + for _, sfi := range sfis { + // The field might be found on the root struct or in embedded structs. + fv := value + for _, i := range sfi.indexPath { + if fv.Kind() == reflect.Ptr { + if fv.IsNil() { + continue FieldLoop } + fv = fv.Elem() } + fv = fv.Field(i) } - if omitEmpty && isEmptyValue(curField) { + + if sfi.omitEmpty && isEmptyValue(fv) { continue } - if len(jsonComment) > 0 { - for _, line := range strings.Split(jsonComment, e.Eol) { - if i > 0 || !isRootObject || e.EmitRootBraces { - e.writeIndent(e.indent) - } - e.WriteString(fmt.Sprintf("# %s", line)) - } - } - if i > 0 || !isRootObject || e.EmitRootBraces { - e.writeIndent(e.indent) - } - e.WriteString(e.quoteName(name)) - e.WriteString(":") - if err := e.str(curField, false, " ", false); err != nil { - return err - } - if len(jsonComment) > 0 && i < l-1 { - e.WriteString(e.Eol) - } - } - if !isRootObject || e.EmitRootBraces { - e.writeIndent(indent1) - e.WriteString("}") + fis = append(fis, fieldInfo{ + field: fv, + name: sfi.name, + comment: sfi.comment, + }) } - - e.indent = indent1 + return e.writeFields(fis, noIndent, separator, isRootObject) default: - if e.UnknownAsNull { - // Use null as a placeholder for non-JSON values. - e.WriteString("null") - } else { - return errors.New("Unsupported type " + value.Type().String()) - } + return errors.New("Unsupported type " + value.Type().String()) } + return nil } @@ -446,19 +436,97 @@ func Marshal(v interface{}) ([]byte, error) { // MarshalWithOptions returns the Hjson encoding of v. // -// Marshal traverses the value v recursively. +// The value v is traversed recursively. // -// Boolean values encode as JSON booleans. +// Boolean values are written as true or false. // -// Floating point, integer, and Number values encode as JSON numbers. +// Floating point, integer, and json.Number values are written as numbers (with +// decimals only if needed, using . as decimals separator). // // String values encode as Hjson strings (quoteless, multiline or // JSON). // -// Array and slice values encode as JSON arrays. +// Array and slice values encode as arrays, surrounded by []. Unlike +// json.Marshal, hjson.Marshal will encode a nil-slice as [] instead of null. +// +// Map values encode as objects, surrounded by {}. The map's key type must be +// possible to print to a string using fmt.Sprintf("%v", key), or implement +// encoding.TextMarshaler. The map keys are sorted alphabetically and +// used as object keys. Unlike json.Marshal, hjson.Marshal will encode a +// nil-map as {} instead of null. +// +// Struct values also encode as objects, surrounded by {}. Only the exported +// fields are encoded to Hjson. The fields will appear in the same order as in +// the struct. +// +// The encoding of each struct field can be customized by the format string +// stored under the "json" key in the struct field's tag. +// The format string gives the name of the field, possibly followed by a comma +// and "omitempty". The name may be empty in order to specify "omitempty" +// without overriding the default field name. +// +// The "omitempty" option specifies that the field should be omitted +// from the encoding if the field has an empty value, defined as +// false, 0, a nil pointer, a nil interface value, and any empty array, +// slice, map, or string. +// +// As a special case, if the field tag is "-", the field is always omitted. +// Note that a field with name "-" can still be generated using the tag "-,". +// +// Comments can be set on struct fields using the "comment" key in the struct +// field's tag. The comment will be written on the line before the field key, +// prefixed with #. Or possible several lines prefixed by #, if there are line +// breaks (\n) in the comment text. +// +// If both the "json" and the "comment" tag keys are used on a struct field +// they should be separated by whitespace. +// +// Examples of struct field tags and their meanings: +// +// // Field appears in Hjson as key "myName". +// Field int `json:"myName"` +// +// // Field appears in Hjson as key "myName" and the field is omitted from +// // the object if its value is empty, as defined above. +// Field int `json:"myName,omitempty"` +// +// // Field appears in Hjson as key "Field" (the default), but the field is +// // skipped if empty. Note the leading comma. +// Field int `json:",omitempty"` // -// Map values encode as JSON objects. The map's key type must be a -// string. The map keys are sorted and used as JSON object keys. +// // Field is ignored by this package. +// Field int `json:"-"` +// +// // Field appears in Hjson as key "-". +// Field int `json:"-,"` +// +// // Field appears in Hjson preceded by a line just containing `# A comment.` +// Field int `comment:"A comment."` +// +// // Field appears in Hjson as key "myName" preceded by a line just +// // containing `# A comment.` +// Field int `json:"myName" comment:"A comment."` +// +// Anonymous struct fields are usually marshaled as if their inner exported fields +// were fields in the outer struct, subject to the usual Go visibility rules amended +// as described in the next paragraph. +// An anonymous struct field with a name given in its JSON tag is treated as +// having that name, rather than being anonymous. +// An anonymous struct field of interface type is treated the same as having +// that type as its name, rather than being anonymous. +// +// The Go visibility rules for struct fields are amended for JSON when +// deciding which field to marshal or unmarshal. If there are +// multiple fields at the same level, and that level is the least +// nested (and would therefore be the nesting level selected by the +// usual Go rules), the following extra rules apply: +// +// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, +// even if there are multiple untagged fields that would otherwise conflict. +// +// 2) If there is exactly one field (tagged or not according to the first rule), that is selected. +// +// 3) Otherwise there are multiple fields, and all are ignored; no error occurs. // // Pointer values encode as the value pointed to. // A nil pointer encodes as the null JSON value. @@ -466,20 +534,26 @@ func Marshal(v interface{}) ([]byte, error) { // Interface values encode as the value contained in the interface. // A nil interface value encodes as the null JSON value. // -// JSON cannot represent cyclic data structures and Marshal does not -// handle them. Passing cyclic structures to Marshal will result in -// an infinite recursion. +// If an encountered value implements the json.Marshaler interface then the +// function MarshalJSON() is called on it. The JSON is then converted to Hjson +// using the current indentation and options given in the call to json.Marshal(). +// +// If an encountered value implements the encoding.TextMarshaler interface +// but not the json.Marshaler interface, then the function MarshalText() is +// called on it to get a text. +// +// Channel, complex, and function values cannot be encoded in Hjson, will +// result in an error. +// +// Hjson cannot represent cyclic data structures and Marshal does not handle +// them. Passing cyclic structures to Marshal will result in an error. // func MarshalWithOptions(v interface{}, options EncoderOptions) ([]byte, error) { - e := &hjsonEncoder{} - e.indent = 0 - e.Eol = options.Eol - e.BracesSameLine = options.BracesSameLine - e.EmitRootBraces = options.EmitRootBraces - e.QuoteAlways = options.QuoteAlways - e.QuoteAmbiguousStrings = options.QuoteAmbiguousStrings - e.IndentBy = options.IndentBy - e.BaseIndentation = options.BaseIndentation + e := &hjsonEncoder{ + indent: 0, + EncoderOptions: options, + structTypeCache: map[reflect.Type][]structFieldInfo{}, + } err := e.str(reflect.ValueOf(v), true, e.BaseIndentation, true) if err != nil { diff --git a/encode_test.go b/encode_test.go index 71965a8..359b002 100644 --- a/encode_test.go +++ b/encode_test.go @@ -1,8 +1,13 @@ package hjson import ( + "bytes" + "encoding/json" + "fmt" + "net" "reflect" "testing" + "time" ) func marshalUnmarshalExpected( @@ -20,7 +25,9 @@ func marshalUnmarshalExpected( t.Errorf("Expected:\n%s\nGot:\n%s\n\n", expectedHjson, string(buf)) } - err = Unmarshal(buf, dst) + decOpt := DefaultDecoderOptions() + decOpt.DisallowUnknownFields = true + err = UnmarshalWithOptions(buf, dst, decOpt) if err != nil { t.Error(err) } @@ -37,6 +44,7 @@ type TestStruct struct { E string `json:"-"` F string `json:"-,"` G string `json:"H,omitempty"` + J string `json:"J,omitempty"` U int `json:",omitempty"` V uint `json:",omitempty"` W float32 `json:",omitempty"` @@ -65,6 +73,7 @@ func TestEncodeStruct(t *testing.T) { E: "baz", F: "qux", G: "thud", + J: "<", U: 3, V: 4, W: 5.0, @@ -72,10 +81,14 @@ func TestEncodeStruct(t *testing.T) { Y: []int{1, 2, 3}, Z: &TestStruct{}, } + buf, err := Marshal(input) if err != nil { t.Error(err) } + if !bytes.Contains(buf, []byte("J: <\n")) { + t.Errorf("Missing 'J: <' in marshal output:\n%s", string(buf)) + } err = Unmarshal(buf, &output) if err != nil { t.Error(err) @@ -87,6 +100,7 @@ func TestEncodeStruct(t *testing.T) { checkKeyValue(t, output, "-", "qux") checkKeyValue(t, output, "H", "thud") checkMissing(t, output, "C") + checkKeyValue(t, output, "J", "<") checkMissing(t, output, "E") checkMissing(t, output, "F") checkKeyValue(t, output, "U", 3.0) @@ -112,6 +126,7 @@ func TestEncodeStruct(t *testing.T) { if err != nil { t.Error(err) } + output = map[string]interface{}{} err = Unmarshal(buf, &output) if err != nil { t.Error(err) @@ -148,6 +163,144 @@ func checkMissing(t *testing.T, m map[string]interface{}, key string) { } } +func TestAnonymousStruct1(t *testing.T) { + type TestStruct2 struct { + TestStruct + Q int + } + + ts2 := TestStruct2{ + Q: 4, + } + ts2.D = "ddd" + + ts3 := ts2 + + marshalUnmarshalExpected(t, `{ + A: 0 + B: 0 + S: "" + D: ddd + -: "" + Q: 4 +}`, &ts3, &ts2, &TestStruct2{}) +} + +func TestAnonymousStruct2(t *testing.T) { + type TestStruct2 struct { + TestStruct `json:"subObj,omitempty"` + Q int + } + + ts2 := TestStruct2{ + Q: 4, + } + ts2.D = "ddd" + + marshalUnmarshalExpected(t, `{ + subObj: + { + A: 0 + B: 0 + S: "" + D: ddd + -: "" + } + Q: 4 +}`, &ts2, &ts2, &TestStruct2{}) +} + +func TestAnonymousStruct3(t *testing.T) { + type S1 struct { + someField string `json:"smFil"` + Afi int + Bfi int `json:"BFI"` + Cfi int + Dfi int `json:"Dfi"` + } + + type S2 struct { + OtherField int `json:",omitempty"` + Afi int + Bfi int + Dfi int + } + + type s4 struct { + YesIncluded bool + OmittedBool bool `json:",omitempty"` + } + + type S3 struct { + S1 + S2 + s4 + Cfi int `json:"-"` + } + + ts := S3{ + Cfi: 4, + } + ts.S1.Afi = 3 + ts.S2.Afi = 5 + ts.S1.Bfi = 7 + ts.S2.Bfi = 8 + ts.S1.Cfi = 9 + ts.S1.Dfi = 11 + ts.S2.Dfi = 22 + + ts2 := ts + ts2.S1.Afi = 0 + ts2.S2.Afi = 0 + ts2.S2.Dfi = 0 + ts2.Cfi = 0 + + marshalUnmarshalExpected(t, `{ + BFI: 7 + Cfi: 9 + Dfi: 11 + Bfi: 8 + YesIncluded: false +}`, &ts2, &ts, &S3{}) +} + +func TestAnonymousStruct4(t *testing.T) { + type S2 struct { + S2Field int + } + type S1 struct { + S2 + Anon struct { + S2 + ReallyAnonymous int + } + S2a S2 + S2b S2 + S2c S2 + } + + marshalUnmarshalExpected(t, `{ + S2Field: 0 + Anon: + { + S2Field: 0 + ReallyAnonymous: 0 + } + S2a: + { + S2Field: 0 + } + S2b: + { + S2Field: 0 + } + S2c: + { + S2Field: 0 + } +}`, &S1{}, &S1{}, &S1{}) +} + func TestEmptyMapsAndSlices(t *testing.T) { type S2 struct { S2Field int @@ -201,29 +354,188 @@ func TestEmptyMapsAndSlices(t *testing.T) { }`, &ts2, &ts3, &ds3) } +func TestStructPointers(t *testing.T) { + type S2 struct { + S2Field int + } + + type S1 struct { + MapNil map[string]interface{} + MapEmpty map[string]interface{} + IntSliceNil []int + IntSliceEmpty []int + S2Pointer *S2 + } + ts := S1{ + MapEmpty: map[string]interface{}{}, + IntSliceEmpty: []int{}, + } + + ts2 := ts + ts2.MapNil = map[string]interface{}{} + ts2.IntSliceNil = []int{} + + marshalUnmarshalExpected(t, `{ + MapNil: {} + MapEmpty: {} + IntSliceNil: [] + IntSliceEmpty: [] + S2Pointer: null +}`, &ts2, &ts, &S1{}) +} + type TestMarshalStruct struct { TestStruct } func (s TestMarshalStruct) MarshalJSON() ([]byte, error) { - return []byte(`"foobar"`), nil + return []byte(`{ + "arr": [ + "foo", + "bar" + ], + "map": { + "key1": 1, + "key2": "B" + } +}`), nil } -func TestEncodeMarshal(t *testing.T) { +func TestEncodeMarshalJSON(t *testing.T) { input := TestMarshalStruct{} + expected1 := `{ + arr: + [ + foo + bar + ] + map: + { + key1: 1 + key2: B + } +}` buf, err := Marshal(input) if err != nil { t.Error(err) } - if !reflect.DeepEqual(buf, []byte(`"foobar"`)) { - t.Error("Marshaler interface error") + if string(buf) != expected1 { + t.Errorf("Expected:\n%s\nGot:\n%s\n\n", expected1, string(buf)) } + buf, err = Marshal(&input) if err != nil { t.Error(err) } - if !reflect.DeepEqual(buf, []byte(`"foobar"`)) { - t.Error("Marshaler interface error") + if string(buf) != expected1 { + t.Errorf("Expected:\n%s\nGot:\n%s\n\n", expected1, string(buf)) + } + + myMap := map[string]interface{}{ + "Zero": -0, + "A": "FirstField", + "B": TestMarshalStruct{}, + "C": struct { + D string + Zero int + }{ + D: "struct field", + Zero: -0, + }, + } + buf, err = Marshal(&myMap) + if err != nil { + t.Error(err) + } + expected2 := `{ + A: FirstField + B: + { + arr: + [ + foo + bar + ] + map: + { + key1: 1 + key2: B + } + } + C: + { + D: struct field + Zero: 0 + } + Zero: 0 +}` + if string(buf) != expected2 { + t.Errorf("Expected:\n%s\nGot:\n%s\n\n", expected2, string(buf)) + } +} + +func TestEncodeMarshalText(t *testing.T) { + input := net.ParseIP("127.0.0.1") + buf, err := Marshal(input) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(buf, []byte(`127.0.0.1`)) { + t.Errorf("Expected '127.0.0.1', got '%s'", string(buf)) + } + buf, err = Marshal(&input) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(buf, []byte(`127.0.0.1`)) { + t.Errorf("Expected '127.0.0.1', got '%s'", string(buf)) + } +} + +type marshallerStruct struct { + A int +} + +func (s marshallerStruct) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("key%d", s.A)), nil +} + +func TestEncodeMarshalTextMapKey(t *testing.T) { + input := map[marshallerStruct]int{ + marshallerStruct{1}: 11, + marshallerStruct{2}: 22, + } + expectedUnmarshal := map[string]interface{}{ + "key1": 11.0, + "key2": 22.0, + } + marshalUnmarshalExpected(t, `{ + key1: 11 + key2: 22 +}`, &expectedUnmarshal, &input, &map[string]interface{}{}) +} + +type TestMarshalInt int + +func (s TestMarshalInt) MarshalJSON() ([]byte, error) { + return []byte(`"foobar"`), nil +} + +func TestEncodeMarshalInt(t *testing.T) { + var input TestMarshalInt + buf, err := Marshal(input) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(buf, []byte(`foobar`)) { + t.Errorf("Expected '\"foobar\"', got '%s'", string(buf)) + } + buf, err = Marshal(&input) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(buf, []byte(`foobar`)) { + t.Errorf("Expected '\"foobar\"', got '%s'", string(buf)) } } @@ -317,15 +629,17 @@ func TestQuoteAmbiguousStrings(t *testing.T) { func marshalUnmarshal(t *testing.T, input string) { buf, err := Marshal(input) if err != nil { - t.Fatal(err) + t.Error(err) + return } var resultPlain string err = Unmarshal(buf, &resultPlain) if err != nil { - t.Fatal(err) + t.Error(err) + return } if resultPlain != input { - t.Fatalf("Expected: '%v' Got: '%v'\n", []byte(input), []byte(resultPlain)) + t.Errorf("Expected: '%v' Got: '%v'\n", []byte(input), []byte(resultPlain)) } type t_obj struct { @@ -336,15 +650,17 @@ func marshalUnmarshal(t *testing.T, input string) { } buf, err = Marshal(obj) if err != nil { - t.Fatal(err) + t.Error(err) + return } var out map[string]interface{} err = Unmarshal(buf, &out) if err != nil { - t.Fatal(err) + t.Error(err) + return } if out["F"] != input { - t.Fatalf("Expected: '%v' Got: '%v'\n", []byte(input), []byte(out["F"].(string))) + t.Errorf("Expected: '%v' Got: '%v'\n", []byte(input), []byte(out["F"].(string))) } } @@ -358,3 +674,160 @@ func TestMarshalUnmarshal(t *testing.T) { marshalUnmarshal(t, "0\r\n'") marshalUnmarshal(t, "0\r\n") } + +func TestCircularReference(t *testing.T) { + timeout := time.After(3 * time.Second) + done := make(chan bool) + go func() { + type Node struct { + Self *Node + } + var obj Node + obj.Self = &obj + _, err := Marshal(obj) + if err == nil { + t.Error("No error returned for circular reference") + } + done <- true + }() + + select { + case <-timeout: + t.Error("The circular reference test is taking too long, is probably stuck in an infinite loop.") + case <-done: + } +} + +func TestPrivateStructFields(t *testing.T) { + obj := struct { + somePrivateField string + }{ + "TEST", + } + b, err := Marshal(obj) + if err != nil { + t.Error(err) + return + } + if string(b) != "{}" { + t.Errorf("Expected '{}', got '%s'", string(b)) + } +} + +func TestMarshalDuplicateFields(t *testing.T) { + type A struct { + B int `json:"rate"` + C []string `json:"rate"` + } + + a := A{ + B: 3, + C: []string{"D", "E"}, + } + + buf, err := Marshal(&a) + if err != nil { + t.Error(err) + } + expected := `{}` + if string(buf) != expected { + t.Errorf("Expected:\n%s\n\nGot:\n%s\n", expected, string(buf)) + } + + var a2 A + err = Unmarshal([]byte("rate: 5"), &a2) + if err != nil { + t.Error(err) + } + // json.Unmarshal will not write at all to fields with duplicate names. + if !reflect.DeepEqual(a2, A{}) { + t.Errorf("Expected empty struct") + } + + type B struct { + B int `json:"rate"` + } + var b B + err = Unmarshal([]byte("rate: 5"), &b) + if err != nil { + t.Error(err) + } + if b.B != 5 { + t.Errorf("Expected 5, got %d\n", b.B) + } +} + +func TestMarshalMapIntKey(t *testing.T) { + m := map[int]bool{ + 3: true, + } + buf, err := Marshal(m) + if err != nil { + t.Error(err) + } + expected := `{ + 3: true +}` + if string(buf) != expected { + t.Errorf("Expected:\n%s\n\nGot:\n%s\n", expected, string(buf)) + } + + m2 := map[int]bool{} + err = Unmarshal(buf, &m2) + if err != nil { + t.Error(err) + } + if !m2[3] { + t.Errorf("Failed to unmarshal into map with int key") + } +} + +func TestMarshalJsonNumber(t *testing.T) { + var n json.Number + buf, err := Marshal(n) + if err != nil { + t.Error(err) + } + expected := `0` + if string(buf) != expected { + t.Errorf("Expected:\n%s\n\nGot:\n%s\n", expected, string(buf)) + } + + n = json.Number("3e5") + buf, err = Marshal(n) + if err != nil { + t.Error(err) + } + expected = `3e5` + if string(buf) != expected { + t.Errorf("Expected:\n%s\n\nGot:\n%s\n", expected, string(buf)) + } +} + +func TestStructComment(t *testing.T) { + type foo struct { + A string `json:"x" comment:"First comment"` + B int32 `comment:"Second comment\nLook ma, new lines"` + C string + D int32 + } + a := foo{A: "hi!", B: 3, C: "some text", D: 5} + h, err := Marshal(a) + if err != nil { + t.Error(err) + } + expected := `{ + # First comment + x: hi! + + # Second comment + # Look ma, new lines + B: 3 + + C: some text + D: 5 +}` + if string(h) != expected { + t.Errorf("Expected:\n%s\nGot:\n%s\n\n", expected, string(h)) + } +} diff --git a/hjson-cli/main.go b/hjson-cli/main.go index 2c3b571..af94aa7 100644 --- a/hjson-cli/main.go +++ b/hjson-cli/main.go @@ -5,9 +5,10 @@ import ( "encoding/json" "flag" "fmt" - "github.com/hjson/hjson-go/v4" "io/ioutil" "os" + + "github.com/hjson/hjson-go/v4" ) func fixJSON(data []byte) []byte { @@ -39,7 +40,6 @@ func main() { var bracesSameLine = flag.Bool("bracesSameLine", false, "Print braces on the same line.") var omitRootBraces = flag.Bool("omitRootBraces", false, "Omit braces at the root.") var quoteAlways = flag.Bool("quoteAlways", false, "Always quote string values.") - var allowMinusZero = flag.Bool("allowMinusZero", false, "Allow -0.") // var showVersion = flag.Bool("V", false, "Show version.") @@ -85,7 +85,6 @@ func main() { opt.BracesSameLine = *bracesSameLine opt.EmitRootBraces = !*omitRootBraces opt.QuoteAlways = *quoteAlways - opt.AllowMinusZero = *allowMinusZero out, err = hjson.MarshalWithOptions(value, opt) if err != nil { panic(err) diff --git a/hjson_test.go b/hjson_test.go index e6b697d..cac1885 100644 --- a/hjson_test.go +++ b/hjson_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/ioutil" + "math" "os" "path/filepath" "strings" @@ -102,10 +103,32 @@ func TestHjson(t *testing.T) { } func TestInvalidDestinationType(t *testing.T) { + input := []byte(`[1,2,3,4]`) var dat map[string]interface{} - err := Unmarshal([]byte(`[1,2,3,4]`), &dat) + err := Unmarshal(input, &dat) if err == nil { - panic("An error should occur") + t.Errorf("Should have failed when trying to unmarshal an array to a map.") + } + + err = Unmarshal(input, 3) + if err == nil { + t.Errorf("Should have failed when trying to unmarshal into non-pointer.") + } +} + +func TestStructDestinationType(t *testing.T) { + var obj struct { + A int + B int + C string + D string + } + err := Unmarshal([]byte("A: 1\nB:2\nC: \u003c\nD: <"), &obj) + if err != nil { + t.Error(err) + } + if obj.A != 1 || obj.B != 2 || obj.C != "<" || obj.D != "<" { + t.Errorf("Unexpected obj values: %+v", obj) } } @@ -116,3 +139,132 @@ func TestNilValue(t *testing.T) { panic("Passing v = to Unmarshal should return an error") } } + +func TestReadmeUnmarshalToStruct(t *testing.T) { + type Sample struct { + Rate int + Array []string + } + + type SampleAlias struct { + Rett int `json:"rate"` + Ashtray []string `json:"array"` + } + + sampleText := []byte(` +{ + # specify rate in requests/second + rate: 1000 + array: + [ + foo + bar + ] +}`) + + { + var sample Sample + Unmarshal(sampleText, &sample) + if sample.Rate != 1000 || sample.Array[0] != "foo" { + t.Errorf("Unexpected sample values: %+v", sample) + } + } + + { + var sampleAlias SampleAlias + Unmarshal(sampleText, &sampleAlias) + if sampleAlias.Rett != 1000 || sampleAlias.Ashtray[0] != "foo" { + t.Errorf("Unexpected sampleAlias values: %+v", sampleAlias) + } + } +} + +func TestUnknownFields(t *testing.T) { + v := struct { + B string + C int + }{} + b := []byte("B: b\nC: 3\nD: 4\n") + err := Unmarshal(b, &v) + if err != nil { + t.Error(err) + } + err = UnmarshalWithOptions(b, &v, DecoderOptions{DisallowUnknownFields: true}) + if err == nil { + t.Errorf("Should have returned error for unknown field D") + } +} + +type MyUnmarshaller struct { + A string + x string +} + +func (c *MyUnmarshaller) UnmarshalJSON(in []byte) error { + var out map[string]interface{} + err := Unmarshal(in, &out) + if err != nil { + return err + } + a, ok := out["A"] + if !ok { + return errors.New("Missing key") + } + b, ok := a.(string) + if !ok { + return errors.New("Not a string") + } + c.x = b + return nil +} + +func TestUnmarshalInterface(t *testing.T) { + var obj MyUnmarshaller + err := Unmarshal([]byte("A: test"), &obj) + if err != nil { + t.Error(err) + } + if obj.A != "" || obj.x != "test" { + t.Errorf("Unexpected obj values: %+v", obj) + } +} + +func TestJSONNumber(t *testing.T) { + var v interface{} + b := []byte("35e-7") + err := UnmarshalWithOptions(b, &v, DecoderOptions{UseJSONNumber: true}) + if err != nil { + t.Error(err) + } + if v.(json.Number).String() != string(b) { + t.Errorf("Expected %s, got %v\n", string(b), v) + } + + b2, err := Marshal(v) + if err != nil { + t.Error(err) + } + if string(b2) != string(b) { + t.Errorf("Expected %s, got %v\n", string(b), string(b2)) + } + + var n json.Number + err = Unmarshal(b, &n) + if err != nil { + t.Error(err) + } + if n.String() != string(b) { + t.Errorf("Expected %s, got %v\n", string(b), n) + } + f, err := n.Float64() + if err != nil { + t.Error(err) + } + if math.Abs(f-35e-7) > 1e-7 { + t.Errorf("Expected %f, got %f\n", 35e-7, f) + } + _, err = n.Int64() + if err == nil { + t.Errorf("Did not expect %v to be parsable to int64", n) + } +} diff --git a/parseNumber.go b/parseNumber.go index 2e66f50..7523fae 100644 --- a/parseNumber.go +++ b/parseNumber.go @@ -1,6 +1,7 @@ package hjson import ( + "encoding/json" "errors" "math" "strconv" @@ -36,16 +37,20 @@ func (p *parseNumber) peek(offs int) byte { } func startsWithNumber(text []byte) bool { - if _, err := tryParseNumber(text, true); err == nil { + if _, err := tryParseNumber(text, true, false); err == nil { return true } return false } -func tryParseNumber(text []byte, stopAtNext bool) (float64, error) { +func tryParseNumber(text []byte, stopAtNext, useJSONNumber bool) (interface{}, error) { // Parse a number value. - p := parseNumber{text, 0, ' '} + p := parseNumber{ + data: text, + at: 0, + ch: ' ', + } leadingZeros := 0 testLeading := true p.next() @@ -97,6 +102,9 @@ func tryParseNumber(text []byte, stopAtNext bool) (float64, error) { if p.ch > 0 || leadingZeros != 0 { return 0, errors.New("Invalid number") } + if useJSONNumber { + return json.Number(string(p.data[0 : end-1])), nil + } number, err := strconv.ParseFloat(string(p.data[0:end-1]), 64) if err != nil { return 0, err diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..29e41e7 --- /dev/null +++ b/structs.go @@ -0,0 +1,269 @@ +package hjson + +import ( + "fmt" + "reflect" + "sort" + "strings" +) + +type fieldInfo struct { + field reflect.Value + name string + comment string +} + +type structFieldInfo struct { + name string + tagged bool + comment string + omitEmpty bool + indexPath []int +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's embedding rules, modified by the presence of +// JSON tags. If there are multiple top-level fields, the boolean +// will be false: This condition is an error in Go and we skip all +// the fields. +func dominantField(fields []structFieldInfo) (structFieldInfo, bool) { + // The fields are sorted in increasing index-length order, then by presence of tag. + // That means that the first field is the dominant one. We need only check + // for error cases: two fields at top level, either both tagged or neither tagged. + if len(fields) > 1 && len(fields[0].indexPath) == len(fields[1].indexPath) && fields[0].tagged == fields[1].tagged { + return structFieldInfo{}, false + } + return fields[0], true +} + +// byIndex sorts by index sequence. +type byIndex []structFieldInfo + +func (x byIndex) Len() int { return len(x) } + +func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byIndex) Less(i, j int) bool { + for k, xik := range x[i].indexPath { + if k >= len(x[j].indexPath) { + return false + } + if xik != x[j].indexPath[k] { + return xik < x[j].indexPath[k] + } + } + return len(x[i].indexPath) < len(x[j].indexPath) +} + +func getStructFieldInfo(rootType reflect.Type) []structFieldInfo { + type structInfo struct { + typ reflect.Type + indexPath []int + } + var sfis []structFieldInfo + structsToInvestigate := []structInfo{structInfo{typ: rootType}} + // Struct types already visited at an earlier depth. + visited := map[reflect.Type]bool{} + // Count the number of specific struct types on a specific depth. + typeDepthCount := map[reflect.Type]int{} + + for len(structsToInvestigate) > 0 { + curStructs := structsToInvestigate + structsToInvestigate = []structInfo{} + curTDC := typeDepthCount + typeDepthCount = map[reflect.Type]int{} + + for _, curStruct := range curStructs { + if visited[curStruct.typ] { + // The struct type has already appeared on an earlier depth. Fields on + // an earlier depth always have precedence over fields with identical + // name on a later depth, so no point in investigating this type again. + continue + } + visited[curStruct.typ] = true + + for i := 0; i < curStruct.typ.NumField(); i++ { + sf := curStruct.typ.Field(i) + + if sf.Anonymous { + t := sf.Type + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + // If the field is not exported and not a struct. + if sf.PkgPath != "" && t.Kind() != reflect.Struct { + // Ignore embedded fields of unexported non-struct types. + continue + } + // Do not ignore embedded fields of unexported struct types + // since they may have exported fields. + } else if sf.PkgPath != "" { + // Ignore unexported non-embedded fields. + continue + } + + jsonTag := sf.Tag.Get("json") + if jsonTag == "-" { + continue + } + + sfi := structFieldInfo{ + name: sf.Name, + comment: sf.Tag.Get("comment"), + } + + splits := strings.Split(jsonTag, ",") + if splits[0] != "" { + sfi.name = splits[0] + sfi.tagged = true + } + if len(splits) > 1 { + for _, opt := range splits[1:] { + if opt == "omitempty" { + sfi.omitEmpty = true + } + } + } + + sfi.indexPath = make([]int, len(curStruct.indexPath)+1) + copy(sfi.indexPath, curStruct.indexPath) + sfi.indexPath[len(curStruct.indexPath)] = i + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + // Follow pointer. + ft = ft.Elem() + } + + // If the current field should be included. + if sfi.tagged || !sf.Anonymous || ft.Kind() != reflect.Struct { + sfis = append(sfis, sfi) + if curTDC[curStruct.typ] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + sfis = append(sfis, sfi) + } + continue + } + + // Record new anonymous struct to explore in next round. + typeDepthCount[ft]++ + if typeDepthCount[ft] == 1 { + structsToInvestigate = append(structsToInvestigate, structInfo{ + typ: ft, + indexPath: sfi.indexPath, + }) + } + } + } + } + + sort.Slice(sfis, func(i, j int) bool { + // sort field by name, breaking ties with depth, then + // breaking ties with "name came from json tag", then + // breaking ties with index sequence. + if sfis[i].name != sfis[j].name { + return sfis[i].name < sfis[j].name + } + if len(sfis[i].indexPath) != len(sfis[j].indexPath) { + return len(sfis[i].indexPath) < len(sfis[j].indexPath) + } + if sfis[i].tagged != sfis[j].tagged { + return sfis[i].tagged + } + return byIndex(sfis).Less(i, j) + }) + + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with JSON tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := sfis[:0] + for advance, i := 0, 0; i < len(sfis); i += advance { + // One iteration per name. + // Find the sequence of sfis with the name of this first field. + sfi := sfis[i] + name := sfi.name + for advance = 1; i+advance < len(sfis); advance++ { + fj := sfis[i+advance] + if fj.name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, sfi) + continue + } + dominant, ok := dominantField(sfis[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + sfis = out + sort.Sort(byIndex(sfis)) + + return sfis +} + +func (e *hjsonEncoder) writeFields( + fis []fieldInfo, + noIndent bool, + separator string, + isRootObject bool, +) error { + if len(fis) == 0 { + e.WriteString(separator) + e.WriteString("{}") + return nil + } + + indent1 := e.indent + if !isRootObject || e.EmitRootBraces { + if !noIndent && !e.BracesSameLine { + e.writeIndent(e.indent) + } else { + e.WriteString(separator) + } + + e.indent++ + e.WriteString("{") + } + + // Join all of the member texts together, separated with newlines + for i, fi := range fis { + if len(fi.comment) > 0 { + for _, line := range strings.Split(fi.comment, e.Eol) { + if i > 0 || !isRootObject || e.EmitRootBraces { + e.writeIndent(e.indent) + } + e.WriteString(fmt.Sprintf("# %s", line)) + } + } + if i > 0 || !isRootObject || e.EmitRootBraces { + e.writeIndent(e.indent) + } + e.WriteString(e.quoteName(fi.name)) + e.WriteString(":") + if err := e.str(fi.field, false, " ", false); err != nil { + return err + } + if len(fi.comment) > 0 && i < len(fis)-1 { + e.WriteString(e.Eol) + } + } + + if !isRootObject || e.EmitRootBraces { + e.writeIndent(indent1) + e.WriteString("}") + } + + e.indent = indent1 + + return nil +}