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
30 changes: 30 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,33 @@

*.swp
.vscode/*

*.out
*.test
gengoalint

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Binaries for programs and plugins
*.so
*.so.*
*.a
*.dll
*.exe
*.exe~
*.test
*.out
*.log

# vendored dependencies
vendor/

# protoc
protoc-*/
66 changes: 53 additions & 13 deletions docs/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,59 @@ func init() { Register() }

// Generate produces the documentation JSON file.
func Generate(_ string, roots []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
// First, build a complete map of all definitions from all roots.
// This ensures that cross-package type references can be resolved.
allDefs := make(map[string]*openapi.Schema)
for _, root := range roots {
if r, ok := root.(*expr.RootExpr); ok {
files = append(files, docsFile(r))
// Create a temporary, isolated context for each root to avoid global state pollution.
prev := openapi.Definitions
openapi.Definitions = make(map[string]*openapi.Schema)

for _, tpe := range r.Types {
if ut, ok := tpe.(*expr.UserTypeExpr); ok {
openapi.GenerateTypeDefinition(r.API, ut)
}
}
for _, rt := range r.ResultTypes {
openapi.GenerateResultTypeDefinition(r.API, rt, expr.DefaultView)
}

// Merge the generated definitions into the global map.
for n, s := range openapi.Definitions {
if _, exists := allDefs[n]; !exists {
allDefs[n] = dupSchema(s)
}
}

// Restore the original global definitions to maintain isolation.
openapi.Definitions = prev
}
}

for _, root := range roots {
if r, ok := root.(*expr.RootExpr); ok {
files = append(files, docsFile(r, allDefs))
}
}
return files, nil
}

func docsFile(r *expr.RootExpr) *codegen.File {

func docsFile(r *expr.RootExpr, allDefs map[string]*openapi.Schema) *codegen.File {
docs := &data{
API: apiDocs(r.API),
Services: servicesDocs(r),
}

// Default behavior: use global OpenAPI definitions to preserve ordering and
// compatibility with existing golden tests.
defs := openapi.Definitions
defs := allDefs

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

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

// Inline $refs if requested via DSL flag.
if plugexpr.Root.InlineRefs {
// When inlining, use the complete set of definitions from all roots
// to ensure cross-package references can be resolved.
inliningDefs := allDefs
if plugexpr.Root.UseJSONTags {
inliningDefs = transformDefinitionsWithJSONTagsHybrid(r, allDefs, nil)
}

// Inline inside service payloads/results/errors.
for _, svc := range docs.Services {
for _, m := range svc.Methods {
if m.Payload != nil && m.Payload.Type != nil {
inlineRefsInSchema(m.Payload.Type, defs, make(map[string]bool))
inlineRefsInSchema(m.Payload.Type, inliningDefs, make(map[string]bool))
}
if m.StreamingPayload != nil && m.StreamingPayload.Type != nil {
inlineRefsInSchema(m.StreamingPayload.Type, defs, make(map[string]bool))
inlineRefsInSchema(m.StreamingPayload.Type, inliningDefs, make(map[string]bool))
}
if m.Result != nil && m.Result.Type != nil {
inlineRefsInSchema(m.Result.Type, defs, make(map[string]bool))
inlineRefsInSchema(m.Result.Type, inliningDefs, make(map[string]bool))
}
if m.StreamingResult != nil && m.StreamingResult.Type != nil {
inlineRefsInSchema(m.StreamingResult.Type, defs, make(map[string]bool))
inlineRefsInSchema(m.StreamingResult.Type, inliningDefs, make(map[string]bool))
}
for _, e := range m.Errors {
if e != nil && e.Type != nil {
inlineRefsInSchema(e.Type, defs, make(map[string]bool))
inlineRefsInSchema(e.Type, inliningDefs, make(map[string]bool))
}
}
}
}
// Inline inside definitions themselves (properties that refer to other defs).
for _, def := range defs {
inlineRefsInSchema(def, defs, make(map[string]bool))
inlineRefsInSchema(def, inliningDefs, make(map[string]bool))
}
}

Expand Down
42 changes: 42 additions & 0 deletions docs/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,45 @@ func TestJSONTagsAndInlineRefs_Complex(t *testing.T) {
t.Fatalf("expected required array in result type, got: %#v", rt)
}
}

func TestInlineRefs_CrossService(t *testing.T) {
t.Cleanup(func() { plugexpr.Root.UseJSONTags = false; plugexpr.Root.InlineRefs = false })
docsMap := genDocs(t, func() {
InlineRefs()
API("Test", func() {})
var SharedType = Type("SharedType", func() {
Field(1, "A", String)
Required("A")
})
Service("S1", func() {
Method("M1", func() {
Payload(SharedType)
HTTP(func() { GET("/") })
GRPC(func() {})
})
})
Service("S2", func() {
Method("M2", func() {
Payload(SharedType)
HTTP(func() { GET("/s2") })
GRPC(func() {})
})
})
})

// Check S1
s1 := docsMap["services"].(map[string]any)["S1"].(map[string]any)
m1 := s1["methods"].(map[string]any)["M1"].(map[string]any)
pt1 := m1["payload"].(map[string]any)["type"].(map[string]any)
if _, hasRef := pt1["$ref"]; hasRef {
t.Fatalf("S1: expected inlined payload schema for shared type, found $ref: %#v", pt1)
}

// Check S2
s2 := docsMap["services"].(map[string]any)["S2"].(map[string]any)
m2 := s2["methods"].(map[string]any)["M2"].(map[string]any)
pt2 := m2["payload"].(map[string]any)["type"].(map[string]any)
if _, hasRef := pt2["$ref"]; hasRef {
t.Fatalf("S2: expected inlined payload schema for shared type, found $ref: %#v", pt2)
}
}
2 changes: 1 addition & 1 deletion docs/testdata/no-payload-array-return.json
Original file line number Diff line number Diff line change
@@ -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}}}}
{"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":{}}
2 changes: 1 addition & 1 deletion docs/testdata/no-payload-map-return.json
Original file line number Diff line number Diff line change
@@ -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}}}}
{"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":{}}
2 changes: 1 addition & 1 deletion docs/testdata/no-payload-primitive-return.json
Original file line number Diff line number Diff line change
@@ -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}}}}
{"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":{}}
2 changes: 1 addition & 1 deletion docs/testdata/no-payload-user-return.json
Original file line number Diff line number Diff line change
@@ -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}}}}
{"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}}}}
2 changes: 1 addition & 1 deletion docs/testdata/user-payload-no-return.json
Original file line number Diff line number Diff line change
@@ -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}}}}
{"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}}}}
Loading