From b35069a821d69ddd4776975d843bdfdfdbf2ccff Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Wed, 1 Jan 2025 13:46:08 -0500 Subject: [PATCH] Add more tests and benchmarks --- .github/workflows/linter.yml | 2 +- Makefile | 93 ++++++---- decoder_test.go | 330 ++++++++++++++++++++++++++++------- doc.go | 16 +- encoder_test.go | 272 +++++++++++++++++++++++++++++ 5 files changed, 613 insertions(+), 100 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 4d61bcd..9f5dd28 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -37,4 +37,4 @@ jobs: uses: golangci/golangci-lint-action@v6 with: # NOTE: Keep this in sync with the version from .golangci.yml - version: v1.60.3 \ No newline at end of file + version: v1.62.2 \ No newline at end of file diff --git a/Makefile b/Makefile index 98f5ab7..669b3fb 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,65 @@ -GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') -GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest - -GO_SEC=$(shell which gosec 2> /dev/null || echo '') -GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest - -GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') -GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest - -.PHONY: golangci-lint -golangci-lint: - $(if $(GO_LINT), ,go install $(GO_LINT_URI)) - @echo "##### Running golangci-lint" - golangci-lint run -v - -.PHONY: gosec -gosec: - $(if $(GO_SEC), ,go install $(GO_SEC_URI)) - @echo "##### Running gosec" - gosec ./... - -.PHONY: govulncheck -govulncheck: - $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) - @echo "##### Running govulncheck" - govulncheck ./... - -.PHONY: verify -verify: golangci-lint gosec govulncheck +## help: 💡 Display available commands +.PHONY: help +help: + @echo '⚡️ GoFiber/Fiber Development:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' +## audit: 🚀 Conduct quality checks +.PHONY: audit +audit: + go mod verify + go vet ./... + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + +## benchmark: 📈 Benchmark code performance +.PHONY: benchmark +benchmark: + go test ./... -benchmem -bench=. -run=^Benchmark_$ + +## coverage: ☂️ Generate coverage report +.PHONY: coverage +coverage: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=/tmp/coverage.out -covermode=atomic + go tool cover -html=/tmp/coverage.out + +## format: 🎨 Fix code format issues +.PHONY: format +format: + go run mvdan.cc/gofumpt@latest -w -l . + +## markdown: 🎨 Find markdown format issues (Requires markdownlint-cli2) +.PHONY: markdown +markdown: + markdownlint-cli2 "**/*.md" "#vendor" + +## lint: 🚨 Run lint checks +.PHONY: lint +lint: + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 run ./... + +## test: 🚦 Execute all tests .PHONY: test test: - @echo "##### Running tests" - go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... \ No newline at end of file + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on + +## longtest: 🚦 Execute all tests 10x +.PHONY: longtest +longtest: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=15 -shuffle=on + +## tidy: 📌 Clean and tidy dependencies +.PHONY: tidy +tidy: + go mod tidy -v + +## betteralign: 📐 Optimize alignment of fields in structs +.PHONY: betteralign +betteralign: + go run github.com/dkorunic/betteralign/cmd/betteralign@latest -test_files -generated_files -apply ./... + +## generate: ⚡️ Generate msgp && interface implementations +.PHONY: generate +generate: + go install github.com/tinylib/msgp@latest + go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c + go generate ./... diff --git a/decoder_test.go b/decoder_test.go index 7106dcf..d3d5c5e 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -56,6 +56,32 @@ type S1 struct { F21 []*rudeBool `schema:"f21"` } +type LargeStructForBenchmark struct { + F1 string `schema:"f1"` + F2 string `schema:"f2"` + F3 int `schema:"f3"` + F4 int `schema:"f4"` + F5 []string `schema:"f5"` + F6 []int `schema:"f6"` + F7 float64 `schema:"f7"` + F8 bool `schema:"f8"` + F9 struct { + N1 time.Time `schema:"n1"` + N2 string `schema:"n2"` + } `schema:"f9"` +} + +// A simple struct for demonstration benchmarks +type SimpleStructForBenchmark struct { + A string `schema:"a"` + B int `schema:"b"` + C bool `schema:"c"` + D float64 `schema:"d"` + E struct { + F float64 `schema:"f"` + } `schema:"e"` +} + type S2 struct { F01 *[]*int `schema:"f1"` } @@ -133,34 +159,34 @@ func TestAll(t *testing.T) { }, F09: 0, F10: []S1{ - S1{ + { F10: []S1{ - S1{F06: &[]*int{&f101, &f102}}, - S1{F06: &[]*int{&f103, &f104}}, + {F06: &[]*int{&f101, &f102}}, + {F06: &[]*int{&f103, &f104}}, }, }, }, F11: []*S1{ - &S1{ + { F11: []*S1{ - &S1{F06: &[]*int{&f111, &f112}}, - &S1{F06: &[]*int{&f113, &f114}}, + {F06: &[]*int{&f111, &f112}}, + {F06: &[]*int{&f113, &f114}}, }, }, }, F12: &[]S1{ - S1{ + { F12: &[]S1{ - S1{F06: &[]*int{&f121, &f122}}, - S1{F06: &[]*int{&f123, &f124}}, + {F06: &[]*int{&f121, &f122}}, + {F06: &[]*int{&f123, &f124}}, }, }, }, F13: &[]*S1{ - &S1{ + { F13: &[]*S1{ - &S1{F06: &[]*int{&f131, &f132}}, - &S1{F06: &[]*int{&f133, &f134}}, + {F06: &[]*int{&f131, &f132}}, + {F06: &[]*int{&f133, &f134}}, }, }, }, @@ -598,8 +624,10 @@ func TestSimpleExample(t *testing.T) { S05: "S5", Str: "Str", }, - Bif: []Baz{{ - F99: []string{"A", "B", "C"}}, + Bif: []Baz{ + { + F99: []string{"A", "B", "C"}, + }, }, } @@ -939,34 +967,34 @@ func TestAllNT(t *testing.T) { }, F9: 0, F10: []S1{ - S1{ + { F10: []S1{ - S1{F06: &[]*int{&f101, &f102}}, - S1{F06: &[]*int{&f103, &f104}}, + {F06: &[]*int{&f101, &f102}}, + {F06: &[]*int{&f103, &f104}}, }, }, }, F11: []*S1{ - &S1{ + { F11: []*S1{ - &S1{F06: &[]*int{&f111, &f112}}, - &S1{F06: &[]*int{&f113, &f114}}, + {F06: &[]*int{&f111, &f112}}, + {F06: &[]*int{&f113, &f114}}, }, }, }, F12: &[]S1{ - S1{ + { F12: &[]S1{ - S1{F06: &[]*int{&f121, &f122}}, - S1{F06: &[]*int{&f123, &f124}}, + {F06: &[]*int{&f121, &f122}}, + {F06: &[]*int{&f123, &f124}}, }, }, }, F13: &[]*S1{ - &S1{ + { F13: &[]*S1{ - &S1{F06: &[]*int{&f131, &f132}}, - &S1{F06: &[]*int{&f133, &f134}}, + {F06: &[]*int{&f131, &f132}}, + {F06: &[]*int{&f133, &f134}}, }, }, }, @@ -1287,7 +1315,7 @@ func TestRegisterConverterSlice(t *testing.T) { expected := []string{"one", "two", "three"} err := decoder.Decode(&result, map[string][]string{ - "multiple": []string{"one,two,three"}, + "multiple": {"one,two,three"}, }) if err != nil { t.Fatalf("Failed to decode: %v", err) @@ -1319,7 +1347,7 @@ func TestRegisterConverterMap(t *testing.T) { }{} err := decoder.Decode(&result, map[string][]string{ - "multiple": []string{"a:one,b:two"}, + "multiple": {"a:one,b:two"}, }) if err != nil { t.Fatal(err) @@ -1366,9 +1394,9 @@ type S16 struct { func TestCustomTypeSlice(t *testing.T) { data := map[string][]string{ - "Value.0": []string{"Louisa May Alcott"}, - "Value.1": []string{"Florence Nightingale"}, - "Value.2": []string{"Clara Barton"}, + "Value.0": {"Louisa May Alcott"}, + "Value.1": {"Florence Nightingale"}, + "Value.2": {"Clara Barton"}, } s := S13{} @@ -1394,9 +1422,9 @@ func TestCustomTypeSlice(t *testing.T) { func TestCustomTypeSliceWithError(t *testing.T) { data := map[string][]string{ - "Value.0": []string{"Louisa May Alcott"}, - "Value.1": []string{"Florence Nightingale"}, - "Value.2": []string{"Clara"}, + "Value.0": {"Louisa May Alcott"}, + "Value.1": {"Florence Nightingale"}, + "Value.2": {"Clara"}, } s := S13{} @@ -1409,9 +1437,9 @@ func TestCustomTypeSliceWithError(t *testing.T) { func TestNoTextUnmarshalerTypeSlice(t *testing.T) { data := map[string][]string{ - "Value.0": []string{"Louisa May Alcott"}, - "Value.1": []string{"Florence Nightingale"}, - "Value.2": []string{"Clara Barton"}, + "Value.0": {"Louisa May Alcott"}, + "Value.1": {"Florence Nightingale"}, + "Value.2": {"Clara Barton"}, } s := S15{} @@ -1434,7 +1462,7 @@ type S18 struct { func TestCustomType(t *testing.T) { data := map[string][]string{ - "Value": []string{"Louisa May Alcott"}, + "Value": {"Louisa May Alcott"}, } s := S17{} @@ -1451,7 +1479,7 @@ func TestCustomType(t *testing.T) { func TestCustomTypeWithError(t *testing.T) { data := map[string][]string{ - "Value": []string{"Louisa"}, + "Value": {"Louisa"}, } s := S17{} @@ -1464,7 +1492,7 @@ func TestCustomTypeWithError(t *testing.T) { func TestNoTextUnmarshalerType(t *testing.T) { data := map[string][]string{ - "Value": []string{"Louisa May Alcott"}, + "Value": {"Louisa May Alcott"}, } s := S18{} @@ -1477,9 +1505,9 @@ func TestNoTextUnmarshalerType(t *testing.T) { func TestExpectedType(t *testing.T) { data := map[string][]string{ - "bools": []string{"1", "a"}, - "date": []string{"invalid"}, - "Foo.Bar": []string{"a", "b"}, + "bools": {"1", "a"}, + "date": {"invalid"}, + "Foo.Bar": {"a", "b"}, } type B struct { @@ -1524,11 +1552,11 @@ type R1 struct { func TestRequiredField(t *testing.T) { var a R1 v := map[string][]string{ - "a": []string{"bbb"}, - "b.c": []string{"88"}, - "b.d": []string{"9"}, - "f": []string{""}, - "h": []string{"true"}, + "a": {"bbb"}, + "b.c": {"88"}, + "b.d": {"9"}, + "f": {""}, + "h": {"true"}, } err := NewDecoder().Decode(&a, v) if err == nil { @@ -1595,7 +1623,7 @@ type R2 struct { func TestRequiredStructFiled(t *testing.T) { v := map[string][]string{ - "a.b": []string{"3"}, + "a.b": {"3"}, } var a R2 err := NewDecoder().Decode(&a, v) @@ -1944,7 +1972,7 @@ func (s *S20) UnmarshalText(text []byte) error { // implementations by its elements. func TestTextUnmarshalerTypeSlice(t *testing.T) { data := map[string][]string{ - "Value": []string{"a,b,c"}, + "Value": {"a,b,c"}, } s := struct { Value S20 @@ -1980,7 +2008,7 @@ type S21B []S21E // requirements imposed on a slice of structs. func TestTextUnmarshalerTypeSliceOfStructs(t *testing.T) { data := map[string][]string{ - "Value": []string{"raw a"}, + "Value": {"raw a"}, } // Implements encoding.TextUnmarshaler, should not throw invalid path // error. @@ -2018,7 +2046,7 @@ func (s *S22) UnmarshalText(text []byte) error { // especially including simply setting the zero value. func TestTextUnmarshalerEmpty(t *testing.T) { data := map[string][]string{ - "Value": []string{""}, // empty value + "Value": {""}, // empty value } // Implements encoding.TextUnmarshaler, should use the type's // UnmarshalText method. @@ -2049,8 +2077,8 @@ type S23 []*S23e func TestUnmashalPointerToEmbedded(t *testing.T) { data := map[string][]string{ - "A.0.F2": []string{"raw a"}, - "A.0.F3": []string{"raw b"}, + "A.0.F2": {"raw a"}, + "A.0.F3": {"raw b"}, } // Implements encoding.TextUnmarshaler, should not throw invalid path @@ -2139,7 +2167,6 @@ func TestDoubleEmbedded(t *testing.T) { if !reflect.DeepEqual(expected, s) { t.Errorf("Expected %v errors, got %v", expected, s) } - } func TestDefaultValuesAreSet(t *testing.T) { @@ -2297,7 +2324,6 @@ func TestRequiredFieldsCannotHaveDefaults(t *testing.T) { if err == nil || !strings.Contains(err.Error(), expected) { t.Errorf("decoding should fail with error msg %s got %q", expected, err) } - } func TestInvalidDefaultElementInSliceRaiseError(t *testing.T) { @@ -2354,7 +2380,7 @@ func TestInvalidDefaultsValuesHaveNoEffect(t *testing.T) { type D struct { B bool `schema:"b,default:invalid"` C *float32 `schema:"c,default:notAFloat"` - //uint types + // uint types D uint `schema:"d,default:notUint"` E uint8 `schema:"e,default:notUint"` F uint16 `schema:"f,default:notUint"` @@ -2393,7 +2419,6 @@ func TestInvalidDefaultsValuesHaveNoEffect(t *testing.T) { decoder := NewDecoder() err := decoder.Decode(&d, data) - if err != nil { t.Errorf("decoding should succeed but got error: %q", err) } @@ -2524,7 +2549,6 @@ func TestDecoder_MaxSize(t *testing.T) { } func TestDecoder_SetMaxSize(t *testing.T) { - t.Run("default maxsize should be equal to given constant", func(t *testing.T) { t.Parallel() dec := NewDecoder() @@ -2545,6 +2569,169 @@ func TestDecoder_SetMaxSize(t *testing.T) { }) } +func TestTimeDurationDecoding(t *testing.T) { + type DurationStruct struct { + Timeout time.Duration `schema:"timeout"` + } + + // Prepare the input data + input := map[string][]string{ + "timeout": {"2s"}, + } + + // Create a decoder with a converter for time.Duration + decoder := NewDecoder() + decoder.RegisterConverter(time.Duration(0), func(s string) reflect.Value { + d, err := time.ParseDuration(s) + if err != nil { + return reflect.Value{} + } + return reflect.ValueOf(d) + }) + + var result DurationStruct + err := decoder.Decode(&result, input) + if err != nil { + t.Fatalf("Failed to decode duration: %v", err) + } + + // Expect 2 seconds + if result.Timeout != 2*time.Second { + t.Errorf("Expected 2s, got %v", result.Timeout) + } +} + +func TestTimeDurationDecodingInvalid(t *testing.T) { + type DurationStruct struct { + Timeout time.Duration `schema:"timeout"` + } + + // Prepare the input data + input := map[string][]string{ + "timeout": {"invalid-duration"}, + } + + // Create a decoder with a converter for time.Duration + decoder := NewDecoder() + decoder.RegisterConverter(time.Duration(0), func(s string) reflect.Value { + // Attempt to parse the duration + d, err := time.ParseDuration(s) + if err != nil { + // Return an invalid reflect.Value to trigger a conversion error + return reflect.Value{} + } + return reflect.ValueOf(d) + }) + + var result DurationStruct + err := decoder.Decode(&result, input) + if err == nil { + t.Error("Expected an error decoding invalid duration, got nil") + } +} + +func TestMultipleConversionErrors(t *testing.T) { + type Fields struct { + IntField int `schema:"int_field"` + BoolField bool `schema:"bool_field"` + Duration time.Duration `schema:"duration_field"` + } + + input := map[string][]string{ + "int_field": {"invalid-int"}, + "bool_field": {"invalid-bool"}, + "duration_field": {"invalid-duration"}, + } + + decoder := NewDecoder() + decoder.RegisterConverter(time.Duration(0), func(s string) reflect.Value { + d, err := time.ParseDuration(s) + if err != nil { + return reflect.Value{} + } + return reflect.ValueOf(d) + }) + + var s Fields + err := decoder.Decode(&s, input) + if err == nil { + t.Fatal("Expected multiple conversion errors, got nil") + } + + // Check that all errors are reported (at least 3). + mErr, ok := err.(MultiError) + if !ok { + t.Fatalf("Expected MultiError, got %T", err) + } + if len(mErr) < 3 { + t.Errorf("Expected at least 3 errors, got %d: %v", len(mErr), mErr) + } +} + +func BenchmarkLargeStructDecode(b *testing.B) { + data := map[string][]string{ + "f1": {"Lorem"}, + "f2": {"Ipsum"}, + "f3": {"123"}, + "f4": {"456"}, + "f5": {"A", "B", "C", "D"}, + "f6": {"10", "20", "30", "40"}, + "f7": {"3.14159"}, + "f8": {"true"}, + "f9.n1": {"2025-01-01T12:00:00Z"}, + "f9.n2": {"NestedStringValue"}, + } + + decoder := NewDecoder() + decoder.RegisterConverter(time.Time{}, func(s string) reflect.Value { + tm, err := time.Parse(time.RFC3339, s) + if err != nil { + return reflect.Value{} + } + return reflect.ValueOf(tm) + }) + + s := &LargeStructForBenchmark{} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = decoder.Decode(s, data) + } +} + +func BenchmarkLargeStructDecodeParallel(b *testing.B) { + data := map[string][]string{ + "f1": {"Lorem"}, + "f2": {"Ipsum"}, + "f3": {"123"}, + "f4": {"456"}, + "f5": {"A", "B", "C", "D"}, + "f6": {"10", "20", "30", "40"}, + "f7": {"3.14159"}, + "f8": {"true"}, + "f9.n1": {"2025-01-01T12:00:00Z"}, + "f9.n2": {"NestedStringValue"}, + } + + decoder := NewDecoder() + decoder.RegisterConverter(time.Time{}, func(s string) reflect.Value { + tm, err := time.Parse(time.RFC3339, s) + if err != nil { + return reflect.Value{} + } + return reflect.ValueOf(tm) + }) + + s := &LargeStructForBenchmark{} + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = decoder.Decode(s, data) + } + }) +} + func BenchmarkSimpleStructDecode(b *testing.B) { type S struct { A string `schema:"a"` @@ -2591,7 +2778,7 @@ func BenchmarkCheckRequiredFields(b *testing.B) { b.ResetTimer() v := reflect.ValueOf(s) - //v = v.Elem() + // v = v.Elem() t := v.Type() var errs MultiError for i := 0; i < b.N; i++ { @@ -2602,3 +2789,26 @@ func BenchmarkCheckRequiredFields(b *testing.B) { b.Fatalf("unexpected errors: %v", errs) } } + +func BenchmarkTimeDurationDecoding(b *testing.B) { + type DurationStruct struct { + Timeout time.Duration `schema:"timeout"` + } + + // Sample input for decoding + input := map[string][]string{ + "timeout": {"2s"}, + } + + decoder := NewDecoder() + decoder.RegisterConverter(time.Duration(0), func(s string) reflect.Value { + d, _ := time.ParseDuration(s) + return reflect.ValueOf(d) + }) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var ds DurationStruct + _ = decoder.Decode(&ds, input) + } +} diff --git a/doc.go b/doc.go index ffa48f6..50faaac 100644 --- a/doc.go +++ b/doc.go @@ -60,14 +60,14 @@ certain fields, use a dash for the name and it will be ignored: The supported field types in the destination struct are: - * bool - * float variants (float32, float64) - * int variants (int, int8, int16, int32, int64) - * string - * uint variants (uint, uint8, uint16, uint32, uint64) - * struct - * a pointer to one of the above types - * a slice or a pointer to a slice of one of the above types + - bool + - float variants (float32, float64) + - int variants (int, int8, int16, int32, int64) + - string + - uint variants (uint, uint8, uint16, uint32, uint64) + - struct + - a pointer to one of the above types + - a slice or a pointer to a slice of one of the above types Non-supported types are simply ignored, however custom types can be registered to be converted. diff --git a/encoder_test.go b/encoder_test.go index 092f0de..df3872b 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -24,6 +24,16 @@ type inner struct { F12 int } +type SimpleStructForBenchmarkEncode struct { + A string `schema:"a"` + B int `schema:"b"` + C bool `schema:"c"` + D float64 `schema:"d"` + E struct { + F float64 `schema:"f"` + } `schema:"e"` +} + func TestFilled(t *testing.T) { f07 := "seven" var f08 int8 = 8 @@ -523,3 +533,265 @@ func TestRegisterEncoderWithPtrType(t *testing.T) { valExists(t, "DateStart", ss.DateStart.time.String(), vals) valExists(t, "DateEnd", "", vals) } + +func TestTimeDurationEncoding(t *testing.T) { + type DurationStruct struct { + Timeout time.Duration `schema:"timeout"` + } + + vals := map[string][]string{} + testData := DurationStruct{ + Timeout: 3 * time.Minute, + } + + enc := NewEncoder() + enc.RegisterEncoder(time.Duration(0), func(v reflect.Value) string { + d := v.Interface().(time.Duration) + return d.String() // "3m0s" + }) + + err := enc.Encode(&testData, vals) + if err != nil { + t.Fatalf("Failed to encode time.Duration: %v", err) + } + + got, ok := vals["timeout"] + if !ok || len(got) < 1 { + t.Fatalf("Encoded map missing key 'timeout'") + } + if got[0] != (3 * time.Minute).String() { + t.Errorf("Expected %q, got %q", (3 * time.Minute).String(), got[0]) + } +} + +// Test for omitempty with zero time.Duration. +func TestTimeDurationOmitEmpty(t *testing.T) { + type DurationStruct struct { + Timeout time.Duration `schema:"timeout,omitempty"` + } + + vals := map[string][]string{} + testData := DurationStruct{ + Timeout: 0, + } + + enc := NewEncoder() + enc.RegisterEncoder(time.Duration(0), func(v reflect.Value) string { + return v.Interface().(time.Duration).String() + }) + + err := enc.Encode(&testData, vals) + if err != nil { + t.Fatalf("Failed to encode time.Duration: %v", err) + } + // Should be omitted since 0 for time.Duration is "zero" and tagged as omitempty + if _, found := vals["timeout"]; found { + t.Errorf("Expected 'timeout' to be omitted, but it was present: %v", vals["timeout"]) + } +} + +func TestEncoderZeroAndNonZeroFields(t *testing.T) { + type ZeroTestStruct struct { + A string `schema:"a,omitempty"` + B int `schema:"b,omitempty"` + C float64 `schema:"c,omitempty"` + D bool `schema:"d,omitempty"` + E *int `schema:"e,omitempty"` + F *string `schema:"f,omitempty"` + G string `schema:"g"` // no omitempty + } + + vals := map[string][]string{} + intVal := 42 + strVal := "Hello" + s := ZeroTestStruct{ + A: "", + B: 0, + C: 0.0, + D: false, + E: &intVal, + F: &strVal, + G: "MustEncode", + } + + enc := NewEncoder() + err := enc.Encode(&s, vals) + if err != nil { + t.Fatalf("Encoding error: %v", err) + } + + // Fields A, B, C, D are zero and should be omitted + if _, found := vals["a"]; found { + t.Errorf("Expected 'a' to be omitted for zero string") + } + if _, found := vals["b"]; found { + t.Errorf("Expected 'b' to be omitted for zero int") + } + if _, found := vals["c"]; found { + t.Errorf("Expected 'c' to be omitted for zero float") + } + if _, found := vals["d"]; found { + t.Errorf("Expected 'd' to be omitted for false bool") + } + + // E is a pointer to an int, so it should appear + gotE, found := vals["e"] + if !found { + t.Error("Expected 'e' to be present") + } else if len(gotE) != 1 || gotE[0] != "42" { + t.Errorf("Expected '42', got %v", gotE) + } + + // F is a pointer to string, so it should appear + gotF, found := vals["f"] + if !found { + t.Error("Expected 'f' to be present") + } else if len(gotF) != 1 || gotF[0] != "Hello" { + t.Errorf("Expected 'Hello', got %v", gotF) + } + + // G has no omitempty tag and must be encoded + gotG, found := vals["g"] + if !found { + t.Error("Expected 'g' to be present") + } else if len(gotG) != 1 || gotG[0] != "MustEncode" { + t.Errorf("Expected 'MustEncode', got %v", gotG) + } +} + +func BenchmarkSimpleStructEncode(b *testing.B) { + s := SimpleStructForBenchmarkEncode{ + A: "abc", + B: 123, + C: true, + D: 3.14, + E: struct { + F float64 `schema:"f"` + }{F: 6.28}, + } + enc := NewEncoder() + + vals := map[string][]string{} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = enc.Encode(&s, vals) + } +} + +func BenchmarkSimpleStructEncodeParallel(b *testing.B) { + s := SimpleStructForBenchmarkEncode{ + A: "abc", + B: 123, + C: true, + D: 3.14, + E: struct { + F float64 `schema:"f"` + }{F: 6.28}, + } + enc := NewEncoder() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + vals := map[string][]string{} + for pb.Next() { + _ = enc.Encode(&s, vals) + } + }) +} + +type LargeStructForBenchmarkEncode struct { + F1 string `schema:"f1"` + F2 string `schema:"f2"` + F3 int `schema:"f3"` + F4 int `schema:"f4"` + F5 []string `schema:"f5"` + F6 []int `schema:"f6"` + F7 float64 `schema:"f7"` + F8 bool `schema:"f8"` + F9 struct { + N1 time.Time `schema:"n1"` + N2 string `schema:"n2"` + } `schema:"f9"` +} + +func BenchmarkLargeStructEncode(b *testing.B) { + s := LargeStructForBenchmarkEncode{ + F1: "Lorem", F2: "Ipsum", F3: 123, F4: 456, + F5: []string{"A", "B", "C", "D"}, + F6: []int{10, 20, 30, 40}, + F7: 3.14159, F8: true, + F9: struct { + N1 time.Time `schema:"n1"` + N2 string `schema:"n2"` + }{ + N1: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC), + N2: "NestedStringValue", + }, + } + enc := NewEncoder() + + // Optionally register a custom encoder for time.Time + enc.RegisterEncoder(time.Time{}, func(v reflect.Value) string { + tVal := v.Interface().(time.Time) + return tVal.Format(time.RFC3339) + }) + + vals := map[string][]string{} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = enc.Encode(&s, vals) + } +} + +func BenchmarkLargeStructEncodeParallel(b *testing.B) { + s := LargeStructForBenchmarkEncode{ + F1: "Lorem", F2: "Ipsum", F3: 123, F4: 456, + F5: []string{"A", "B", "C", "D"}, + F6: []int{10, 20, 30, 40}, + F7: 3.14159, F8: true, + F9: struct { + N1 time.Time `schema:"n1"` + N2 string `schema:"n2"` + }{ + N1: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC), + N2: "NestedStringValue", + }, + } + enc := NewEncoder() + enc.RegisterEncoder(time.Time{}, func(v reflect.Value) string { + tVal := v.Interface().(time.Time) + return tVal.Format(time.RFC3339) + }) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + vals := map[string][]string{} + for pb.Next() { + _ = enc.Encode(&s, vals) + } + }) +} + +func BenchmarkTimeDurationEncoding(b *testing.B) { + type DurationStruct struct { + Timeout time.Duration `schema:"timeout"` + } + + testData := DurationStruct{ + Timeout: 5 * time.Second, + } + + enc := NewEncoder() + enc.RegisterEncoder(time.Duration(0), func(v reflect.Value) string { + return v.Interface().(time.Duration).String() + }) + + vals := map[string][]string{} + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = enc.Encode(&testData, vals) + } +}