Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
22 changes: 11 additions & 11 deletions merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
110 changes: 74 additions & 36 deletions unic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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}
}
}
}
Expand All @@ -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
}
}
}
}
Expand Down
46 changes: 42 additions & 4 deletions unic_test.go
Original file line number Diff line number Diff line change
@@ -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 { # веб
Expand All @@ -25,6 +25,7 @@ servers {
prefix /api/admin/v1; # префикс методов api для админки
middleware [log, oauth]; # набор миделвар
}
authBy [passwd, oauth];
}
`

Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Loading