diff --git a/cmd/deep-gen/main.go b/cmd/deep-gen/main.go index 388f2bd..d0367ab 100644 --- a/cmd/deep-gen/main.go +++ b/cmd/deep-gen/main.go @@ -487,7 +487,6 @@ import ( {{- end}} {{- if .NeedsDeep}} deep "github.com/brunoga/deep/v5" - _deepengine "github.com/brunoga/deep/v5/internal/engine" {{- end}} {{- if .NeedsCrdt}} crdt "github.com/brunoga/deep/v5/crdt" @@ -515,7 +514,7 @@ func (t *{{.TypeName}}) Patch(p {{.P}}Patch[{{.TypeName}}], logger *slog.Logger) if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := {{.P}}ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/cmd/deep-gen/main_test.go b/cmd/deep-gen/main_test.go index e3b67c9..6f60d0a 100644 --- a/cmd/deep-gen/main_test.go +++ b/cmd/deep-gen/main_test.go @@ -1,9 +1,12 @@ package main import ( + "go/parser" + "go/token" "os" "os/exec" "path/filepath" + "strconv" "strings" "testing" ) @@ -43,3 +46,44 @@ func TestGeneratorOutput(t *testing.T) { t.Errorf("generator output does not match golden file\nwant:\n%s\n\ngot:\n%s", goldenStr, gotStr) } } + +// TestGeneratedCodeHasNoInternalImports asserts that code emitted by deep-gen +// never imports an internal/... package of this module. Such imports compile +// inside the module but break for any downstream user — see the bug fixed by +// exposing deep.ApplyOpReflection as a public wrapper around the engine's +// reflection fallback. +func TestGeneratedCodeHasNoInternalImports(t *testing.T) { + tmpDir := t.TempDir() + genBin := filepath.Join(tmpDir, "deep-gen") + if out, err := exec.Command("go", "build", "-o", genBin, ".").CombinedOutput(); err != nil { + t.Fatalf("build deep-gen: %v\n%s", err, out) + } + + // internal/testmodels is rich enough to exercise the reflection fallback + // (which is what previously dragged the internal/engine import in). + outFile := filepath.Join(tmpDir, "user_deep.go") + if out, err := exec.Command(genBin, "-type=User,Detail", "-output", outFile, "../../internal/testmodels").CombinedOutput(); err != nil { + t.Fatalf("run deep-gen: %v\n%s", err, out) + } + + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, outFile, nil, parser.ImportsOnly) + if err != nil { + t.Fatalf("parse generated file: %v", err) + } + + const modulePrefix = "github.com/brunoga/deep/v5/" + for _, imp := range file.Imports { + path, err := strconv.Unquote(imp.Path.Value) + if err != nil { + t.Fatalf("unquote import %q: %v", imp.Path.Value, err) + } + if !strings.HasPrefix(path, modulePrefix) { + continue + } + rel := strings.TrimPrefix(path, modulePrefix) + if rel == "internal" || strings.HasPrefix(rel, "internal/") || strings.Contains(rel, "/internal/") { + t.Errorf("generated code imports internal package %q; expose a public wrapper instead", path) + } + } +} diff --git a/engine.go b/engine.go index 7a2f1b3..03ece6a 100644 --- a/engine.go +++ b/engine.go @@ -31,6 +31,16 @@ func WithLogger(l *slog.Logger) ApplyOption { return func(c *applyConfig) { c.logger = l } } +// ApplyOpReflection applies a single Operation to target using reflection. +// +// This is intended for use by code generated by deep-gen as a fallback for +// operations the generated fast-path does not handle (e.g. slice index or map +// key paths). It is not part of the stable user-facing API; prefer [Apply] +// for normal use. +func ApplyOpReflection[T any](target *T, op Operation, logger *slog.Logger) error { + return engine.ApplyOpReflection(target, op, logger) +} + // Apply applies a Patch to a target pointer. // v5 prioritizes the generated Patch method but falls back to reflection if needed. // diff --git a/examples/atomic_config/proxyconfig_deep.go b/examples/atomic_config/proxyconfig_deep.go index 2efca95..851414f 100644 --- a/examples/atomic_config/proxyconfig_deep.go +++ b/examples/atomic_config/proxyconfig_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" ) @@ -31,7 +30,7 @@ func (t *ProxyConfig) Patch(p deep.Patch[ProxyConfig], logger *slog.Logger) erro if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } @@ -309,7 +308,7 @@ func (t *SystemMeta) Patch(p deep.Patch[SystemMeta], logger *slog.Logger) error if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/audit_logging/user_deep.go b/examples/audit_logging/user_deep.go index 2ee2ee2..e970ee4 100644 --- a/examples/audit_logging/user_deep.go +++ b/examples/audit_logging/user_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" "strings" @@ -32,7 +31,7 @@ func (t *User) Patch(p deep.Patch[User], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/concurrent_updates/stock_deep.go b/examples/concurrent_updates/stock_deep.go index ece9251..44c6cf8 100644 --- a/examples/concurrent_updates/stock_deep.go +++ b/examples/concurrent_updates/stock_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" ) @@ -31,7 +30,7 @@ func (t *Stock) Patch(p deep.Patch[Stock], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/config_manager/config_deep.go b/examples/config_manager/config_deep.go index 838a797..4fded8b 100644 --- a/examples/config_manager/config_deep.go +++ b/examples/config_manager/config_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" "strings" @@ -32,7 +31,7 @@ func (t *Config) Patch(p deep.Patch[Config], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/http_patch_api/resource_deep.go b/examples/http_patch_api/resource_deep.go index f1d7672..b8bee12 100644 --- a/examples/http_patch_api/resource_deep.go +++ b/examples/http_patch_api/resource_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" ) @@ -31,7 +30,7 @@ func (t *Resource) Patch(p deep.Patch[Resource], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/json_interop/uistate_deep.go b/examples/json_interop/uistate_deep.go index 4ba2fd6..b2751f6 100644 --- a/examples/json_interop/uistate_deep.go +++ b/examples/json_interop/uistate_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" ) @@ -31,7 +30,7 @@ func (t *UIState) Patch(p deep.Patch[UIState], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/keyed_inventory/inventory_deep.go b/examples/keyed_inventory/inventory_deep.go index 16fafcb..dc0235a 100644 --- a/examples/keyed_inventory/inventory_deep.go +++ b/examples/keyed_inventory/inventory_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" ) @@ -31,7 +30,7 @@ func (t *Item) Patch(p deep.Patch[Item], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } @@ -309,7 +308,7 @@ func (t *Inventory) Patch(p deep.Patch[Inventory], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/multi_error/strictuser_deep.go b/examples/multi_error/strictuser_deep.go index b2442db..6b1f594 100644 --- a/examples/multi_error/strictuser_deep.go +++ b/examples/multi_error/strictuser_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" ) @@ -31,7 +30,7 @@ func (t *StrictUser) Patch(p deep.Patch[StrictUser], logger *slog.Logger) error if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/policy_engine/employee_deep.go b/examples/policy_engine/employee_deep.go index 48b96b2..ce03ed8 100644 --- a/examples/policy_engine/employee_deep.go +++ b/examples/policy_engine/employee_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" ) @@ -31,7 +30,7 @@ func (t *Employee) Patch(p deep.Patch[Employee], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/state_management/docstate_deep.go b/examples/state_management/docstate_deep.go index ff200b5..7e77670 100644 --- a/examples/state_management/docstate_deep.go +++ b/examples/state_management/docstate_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" "strings" @@ -32,7 +31,7 @@ func (t *DocState) Patch(p deep.Patch[DocState], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/struct_map_keys/fleet_deep.go b/examples/struct_map_keys/fleet_deep.go index 165dbc6..1b84ff2 100644 --- a/examples/struct_map_keys/fleet_deep.go +++ b/examples/struct_map_keys/fleet_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" ) @@ -30,7 +29,7 @@ func (t *Fleet) Patch(p deep.Patch[Fleet], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/three_way_merge/systemconfig_deep.go b/examples/three_way_merge/systemconfig_deep.go index f14d549..9436776 100644 --- a/examples/three_way_merge/systemconfig_deep.go +++ b/examples/three_way_merge/systemconfig_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" "strings" @@ -32,7 +31,7 @@ func (t *SystemConfig) Patch(p deep.Patch[SystemConfig], logger *slog.Logger) er if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/examples/websocket_sync/gameworld_deep.go b/examples/websocket_sync/gameworld_deep.go index 9aad386..87888ba 100644 --- a/examples/websocket_sync/gameworld_deep.go +++ b/examples/websocket_sync/gameworld_deep.go @@ -5,7 +5,6 @@ import ( "fmt" deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" "strings" @@ -32,7 +31,7 @@ func (t *GameWorld) Patch(p deep.Patch[GameWorld], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } @@ -314,7 +313,7 @@ func (t *Player) Patch(p deep.Patch[Player], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } diff --git a/internal/testmodels/user_deep.go b/internal/testmodels/user_deep.go index 70fa749..e4d1589 100644 --- a/internal/testmodels/user_deep.go +++ b/internal/testmodels/user_deep.go @@ -6,7 +6,6 @@ import ( deep "github.com/brunoga/deep/v5" "github.com/brunoga/deep/v5/condition" crdt "github.com/brunoga/deep/v5/crdt" - _deepengine "github.com/brunoga/deep/v5/internal/engine" "log/slog" "regexp" "strings" @@ -33,7 +32,7 @@ func (t *User) Patch(p deep.Patch[User], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } } @@ -564,7 +563,7 @@ func (t *Detail) Patch(p deep.Patch[Detail], logger *slog.Logger) error { if err != nil { errs = append(errs, err) } else if !handled { - if err := _deepengine.ApplyOpReflection(t, op, logger); err != nil { + if err := deep.ApplyOpReflection(t, op, logger); err != nil { errs = append(errs, err) } }