diff --git a/encode.go b/encode.go index 86cda18..700bebb 100644 --- a/encode.go +++ b/encode.go @@ -106,7 +106,7 @@ func (e *encoder) writeEmptyBlock(name string) (err error) { return e.writeByte('\n') } -func (e *encoder) writeMergedBlock(name string, vals []interface{}, path string) (err error) { +func (e *encoder) writeMergedBlock(name string, vals []any, path string) (err error) { if err = e.writeIndent(); err != nil { return err } diff --git a/merge.go b/merge.go index a5b011a..087c240 100644 --- a/merge.go +++ b/merge.go @@ -10,7 +10,7 @@ import ( ) //nolint:unused -func mergeValues(a, b interface{}) (interface{}, error) { +func mergeValues(a, b any) (any, error) { if a == nil && b == nil { return nil, nil } @@ -55,10 +55,10 @@ func mergeValues(a, b interface{}) (interface{}, error) { // Если типы не совпадают (и не обе структуры) -> список if baseA.Type() != baseB.Type() { - return []interface{}{a, b}, nil + return []any{a, b}, nil } - var result interface{} + var result any var err error switch baseA.Kind() { case reflect.Struct: @@ -96,7 +96,7 @@ func derefValue(v reflect.Value) reflect.Value { } //nolint:unused -func mergeStruct(va, vb reflect.Value) (interface{}, error) { +func mergeStruct(va, vb reflect.Value) (any, error) { t := va.Type() result := reflect.New(t).Elem() result.Set(va) @@ -124,7 +124,7 @@ func mergeStruct(va, vb reflect.Value) (interface{}, error) { } //nolint:unused -func mergeMap(va, vb reflect.Value) (interface{}, error) { +func mergeMap(va, vb reflect.Value) (any, error) { t := va.Type() result := reflect.MakeMap(t) @@ -151,7 +151,7 @@ func mergeMap(va, vb reflect.Value) (interface{}, error) { } //nolint:unused,unparam -func mergeSlice(va, vb reflect.Value) (interface{}, error) { +func mergeSlice(va, vb reflect.Value) (any, error) { totalLen := va.Len() + vb.Len() result := reflect.MakeSlice(va.Type(), totalLen, totalLen) reflect.Copy(result, va) @@ -160,7 +160,7 @@ func mergeSlice(va, vb reflect.Value) (interface{}, error) { } //nolint:unused -func mergeArray(va, vb reflect.Value) (interface{}, error) { +func mergeArray(va, vb reflect.Value) (any, error) { if va.Len() != vb.Len() { return vb.Interface(), nil } @@ -177,13 +177,13 @@ func mergeArray(va, vb reflect.Value) (interface{}, error) { } //nolint:unused -func structToMap(v reflect.Value) (map[string]interface{}, error) { +func structToMap(v reflect.Value) (map[string]any, error) { v = derefValue(v) if v.Kind() != reflect.Struct { return nil, errNotStruct } t := v.Type() - result := make(map[string]interface{}) + result := make(map[string]any) for i := 0; i < t.NumField(); i++ { field := t.Field(i) if field.PkgPath != "" && !field.Anonymous { @@ -218,7 +218,7 @@ func structToMap(v reflect.Value) (map[string]interface{}, error) { } //nolint:unused -func mergeMapValues(a, b map[string]interface{}) (map[string]interface{}, error) { +func mergeMapValues(a, b map[string]any) (map[string]any, error) { va := reflect.ValueOf(a) vb := reflect.ValueOf(b) if va.Kind() != reflect.Map || vb.Kind() != reflect.Map { @@ -228,7 +228,7 @@ func mergeMapValues(a, b map[string]interface{}) (map[string]interface{}, error) if err != nil { return nil, err } - return mergedVal.(map[string]interface{}), nil + return mergedVal.(map[string]any), nil } func hasAttrs(t reflect.Type) (bool, error) { diff --git a/unic.go b/unic.go index 05e6251..9c03b21 100644 --- a/unic.go +++ b/unic.go @@ -76,12 +76,13 @@ func decodeTarget(v any) (reflect.Value, error) { // Marshal returns the UNIC encoding of v. // v must be a struct or a non-nil pointer to struct. -func Marshal(args ...any) ([]byte, error) { +func Marshal(args ...any) ([]byte, error) { //nolint:gocyclo if len(args) == 0 { return nil, nil } - fieldsMap := make(map[string][]interface{}) + fieldsMap := make(map[string][]any) + metaList := make([]*structMeta, 0, len(args)) for _, arg := range args { rv, err := encodeTarget(arg) if err != nil { @@ -93,6 +94,8 @@ func Marshal(args ...any) ([]byte, error) { return nil, err } + metaList = append(metaList, meta) + for _, bf := range meta.fields { if bf.attr > 0 { continue @@ -105,30 +108,56 @@ func Marshal(args ...any) ([]byte, error) { } } - mergedGroups := make(map[string][]interface{}) + mergedGroups := make(map[string][]any) for name, vals := range fieldsMap { - if len(vals) > 1 { - allStructs := true - hasAnyAttr := false - for _, val := range vals { - fv := marshalDeref(reflect.ValueOf(val)) - if fv.Kind() != reflect.Struct { - allStructs = false - break - } - has, err := hasAttrs(fv.Type()) - if err != nil { + if len(vals) <= 1 { + continue + } + + allScalar := true + allStructs := true + hasAnyAttr := false + + for _, val := range vals { + fv := marshalDeref(reflect.ValueOf(val)) + if !fv.IsValid() { + continue + } + + if !allScalar && !allStructs { + break + } + + k := fv.Kind() + + allStructs = allStructs && k == reflect.Struct + if allStructs && !hasAnyAttr { + var err error + if hasAnyAttr, err = hasAttrs(fv.Type()); err != nil { return nil, err } - if has { - hasAnyAttr = true + } + + allScalar = allScalar && k != reflect.Struct && k != reflect.Map && k != reflect.Slice && k != reflect.Array + } + + if allStructs && !hasAnyAttr { + mergedGroups[name] = vals + delete(fieldsMap, name) + continue + } + + if allScalar { + allEqual := true + first := vals[0] + for i := 1; i < len(vals); i++ { + if !reflect.DeepEqual(first, vals[i]) { + allEqual = false break } } - if allStructs && !hasAnyAttr { - mergedGroups[name] = vals - delete(fieldsMap, name) - continue + if allEqual { + fieldsMap[name] = []any{first} } } } @@ -138,28 +167,37 @@ func Marshal(args ...any) ([]byte, error) { e := &encoder{buf: buf} - for name, vals := range mergedGroups { - if err := e.writeMergedBlock(name, vals, ""); err != nil { - return nil, err - } - } + for _, meta := range metaList { + for _, mf := range meta.fields { - for name, vals := range fieldsMap { - for _, val := range vals { - fv := reflect.ValueOf(val) - if fv.Kind() == reflect.Ptr && fv.IsNil() { - if err := e.writeEmptyBlock(name); err != nil { + if vals, ok := mergedGroups[mf.name]; ok { + if err := e.writeMergedBlock(mf.name, vals, ""); err != nil { return nil, err } - continue + delete(mergedGroups, mf.name) } - fv = marshalDeref(fv) - if !fv.IsValid() { + + vals, ok := fieldsMap[mf.name] + if !ok { continue } - f := &boundField{fieldTag: fieldTag{name: name}} - if err := e.writeNamed(f, fv, ""); err != nil { - return nil, err + delete(fieldsMap, mf.name) + for _, val := range vals { + fv := reflect.ValueOf(val) + if fv.Kind() == reflect.Ptr && fv.IsNil() { + if err := e.writeEmptyBlock(mf.name); err != nil { + return nil, err + } + continue + } + fv = marshalDeref(fv) + if !fv.IsValid() { + continue + } + f := &boundField{fieldTag: fieldTag{name: mf.name}} + if err := e.writeNamed(f, fv, ""); err != nil { + return nil, err + } } } } diff --git a/unic_test.go b/unic_test.go index b0f43c2..93301a9 100644 --- a/unic_test.go +++ b/unic_test.go @@ -1,13 +1,13 @@ package unic import ( + "fmt" "reflect" "strings" "testing" ) -const readmeConfig = ` -log_level 1; +const readmeConfig = `log_level 1; servers { domains ['localhost', 'local.host']; server web { # веб @@ -25,6 +25,7 @@ servers { prefix /api/admin/v1; # префикс методов api для админки middleware [log, oauth]; # набор миделвар } + authBy [passwd, oauth]; } ` @@ -45,9 +46,26 @@ type readmeConfigStruct struct { Prefix string `unic:"prefix"` Middleware []string `unic:"middleware"` } `unic:"route"` + AuthBy []string `unic:"authBy"` } `unic:"servers,desc='настройки серверов'"` } +func TestUnit_Base(t *testing.T) { + var cfg readmeConfigStruct + if err := Unmarshal([]byte(readmeConfig), &cfg); err != nil { + t.Fatal(err) + } + b, err := Marshal(cfg) + if err != nil { + t.Fatal(err) + } + excepted := "log_level 1;\nservers {\n\tdomains [localhost, local.host]; # список доменов\n\tserver web {\n\t\tport 80; # номер порта\n\t\thost 127.0.0.1; # IP или домен\n\t\tttl [1, 2, 3];\n\t\tssl [/etc/ssl/host1.pem, /etc/ssl/host1.pem]; # пути для сертификатов\n\t}\n\tserver admin {\n\t\tport 80; # номер порта\n\t\thost 127.0.0.2; # IP или домен\n\t\tauth (user1, passwd1, user2, passwd2);\n\t}\n\troute admin {\n\t\tprefix /api/admin/v1;\n\t\tmiddleware [log, oauth];\n\t}\n\tauthBy [passwd, oauth];\n}\n" + if string(b) != excepted { + fmt.Printf("got %q,\n\n want %q", string(b), excepted) + t.Errorf("got %s, want %s", string(b), excepted) + } +} + func TestUnit_UnmarshalREADME(t *testing.T) { var cfg readmeConfigStruct if err := Unmarshal([]byte(readmeConfig), &cfg); err != nil { @@ -702,13 +720,33 @@ func TestUnit_UnmarshalUnknownFieldsIgnored(t *testing.T) { } } +func TestUnit_README_EXAMPLE(t *testing.T) { + type Part1 struct { + Common string `unic:"common"` + A int `unic:"a"` + } + type Part2 struct { + Common string `unic:"common"` + B bool `unic:"b"` + } + + data, _ := Marshal(Part1{Common: "shared", A: 42}, Part2{Common: "shared", B: true}) + excepted := `common shared; +a 42; +b true; +` + if string(data) != excepted { + t.Fatalf("want=%q\ngot=%q", excepted, data) + } +} + /* goos: linux goarch: amd64 pkg: go.osspkg.com/unic cpu: 12th Gen Intel(R) Core(TM) i9-12900KF Benchmark_Unmarshal -Benchmark_Unmarshal-24 283897 5598 ns/op 10318 B/op 185 allocs/op +Benchmark_Unmarshal-4 299887 3943 ns/op 10917 B/op 199 allocs/op PASS */ func Benchmark_Unmarshal(b *testing.B) { @@ -731,7 +769,7 @@ goarch: amd64 pkg: go.osspkg.com/unic cpu: 12th Gen Intel(R) Core(TM) i9-12900KF Benchmark_Marshal -Benchmark_Marshal-24 1685868 707.7 ns/op 1363 B/op 35 allocs/op +Benchmark_Marshal-4 1000000 1272 ns/op 1488 B/op 37 allocs/op PASS */ func Benchmark_Marshal(b *testing.B) {