feat: replace mapping generation with checked copy utilities
This commit is contained in:
@@ -18,7 +18,6 @@ import (
|
||||
"example.com/composition/internal/convert"
|
||||
"example.com/composition/internal/domain"
|
||||
"example.com/composition/internal/handler"
|
||||
"example.com/composition/internal/mapping"
|
||||
"example.com/composition/internal/middleware"
|
||||
"example.com/composition/internal/svc"
|
||||
"example.com/composition/internal/types"
|
||||
@@ -86,16 +85,16 @@ func TestHTTPComposition(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestFullFormAndMappingSemantics(t *testing.T) {
|
||||
func TestFullFormAndCopySemantics(t *testing.T) {
|
||||
name := "employee"
|
||||
money := amount.Money("2.00")
|
||||
yes := true
|
||||
req := types.EnterpriseRequest{Name: &name, Salary: &money, Enabled: &yes, Labels: []string{"one"}}
|
||||
first, e := mapping.FromEnterprise(req)
|
||||
first, e := convert.Employee(req)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
second, e := mapping.FromEnterprise(types.EnterpriseRequest{})
|
||||
second, e := convert.Employee(types.EnterpriseRequest{})
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
@@ -108,26 +107,26 @@ func TestFullFormAndMappingSemantics(t *testing.T) {
|
||||
t.Fatal("unexpected implicit deep copy")
|
||||
}
|
||||
req.Employer = &money
|
||||
first, e = mapping.FromEnterprise(req)
|
||||
first, e = convert.Employee(req)
|
||||
if e != nil || first.Employer != req.Employer {
|
||||
t.Fatal("nullable pointer changed", e)
|
||||
}
|
||||
bad := "reject"
|
||||
failed, e := mapping.FromEnterprise(types.EnterpriseRequest{Name: &bad})
|
||||
failed, e := convert.Employee(types.EnterpriseRequest{Name: &bad})
|
||||
if !errors.Is(e, convert.ErrName) || failed.EmployeeName != "" || !strings.Contains(e.Error(), "EmployeeName") {
|
||||
t.Fatal("converter cause/zero result lost", e)
|
||||
}
|
||||
empty, e := mapping.ToBatch(domain.Batch{})
|
||||
empty, e := convert.ToBatch(domain.Batch{})
|
||||
if e != nil || empty.Rows != nil {
|
||||
t.Fatal("nil slice changed", e)
|
||||
}
|
||||
src := domain.Batch{Rows: []domain.Row{}, Fixed: [2]domain.Row{{Value: "a"}, {Value: "b"}}, Optional: &name}
|
||||
out, e := mapping.ToBatch(src)
|
||||
out, e := convert.ToBatch(src)
|
||||
if e != nil || out.Rows == nil || len(out.Rows) != 0 || out.Fixed[1].Value != "b" || out.Optional != src.Optional {
|
||||
t.Fatal(out, e)
|
||||
}
|
||||
src.Rows = []domain.Row{{Value: "one"}}
|
||||
out, e = mapping.ToBatch(src)
|
||||
out, e = convert.ToBatch(src)
|
||||
if e != nil || out.Rows[0].Value != "one" {
|
||||
t.Fatal(out, e)
|
||||
}
|
||||
@@ -140,7 +139,7 @@ func TestFullFormAndMappingSemantics(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := mapping.FromEnterprise(types.EnterpriseRequest{Name: &name, Salary: &money})
|
||||
result, err := convert.Employee(types.EnterpriseRequest{Name: &name, Salary: &money})
|
||||
if err != nil || result.EmployeeName != name || result.Salary != money {
|
||||
t.Error(result, err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package convert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"example.com/composition/internal/amount"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
@@ -19,11 +18,4 @@ func Name(value *string) (string, error) {
|
||||
}
|
||||
return *value, nil
|
||||
}
|
||||
func Money(value *amount.Money) amount.Money {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
func Bool(value *bool) bool { return value != nil && *value }
|
||||
func ID(value int64) string { return strconv.FormatInt(value, 10) }
|
||||
|
||||
29
cmd/go-omni/apigen/testdata/composition/copy.go
vendored
Normal file
29
cmd/go-omni/apigen/testdata/composition/copy.go
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"example.com/composition/internal/domain"
|
||||
"example.com/composition/internal/types"
|
||||
"git.yuntuo.hk/k2safe/go-omni/copyx"
|
||||
)
|
||||
|
||||
// The three endpoints reuse one ordinary Go helper and their existing scalar.
|
||||
// There is no field list or package/type name JSON to keep in sync.
|
||||
func Employee[S any](source S) (domain.Employee, error) {
|
||||
var target domain.Employee
|
||||
err := copyx.Copy(&target, source,
|
||||
copyx.WithFieldNames[S, domain.Employee](map[string]string{"Name": "EmployeeName"}),
|
||||
copyx.WithNilPolicy(copyx.NilZero),
|
||||
copyx.WithConverter(Name),
|
||||
)
|
||||
return target, err
|
||||
}
|
||||
func Response(source domain.Result) (types.EmployeeResponse, error) {
|
||||
var target types.EmployeeResponse
|
||||
err := copyx.Copy(&target, source, copyx.WithConverter(func(v int64) (string, error) { return ID(v), nil }))
|
||||
return target, err
|
||||
}
|
||||
func ToBatch(source domain.Batch) (domain.BatchDTO, error) {
|
||||
var target domain.BatchDTO
|
||||
err := copyx.Copy(&target, source)
|
||||
return target, err
|
||||
}
|
||||
@@ -14,7 +14,7 @@ type Result struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// Distinct container item types exercise explicitly selected element mappings.
|
||||
// Distinct container item types exercise explicit Go collection copying.
|
||||
type Row struct{ Value string }
|
||||
type RowDTO struct{ Value string }
|
||||
type Batch struct {
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"example.com/nullable/internal/convert"
|
||||
"example.com/nullable/internal/domain"
|
||||
"example.com/nullable/internal/handler"
|
||||
"example.com/nullable/internal/mapping"
|
||||
"example.com/nullable/internal/svc"
|
||||
"example.com/nullable/internal/types"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
)
|
||||
|
||||
func TestNullableMappings(t *testing.T) {
|
||||
for _, src := range []types.Parent{{}, {Children: []*types.Child{}}, {Child: &types.Child{}, Children: []*types.Child{nil, {Name: "two"}}}} {
|
||||
out, err := mapping.ToParent(src)
|
||||
if err != nil || (src.Child == nil) != (out.Child == nil) || (src.Children == nil) != (out.Children == nil) || len(src.Children) != len(out.Children) {
|
||||
t.Fatal(src, out, err)
|
||||
}
|
||||
if src.Child != nil && out.Child.Name != src.Child.Name {
|
||||
t.Fatal("zero child changed")
|
||||
}
|
||||
if len(out.Children) > 0 && (out.Children[0] != nil || out.Children[1].Name != "two") {
|
||||
t.Fatal(out)
|
||||
}
|
||||
}
|
||||
src := types.Parent{Child: &types.Child{Name: "one"}, Children: []*types.Child{{Name: "two"}}}
|
||||
out, err := mapping.ToParentPtr(src)
|
||||
if err != nil || out == nil || out.Child.Name != "one" {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
out.Child.Name = "changed"
|
||||
out.Children[0].Name = "changed"
|
||||
if src.Child.Name != "one" || src.Children[0].Name != "two" {
|
||||
t.Fatal("nested mapped values alias input")
|
||||
}
|
||||
plain := domain.Parent{Child: &domain.Child{Name: "shared"}}
|
||||
copy, err := mapping.ShallowCopy(plain)
|
||||
if err != nil || copy.Child != plain.Child {
|
||||
t.Fatal("ordinary assignment semantics changed", err)
|
||||
}
|
||||
zero, err := mapping.RequiredZero(types.Parent{})
|
||||
if err != nil || zero.Child.Name != "" {
|
||||
t.Fatal(zero, err)
|
||||
}
|
||||
for _, fn := range []func(types.Parent) (domain.Required, error){mapping.RequiredZero, mapping.RequiredError} {
|
||||
out, err := fn(types.Parent{Child: &types.Child{Name: "required"}})
|
||||
if err != nil || out.Child.Name != "required" {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
}
|
||||
_, err = mapping.RequiredError(types.Parent{})
|
||||
if err == nil || !strings.Contains(err.Error(), "RequiredError.Child") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = mapping.RequirePointer(types.Parent{})
|
||||
if err == nil || !strings.Contains(err.Error(), "RequirePointer.Child") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lift, err := mapping.Lift(domain.ValueHolder{Child: domain.Child{Name: "lift"}})
|
||||
if err != nil || lift.Child == nil || lift.Child.Name != "lift" {
|
||||
t.Fatal(lift, err)
|
||||
}
|
||||
batch, err := mapping.Batch(domain.InputBatch{Fixed: [2]*domain.Child{nil, {Name: "array"}}})
|
||||
if err != nil || batch.Fixed[0] != nil || batch.Fixed[1].Name != "array" {
|
||||
t.Fatal(batch, err)
|
||||
}
|
||||
_, err = mapping.Batch(domain.InputBatch{Fixed: [2]*domain.Child{nil, {Name: "bad"}}})
|
||||
if !errors.Is(err, convert.ErrValue) || !strings.Contains(err.Error(), "Batch.Fixed[1]") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, input := range []types.Parent{{Child: &types.Child{Name: "bad"}}, {Children: []*types.Child{nil, {Name: "bad"}}}} {
|
||||
failed, err := mapping.ToParentPtr(input)
|
||||
if failed != nil || !errors.Is(err, convert.ErrValue) || !strings.Contains(err.Error(), "ToChild.Name") {
|
||||
t.Fatal(failed, err)
|
||||
}
|
||||
if len(input.Children) > 0 && !strings.Contains(err.Error(), "ToParent.Children[1]") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value, err := mapping.ToParent(input)
|
||||
if value.Child != nil || value.Children != nil || !errors.Is(err, convert.ErrValue) {
|
||||
t.Fatal("partial result escaped", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitNilAndConcurrentCalls(t *testing.T) {
|
||||
zero, err := mapping.ScalarZero(domain.ScalarInput{})
|
||||
if err != nil || zero != (domain.ScalarOutput{}) {
|
||||
t.Fatal(zero, err)
|
||||
}
|
||||
_, err = mapping.ScalarError(domain.ScalarInput{})
|
||||
if err == nil {
|
||||
t.Fatal("nil accepted")
|
||||
}
|
||||
text, count, enabled := "", int64(0), false
|
||||
out, err := mapping.ScalarError(domain.ScalarInput{Text: &text, Count: &count, Enabled: &enabled})
|
||||
if err != nil || out != (domain.ScalarOutput{}) {
|
||||
t.Fatal("non-nil zero rejected", out, err)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 32; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
out, err := mapping.ToParent(types.Parent{Child: &types.Child{Name: "parallel"}})
|
||||
if err != nil || out.Child == nil || out.Child.Name != "parallel" {
|
||||
t.Error(out, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPNullableMapping(t *testing.T) {
|
||||
cfg := logx.Default()
|
||||
cfg.Writer = io.Discard
|
||||
logger, err := logx.New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := svc.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer deps.Close()
|
||||
app, err := handler.NewApp(deps)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer app.Close()
|
||||
for _, body := range []string{`{}`, `{"child":null}`, `{"child":{"name":""}}`, `{"child":{"name":"one"},"children":[null,{"name":"two"}]}`, `{"children":[]}`} {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
app.Engine().ServeHTTP(w, req)
|
||||
if w.Code != 200 {
|
||||
t.Fatal(w.Code, w.Body.String())
|
||||
}
|
||||
var input, output types.Parent
|
||||
if err := json.Unmarshal([]byte(body), &input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if (input.Child == nil) != (output.Child == nil) || (input.Children == nil) != (output.Children == nil) || len(input.Children) != len(output.Children) {
|
||||
t.Fatal(body, w.Body.String())
|
||||
}
|
||||
if input.Child != nil && input.Child.Name != output.Child.Name {
|
||||
t.Fatal(w.Body.String())
|
||||
}
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/", strings.NewReader(`{"child":{"name":"bad"}}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
app.Engine().ServeHTTP(w, req)
|
||||
if w.Code != 500 || strings.Contains(w.Body.String(), convert.ErrValue.Error()) || strings.Contains(w.Body.String(), "ToChild") {
|
||||
t.Fatal("private cause leaked", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
15
cmd/go-omni/apigen/testdata/nullable/convert.go
vendored
15
cmd/go-omni/apigen/testdata/nullable/convert.go
vendored
@@ -1,15 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var ErrValue = errors.New("private conversion cause")
|
||||
|
||||
func Name(value string) (string, error) {
|
||||
if value == "bad" {
|
||||
return "", fmt.Errorf("conversion refused: %w", ErrValue)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
23
cmd/go-omni/apigen/testdata/nullable/domain.go
vendored
23
cmd/go-omni/apigen/testdata/nullable/domain.go
vendored
@@ -1,23 +0,0 @@
|
||||
package domain
|
||||
|
||||
type Child struct{ Name string }
|
||||
type ChildDTO struct{ Name string }
|
||||
type Parent struct {
|
||||
Child *Child
|
||||
Children []*Child
|
||||
}
|
||||
type Required struct{ Child Child }
|
||||
type ValueHolder struct{ Child Child }
|
||||
type PointerHolder struct{ Child *ChildDTO }
|
||||
type InputBatch struct{ Fixed [2]*Child }
|
||||
type OutputBatch struct{ Fixed [2]*ChildDTO }
|
||||
type ScalarInput struct {
|
||||
Text *string
|
||||
Count *int64
|
||||
Enabled *bool
|
||||
}
|
||||
type ScalarOutput struct {
|
||||
Text string
|
||||
Count int64
|
||||
Enabled bool
|
||||
}
|
||||
15
cmd/go-omni/apigen/testdata/nullable/service.api
vendored
15
cmd/go-omni/apigen/testdata/nullable/service.api
vendored
@@ -1,15 +0,0 @@
|
||||
type Child {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type Parent {
|
||||
Child *Child `json:"child,optional"`
|
||||
Children []*Child `json:"children,optional"`
|
||||
}
|
||||
@server (
|
||||
group: probe
|
||||
response: raw
|
||||
)
|
||||
service probe {
|
||||
@handler roundTrip
|
||||
post / (Parent) returns (Parent)
|
||||
}
|
||||
Reference in New Issue
Block a user