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
3 changes: 1 addition & 2 deletions cmd/deep-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
Expand Down
44 changes: 44 additions & 0 deletions cmd/deep-gen/main_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package main

import (
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
)
Expand Down Expand Up @@ -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)
}
}
}
10 changes: 10 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
5 changes: 2 additions & 3 deletions examples/atomic_config/proxyconfig_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/audit_logging/user_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/concurrent_updates/stock_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/config_manager/config_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/http_patch_api/resource_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/json_interop/uistate_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions examples/keyed_inventory/inventory_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/multi_error/strictuser_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/policy_engine/employee_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/state_management/docstate_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/struct_map_keys/fleet_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions examples/three_way_merge/systemconfig_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions examples/websocket_sync/gameworld_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions internal/testmodels/user_deep.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading