Skip to content

Commit 16d81c5

Browse files
authored
fix(docs): ensure cross-package refs are inlined (#201)
* fix(docs): ensure cross-package refs are inlined The docs plugin was failing to inline type references for types defined in other design packages. This was because it processed each design root in isolation, resulting in an incomplete map of definitions when resolving refs. This commit refactors the generator to first build a global map of all type definitions from all design roots. This complete map is then used for the inlining process, ensuring that cross-package and cross-service references are correctly resolved. A new test case, , has been added to verify this behavior and prevent regressions. * refactor(docs): update golden files after generator change * refactor(docs): update generated code and golden files
1 parent ec22000 commit 16d81c5

8 files changed

Lines changed: 130 additions & 18 deletions

.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,33 @@
1313

1414
*.swp
1515
.vscode/*
16+
17+
*.out
18+
*.test
19+
gengoalint
20+
21+
# OS generated files
22+
.DS_Store
23+
.DS_Store?
24+
._*
25+
.Spotlight-V100
26+
.Trashes
27+
ehthumbs.db
28+
Thumbs.db
29+
30+
# Binaries for programs and plugins
31+
*.so
32+
*.so.*
33+
*.a
34+
*.dll
35+
*.exe
36+
*.exe~
37+
*.test
38+
*.out
39+
*.log
40+
41+
# vendored dependencies
42+
vendor/
43+
44+
# protoc
45+
protoc-*/

docs/generate.go

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,59 @@ func init() { Register() }
3030

3131
// Generate produces the documentation JSON file.
3232
func Generate(_ string, roots []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
33+
// First, build a complete map of all definitions from all roots.
34+
// This ensures that cross-package type references can be resolved.
35+
allDefs := make(map[string]*openapi.Schema)
3336
for _, root := range roots {
3437
if r, ok := root.(*expr.RootExpr); ok {
35-
files = append(files, docsFile(r))
38+
// Create a temporary, isolated context for each root to avoid global state pollution.
39+
prev := openapi.Definitions
40+
openapi.Definitions = make(map[string]*openapi.Schema)
41+
42+
for _, tpe := range r.Types {
43+
if ut, ok := tpe.(*expr.UserTypeExpr); ok {
44+
openapi.GenerateTypeDefinition(r.API, ut)
45+
}
46+
}
47+
for _, rt := range r.ResultTypes {
48+
openapi.GenerateResultTypeDefinition(r.API, rt, expr.DefaultView)
49+
}
50+
51+
// Merge the generated definitions into the global map.
52+
for n, s := range openapi.Definitions {
53+
if _, exists := allDefs[n]; !exists {
54+
allDefs[n] = dupSchema(s)
55+
}
56+
}
57+
58+
// Restore the original global definitions to maintain isolation.
59+
openapi.Definitions = prev
60+
}
61+
}
62+
63+
for _, root := range roots {
64+
if r, ok := root.(*expr.RootExpr); ok {
65+
files = append(files, docsFile(r, allDefs))
3666
}
3767
}
3868
return files, nil
3969
}
4070

41-
func docsFile(r *expr.RootExpr) *codegen.File {
42-
71+
func docsFile(r *expr.RootExpr, allDefs map[string]*openapi.Schema) *codegen.File {
4372
docs := &data{
4473
API: apiDocs(r.API),
4574
Services: servicesDocs(r),
4675
}
4776

4877
// Default behavior: use global OpenAPI definitions to preserve ordering and
4978
// compatibility with existing golden tests.
50-
defs := openapi.Definitions
79+
defs := allDefs
5180

5281
// If either option is enabled, build a local definition map for this root
5382
// and apply transforms/inlining as needed, isolating from global state.
54-
if plugexpr.Root.UseJSONTags || plugexpr.Root.InlineRefs {
83+
if plugexpr.Root.UseJSONTags {
84+
// Re-scope the definitions to only those present in the current root,
85+
// but use the globally-aware `allDefs` for lookups during transforms.
5586
local := make(map[string]*openapi.Schema)
5687
prev := openapi.Definitions
5788
openapi.Definitions = make(map[string]*openapi.Schema)
@@ -63,8 +94,10 @@ func docsFile(r *expr.RootExpr) *codegen.File {
6394
for _, rt := range r.ResultTypes {
6495
openapi.GenerateResultTypeDefinition(r.API, rt, expr.DefaultView)
6596
}
66-
for n, s := range openapi.Definitions {
67-
local[n] = dupSchema(s)
97+
for n := range openapi.Definitions {
98+
if def, ok := allDefs[n]; ok {
99+
local[n] = dupSchema(def)
100+
}
68101
}
69102
openapi.Definitions = prev
70103

@@ -92,31 +125,38 @@ func docsFile(r *expr.RootExpr) *codegen.File {
92125

93126
// Inline $refs if requested via DSL flag.
94127
if plugexpr.Root.InlineRefs {
128+
// When inlining, use the complete set of definitions from all roots
129+
// to ensure cross-package references can be resolved.
130+
inliningDefs := allDefs
131+
if plugexpr.Root.UseJSONTags {
132+
inliningDefs = transformDefinitionsWithJSONTagsHybrid(r, allDefs, nil)
133+
}
134+
95135
// Inline inside service payloads/results/errors.
96136
for _, svc := range docs.Services {
97137
for _, m := range svc.Methods {
98138
if m.Payload != nil && m.Payload.Type != nil {
99-
inlineRefsInSchema(m.Payload.Type, defs, make(map[string]bool))
139+
inlineRefsInSchema(m.Payload.Type, inliningDefs, make(map[string]bool))
100140
}
101141
if m.StreamingPayload != nil && m.StreamingPayload.Type != nil {
102-
inlineRefsInSchema(m.StreamingPayload.Type, defs, make(map[string]bool))
142+
inlineRefsInSchema(m.StreamingPayload.Type, inliningDefs, make(map[string]bool))
103143
}
104144
if m.Result != nil && m.Result.Type != nil {
105-
inlineRefsInSchema(m.Result.Type, defs, make(map[string]bool))
145+
inlineRefsInSchema(m.Result.Type, inliningDefs, make(map[string]bool))
106146
}
107147
if m.StreamingResult != nil && m.StreamingResult.Type != nil {
108-
inlineRefsInSchema(m.StreamingResult.Type, defs, make(map[string]bool))
148+
inlineRefsInSchema(m.StreamingResult.Type, inliningDefs, make(map[string]bool))
109149
}
110150
for _, e := range m.Errors {
111151
if e != nil && e.Type != nil {
112-
inlineRefsInSchema(e.Type, defs, make(map[string]bool))
152+
inlineRefsInSchema(e.Type, inliningDefs, make(map[string]bool))
113153
}
114154
}
115155
}
116156
}
117157
// Inline inside definitions themselves (properties that refer to other defs).
118158
for _, def := range defs {
119-
inlineRefsInSchema(def, defs, make(map[string]bool))
159+
inlineRefsInSchema(def, inliningDefs, make(map[string]bool))
120160
}
121161
}
122162

docs/generate_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,45 @@ func TestJSONTagsAndInlineRefs_Complex(t *testing.T) {
291291
t.Fatalf("expected required array in result type, got: %#v", rt)
292292
}
293293
}
294+
295+
func TestInlineRefs_CrossService(t *testing.T) {
296+
t.Cleanup(func() { plugexpr.Root.UseJSONTags = false; plugexpr.Root.InlineRefs = false })
297+
docsMap := genDocs(t, func() {
298+
InlineRefs()
299+
API("Test", func() {})
300+
var SharedType = Type("SharedType", func() {
301+
Field(1, "A", String)
302+
Required("A")
303+
})
304+
Service("S1", func() {
305+
Method("M1", func() {
306+
Payload(SharedType)
307+
HTTP(func() { GET("/") })
308+
GRPC(func() {})
309+
})
310+
})
311+
Service("S2", func() {
312+
Method("M2", func() {
313+
Payload(SharedType)
314+
HTTP(func() { GET("/s2") })
315+
GRPC(func() {})
316+
})
317+
})
318+
})
319+
320+
// Check S1
321+
s1 := docsMap["services"].(map[string]any)["S1"].(map[string]any)
322+
m1 := s1["methods"].(map[string]any)["M1"].(map[string]any)
323+
pt1 := m1["payload"].(map[string]any)["type"].(map[string]any)
324+
if _, hasRef := pt1["$ref"]; hasRef {
325+
t.Fatalf("S1: expected inlined payload schema for shared type, found $ref: %#v", pt1)
326+
}
327+
328+
// Check S2
329+
s2 := docsMap["services"].(map[string]any)["S2"].(map[string]any)
330+
m2 := s2["methods"].(map[string]any)["M2"].(map[string]any)
331+
pt2 := m2["payload"].(map[string]any)["type"].(map[string]any)
332+
if _, hasRef := pt2["$ref"]; hasRef {
333+
t.Fatalf("S2: expected inlined payload schema for shared type, found $ref: %#v", pt2)
334+
}
335+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"type":"array","items":{"type":"string","example":"In voluptatem consectetur."}},"example":["Accusamus saepe et sit.","Deleniti soluta veritatis odit minus voluptatum."]}}}}},"definitions":{"User":{"title":"User","type":"object","properties":{"att1":{"type":"string","example":"In voluptatem consectetur."},"att2":{"type":"integer","example":443436312039258672,"format":"int64"}},"example":{"att1":"Accusamus saepe et sit.","att2":8511135955551101225}}}}
1+
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"type":"array","items":{"type":"string","example":"In voluptatem consectetur."}},"example":["Accusamus saepe et sit.","Deleniti soluta veritatis odit minus voluptatum."]}}}}},"definitions":{}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"type":"object","additionalProperties":{"type":"integer","example":650424415,"format":"int32"}},"example":{"Consectetur consequatur necessitatibus accusamus saepe et.":1637648643}}}}}},"definitions":{"User":{"title":"User","type":"object","properties":{"att1":{"type":"string","example":"In voluptatem consectetur."},"att2":{"type":"integer","example":443436312039258672,"format":"int64"}},"example":{"att1":"Accusamus saepe et sit.","att2":8511135955551101225}}}}
1+
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"type":"object","additionalProperties":{"type":"integer","example":650424415,"format":"int32"}},"example":{"Consectetur consequatur necessitatibus accusamus saepe et.":1637648643}}}}}},"definitions":{}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"type":"string"},"example":"In voluptatem consectetur."}}}}},"definitions":{"User":{"title":"User","type":"object","properties":{"att1":{"type":"string","example":"In voluptatem consectetur."},"att2":{"type":"integer","example":443436312039258672,"format":"int64"}},"example":{"att1":"Accusamus saepe et sit.","att2":8511135955551101225}}}}
1+
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"type":"string"},"example":"In voluptatem consectetur."}}}}},"definitions":{}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"$ref":"#/definitions/User"},"example":{"att1":"In voluptatem consectetur.","att2":443436312039258672}}}}}},"definitions":{"User":{"title":"User","type":"object","properties":{"att1":{"type":"string","example":"In voluptatem consectetur."},"att2":{"type":"integer","example":443436312039258672,"format":"int64"}},"example":{"att1":"Accusamus saepe et sit.","att2":8511135955551101225}}}}
1+
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","result":{"type":{"$ref":"#/definitions/User"},"example":{"att1":"Soluta veritatis odit minus voluptatum sunt commodi.","att2":2887366790483849171}}}}}},"definitions":{"User":{"title":"User","type":"object","properties":{"att1":{"type":"string","example":"In voluptatem consectetur."},"att2":{"type":"integer","example":443436312039258672,"format":"int64"}},"example":{"att1":"Accusamus saepe et sit.","att2":8511135955551101225}}}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","payload":{"type":{"$ref":"#/definitions/User"},"example":{"att1":"Soluta veritatis odit minus voluptatum sunt commodi.","att2":2887366790483849171}}}}}},"definitions":{"User":{"title":"User","type":"object","properties":{"att1":{"type":"string","example":"In voluptatem consectetur."},"att2":{"type":"integer","example":443436312039258672,"format":"int64"}},"example":{"att1":"Accusamus saepe et sit.","att2":8511135955551101225}}}}
1+
{"api":{"name":"Test API","version":"0.0.1","servers":{"Test API":{"name":"Test API","description":"Default server for Test API","services":["Service"],"hosts":{"localhost":{"name":"localhost","server":"Test API","uris":["http://localhost:80","grpc://localhost:8080"]}}}}},"services":{"Service":{"name":"Service","methods":{"Method":{"name":"Method","payload":{"type":{"$ref":"#/definitions/User"},"example":{"att1":"Exercitationem suscipit deleniti.","att2":7517419439717785447}}}}}},"definitions":{"User":{"title":"User","type":"object","properties":{"att1":{"type":"string","example":"In voluptatem consectetur."},"att2":{"type":"integer","example":443436312039258672,"format":"int64"}},"example":{"att1":"Accusamus saepe et sit.","att2":8511135955551101225}}}}

0 commit comments

Comments
 (0)