-
Notifications
You must be signed in to change notification settings - Fork 1
feat: recognize generated schema types (SchemaTypeName + UnwrapSchema) #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||||||||||
| package modusgraph | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import "reflect" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Schema identifies a value as a record of a generated schema-defining type. | ||||||||||||||||||||||
| // modusgraph-gen-emitted schema structs implement this via a generated | ||||||||||||||||||||||
| // SchemaTypeName() method that returns the canonical entity name | ||||||||||||||||||||||
| // (e.g. "Studio"). The interface is intentionally minimal — a single method | ||||||||||||||||||||||
| // returning a useful piece of metadata. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // Plain user structs (not emitted by modusgraph-gen) do not implement Schema | ||||||||||||||||||||||
| // and are unaffected by the modusgraph.Client routing it enables; they pass | ||||||||||||||||||||||
| // through to the existing reflection-based dgman pipeline exactly as before. | ||||||||||||||||||||||
| type Schema interface { | ||||||||||||||||||||||
| SchemaTypeName() string | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // UnwrapSchema returns the schema-defining record contained in obj. If obj | ||||||||||||||||||||||
| // is nil, it is returned as-is. If obj is already a Schema, it is returned | ||||||||||||||||||||||
| // as-is. If obj exposes an Unwrap() method whose return value satisfies | ||||||||||||||||||||||
| // Schema, that return is substituted. Otherwise obj is returned unchanged. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // This is the bridge between modusgraph-gen-emitted wrapper types and the | ||||||||||||||||||||||
| // rest of modusgraph.Client. It is purely additive: types that don't | ||||||||||||||||||||||
| // implement Schema and don't have an Unwrap() method (i.e. existing | ||||||||||||||||||||||
| // modusgraph users' plain structs) pass through untouched. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // Note on errors.Unwrap overlap: Go's errors package uses Unwrap() error | ||||||||||||||||||||||
| // as the standard "give me the wrapped thing" method. UnwrapSchema's | ||||||||||||||||||||||
| // secondary check (the returned value must itself implement Schema) means | ||||||||||||||||||||||
| // an error wrapper is not mistaken for a modusgraph wrapper — the | ||||||||||||||||||||||
| // reflection probe finds Unwrap(), calls it, gets an error, fails the | ||||||||||||||||||||||
| // Schema check, and returns the original obj. | ||||||||||||||||||||||
| func UnwrapSchema(obj any) any { | ||||||||||||||||||||||
| if obj == nil { | ||||||||||||||||||||||
| return obj | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if _, ok := obj.(Schema); ok { | ||||||||||||||||||||||
| return obj | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| v := reflect.ValueOf(obj) | ||||||||||||||||||||||
| if !v.IsValid() { | ||||||||||||||||||||||
| return obj | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| m := v.MethodByName("Unwrap") | ||||||||||||||||||||||
| if !m.IsValid() { | ||||||||||||||||||||||
| return obj | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| mt := m.Type() | ||||||||||||||||||||||
| if mt.NumIn() != 0 || mt.NumOut() != 1 { | ||||||||||||||||||||||
| return obj | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| inner := m.Call(nil)[0].Interface() | ||||||||||||||||||||||
| if _, ok := inner.(Schema); ok { | ||||||||||||||||||||||
|
Comment on lines
+53
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Potential panic when invoking reflected Unwrap() on typed nil pointer wrappers Prompt for AI agents
Suggested change
|
||||||||||||||||||||||
| return inner | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return obj | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package modusgraph | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
| ) | ||
|
|
||
| type fakeRecord struct{ name string } | ||
|
|
||
| func (f *fakeRecord) SchemaTypeName() string { return f.name } | ||
|
|
||
| type fakeWrapper struct{ inner *fakeRecord } | ||
|
|
||
| func (w *fakeWrapper) Unwrap() *fakeRecord { return w.inner } | ||
|
|
||
| type fakeNonSchema struct{ X string } | ||
|
|
||
| func TestUnwrapSchema_PassthroughForPlainStruct(t *testing.T) { | ||
| in := &fakeNonSchema{X: "hi"} | ||
| out := UnwrapSchema(in) | ||
| if out != any(in) { | ||
| t.Fatalf("expected passthrough, got %T", out) | ||
| } | ||
| } | ||
|
|
||
| func TestUnwrapSchema_PassthroughForSchemaStruct(t *testing.T) { | ||
| in := &fakeRecord{name: "Studio"} | ||
| out := UnwrapSchema(in) | ||
| if out != any(in) { | ||
| t.Fatalf("expected passthrough for direct Schema, got %T", out) | ||
| } | ||
| } | ||
|
|
||
| func TestUnwrapSchema_UnwrapsWrapper(t *testing.T) { | ||
| inner := &fakeRecord{name: "Studio"} | ||
| w := &fakeWrapper{inner: inner} | ||
| out := UnwrapSchema(w) | ||
| if out != any(inner) { | ||
| t.Fatalf("expected unwrapped inner, got %T (%v)", out, out) | ||
| } | ||
| } | ||
|
|
||
| func TestUnwrapSchema_IgnoresErrorsUnwrap(t *testing.T) { | ||
| // errors.New("x") has no Unwrap; wrap one to get something with Unwrap() error. | ||
| inner := errors.New("inner") | ||
| outer := &wrappedErr{err: inner} | ||
| out := UnwrapSchema(outer) | ||
| if out != any(outer) { | ||
| t.Fatalf("expected passthrough for error wrapper, got %T", out) | ||
| } | ||
| } | ||
|
|
||
| type wrappedErr struct{ err error } | ||
|
|
||
| func (w *wrappedErr) Error() string { return w.err.Error() } | ||
| func (w *wrappedErr) Unwrap() error { return w.err } | ||
|
|
||
| func TestUnwrapSchema_NilInput(t *testing.T) { | ||
| if out := UnwrapSchema(nil); out != nil { | ||
| t.Fatalf("expected nil for nil input, got %v", out) | ||
| } | ||
| } | ||
|
|
||
| // recordingClient is the minimal surface needed to verify that wrappers | ||
| // passed to the Client interface get unwrapped before reaching internal | ||
| // reflection. It records whatever it received and returns nil. Each method | ||
| // applies obj = UnwrapSchema(obj) at the top, mirroring the patch landing | ||
| // in this task. | ||
| type recordingClient struct { | ||
| seen []any | ||
| } | ||
|
|
||
| func (c *recordingClient) capture(obj any) any { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Tests use a local Prompt for AI agents |
||
| obj = UnwrapSchema(obj) | ||
| c.seen = append(c.seen, obj) | ||
| return obj | ||
| } | ||
|
|
||
| func TestUnwrapSchema_CaptureForwardsInner(t *testing.T) { | ||
| inner := &fakeRecord{name: "Studio"} | ||
| w := &fakeWrapper{inner: inner} | ||
| c := &recordingClient{} | ||
| got := c.capture(w) | ||
| if got != any(inner) { | ||
| t.Fatalf("expected inner record, got %T (%v)", got, got) | ||
| } | ||
| if len(c.seen) != 1 || c.seen[0] != any(inner) { | ||
| t.Fatalf("expected recording to hold inner record, got %v", c.seen) | ||
| } | ||
| } | ||
|
|
||
| func TestUnwrapSchema_CapturePassthroughForPlain(t *testing.T) { | ||
| plain := &fakeNonSchema{X: "y"} | ||
| c := &recordingClient{} | ||
| got := c.capture(plain) | ||
| if got != any(plain) { | ||
| t.Fatalf("expected plain struct passthrough, got %T", got) | ||
| } | ||
| } | ||
|
|
||
| func TestUnwrapSchema_VariadicUnwrapsEachElement(t *testing.T) { | ||
| innerA := &fakeRecord{name: "Studio"} | ||
| innerB := &fakeRecord{name: "Film"} | ||
| templates := []any{ | ||
| &fakeWrapper{inner: innerA}, | ||
| innerB, // already a Schema; passthrough | ||
| } | ||
| for i, obj := range templates { | ||
| templates[i] = UnwrapSchema(obj) | ||
| } | ||
| if templates[0] != any(innerA) { | ||
| t.Fatalf("template[0]: expected innerA, got %T", templates[0]) | ||
| } | ||
| if templates[1] != any(innerB) { | ||
| t.Fatalf("template[1]: expected innerB (passthrough), got %T", templates[1]) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: UnwrapSchema misses pointer-receiver Unwrap() methods when wrapper is passed by value
Prompt for AI agents