Skip to content

Commit 36f43b5

Browse files
committed
Rewritten with reflection-based traversal
1 parent efd73ff commit 36f43b5

File tree

2 files changed

+84
-175
lines changed

2 files changed

+84
-175
lines changed

utils/component/openapi_generator.go

Lines changed: 71 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"reflect"
78
"strings"
89

910
"cuelang.org/go/cue"
@@ -204,188 +205,92 @@ func getResolvedManifest(manifest string) (string, error) {
204205
return string(resolved), nil
205206
}
206207

207-
// clearDocRefs clears $ref strings across the entire OpenAPI document.
208+
// clearDocRefs uses reflection to walk the entire OpenAPI document and clear
209+
// all $ref strings so that json.Marshal outputs fully inlined schemas.
210+
// It uses two tracking mechanisms:
211+
// - visited: permanent set for general pointers to avoid re-processing
212+
// - schemaStack: path-based set for *Schema pointers to detect circular
213+
// schema references (add on enter, remove on exit), allowing the same
214+
// schema to appear in multiple non-circular positions
208215
func clearDocRefs(doc *openapi3.T) {
209-
stack := make(map[*openapi3.Schema]bool)
210-
visited := make(map[*openapi3.PathItem]bool)
216+
visited := make(map[uintptr]bool)
217+
schemaStack := make(map[uintptr]bool)
218+
walkAndClearRefs(reflect.ValueOf(doc), visited, schemaStack)
219+
}
211220

212-
if doc.Components != nil {
213-
for _, sr := range doc.Components.Schemas {
214-
clearSchemaRefs(sr, stack)
215-
}
216-
for _, pr := range doc.Components.Parameters {
217-
clearParameterRefs(pr, stack, visited)
218-
}
219-
for _, hr := range doc.Components.Headers {
220-
clearHeaderRefs(hr, stack, visited)
221-
}
222-
for _, rb := range doc.Components.RequestBodies {
223-
clearRequestBodyRefs(rb, stack, visited)
221+
var schemaRefType = reflect.TypeOf((*openapi3.SchemaRef)(nil))
222+
223+
func walkAndClearRefs(v reflect.Value, visited map[uintptr]bool, schemaStack map[uintptr]bool) {
224+
switch v.Kind() {
225+
case reflect.Ptr:
226+
if v.IsNil() {
227+
return
224228
}
225-
for _, rr := range doc.Components.Responses {
226-
clearResponseRefs(rr, stack, visited)
229+
230+
// SchemaRef needs path-based cycle detection so shared (non-circular)
231+
// schemas are fully expanded while true cycles are broken.
232+
if v.Type() == schemaRefType {
233+
sr := v.Interface().(*openapi3.SchemaRef)
234+
sr.Ref = ""
235+
if sr.Value == nil {
236+
return
237+
}
238+
schemaPtr := reflect.ValueOf(sr.Value).Pointer()
239+
if schemaStack[schemaPtr] {
240+
sr.Value = &openapi3.Schema{}
241+
return
242+
}
243+
schemaStack[schemaPtr] = true
244+
walkAndClearRefs(reflect.ValueOf(sr.Value), visited, schemaStack)
245+
delete(schemaStack, schemaPtr)
246+
return
227247
}
228-
for _, cr := range doc.Components.Callbacks {
229-
clearCallbackRefs(cr, stack, visited)
248+
249+
ptr := v.Pointer()
250+
if visited[ptr] {
251+
return
230252
}
231-
for _, er := range doc.Components.Examples {
232-
if er != nil {
233-
er.Ref = ""
253+
visited[ptr] = true
254+
255+
elem := v.Elem()
256+
if elem.Kind() == reflect.Struct {
257+
if refField := elem.FieldByName("Ref"); refField.IsValid() && refField.Kind() == reflect.String {
258+
refField.SetString("")
234259
}
235260
}
236-
for _, lr := range doc.Components.Links {
237-
if lr != nil {
238-
lr.Ref = ""
261+
walkAndClearRefs(elem, visited, schemaStack)
262+
263+
case reflect.Struct:
264+
// Handle types with unexported map fields (Paths, Callback, Responses)
265+
// accessed via a Map() method.
266+
if v.CanAddr() {
267+
if mapMethod := v.Addr().MethodByName("Map"); mapMethod.IsValid() {
268+
results := mapMethod.Call(nil)
269+
if len(results) == 1 && results[0].Kind() == reflect.Map {
270+
walkAndClearRefs(results[0], visited, schemaStack)
271+
}
239272
}
240273
}
241-
for _, ssr := range doc.Components.SecuritySchemes {
242-
if ssr != nil {
243-
ssr.Ref = ""
274+
for i := 0; i < v.NumField(); i++ {
275+
field := v.Field(i)
276+
if field.CanInterface() {
277+
walkAndClearRefs(field, visited, schemaStack)
244278
}
245279
}
246-
}
247280

248-
if doc.Paths != nil {
249-
for _, pathItem := range doc.Paths.Map() {
250-
clearPathItemRefs(pathItem, stack, visited)
281+
case reflect.Map:
282+
for _, key := range v.MapKeys() {
283+
walkAndClearRefs(v.MapIndex(key), visited, schemaStack)
251284
}
252-
}
253-
}
254285

255-
func clearContentRefs(content openapi3.Content, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
256-
for _, mt := range content {
257-
if mt != nil {
258-
clearSchemaRefs(mt.Schema, stack)
286+
case reflect.Slice:
287+
for i := 0; i < v.Len(); i++ {
288+
walkAndClearRefs(v.Index(i), visited, schemaStack)
259289
}
260-
}
261-
}
262-
263-
func clearParameterRefs(pr *openapi3.ParameterRef, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
264-
if pr == nil {
265-
return
266-
}
267-
pr.Ref = ""
268-
if pr.Value != nil {
269-
clearSchemaRefs(pr.Value.Schema, stack)
270-
clearContentRefs(pr.Value.Content, stack, visited)
271-
}
272-
}
273-
274-
func clearHeaderRefs(hr *openapi3.HeaderRef, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
275-
if hr == nil {
276-
return
277-
}
278-
hr.Ref = ""
279-
if hr.Value != nil {
280-
clearSchemaRefs(hr.Value.Schema, stack)
281-
clearContentRefs(hr.Value.Content, stack, visited)
282-
}
283-
}
284290

285-
func clearRequestBodyRefs(rb *openapi3.RequestBodyRef, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
286-
if rb == nil {
287-
return
288-
}
289-
rb.Ref = ""
290-
if rb.Value != nil {
291-
clearContentRefs(rb.Value.Content, stack, visited)
292-
}
293-
}
294-
295-
func clearResponseRefs(rr *openapi3.ResponseRef, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
296-
if rr == nil {
297-
return
298-
}
299-
rr.Ref = ""
300-
if rr.Value != nil {
301-
clearContentRefs(rr.Value.Content, stack, visited)
302-
for _, hr := range rr.Value.Headers {
303-
clearHeaderRefs(hr, stack, visited)
291+
case reflect.Interface:
292+
if !v.IsNil() {
293+
walkAndClearRefs(v.Elem(), visited, schemaStack)
304294
}
305295
}
306296
}
307-
308-
func clearCallbackRefs(cr *openapi3.CallbackRef, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
309-
if cr == nil {
310-
return
311-
}
312-
cr.Ref = ""
313-
if cr.Value != nil {
314-
for _, pathItem := range cr.Value.Map() {
315-
clearPathItemRefs(pathItem, stack, visited)
316-
}
317-
}
318-
}
319-
320-
func clearPathItemRefs(pathItem *openapi3.PathItem, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
321-
if pathItem == nil || visited[pathItem] {
322-
return
323-
}
324-
visited[pathItem] = true
325-
for _, pr := range pathItem.Parameters {
326-
clearParameterRefs(pr, stack, visited)
327-
}
328-
for _, op := range pathItem.Operations() {
329-
clearOperationRefs(op, stack, visited)
330-
}
331-
}
332-
333-
func clearOperationRefs(op *openapi3.Operation, stack map[*openapi3.Schema]bool, visited map[*openapi3.PathItem]bool) {
334-
if op == nil {
335-
return
336-
}
337-
for _, pr := range op.Parameters {
338-
clearParameterRefs(pr, stack, visited)
339-
}
340-
clearRequestBodyRefs(op.RequestBody, stack, visited)
341-
if op.Responses != nil {
342-
for _, rr := range op.Responses.Map() {
343-
clearResponseRefs(rr, stack, visited)
344-
}
345-
}
346-
for _, cr := range op.Callbacks {
347-
clearCallbackRefs(cr, stack, visited)
348-
}
349-
}
350-
351-
// clearSchemaRefs recursively clears $ref strings on all nested SchemaRefs
352-
// so that json.Marshal outputs fully inlined schemas. The stack set tracks
353-
// Schema values (not SchemaRef pointers) on the current recursion path to
354-
// detect circular references. kin-openapi resolves $refs by creating
355-
// different SchemaRef objects that share the same underlying Schema pointer,
356-
// so tracking by *Schema is necessary to catch all cycles.
357-
func clearSchemaRefs(sr *openapi3.SchemaRef, stack map[*openapi3.Schema]bool) {
358-
if sr == nil {
359-
return
360-
}
361-
sr.Ref = ""
362-
s := sr.Value
363-
if s == nil {
364-
return
365-
}
366-
if stack[s] {
367-
sr.Value = &openapi3.Schema{}
368-
return
369-
}
370-
stack[s] = true
371-
for _, child := range s.AllOf {
372-
clearSchemaRefs(child, stack)
373-
}
374-
for _, child := range s.AnyOf {
375-
clearSchemaRefs(child, stack)
376-
}
377-
for _, child := range s.OneOf {
378-
clearSchemaRefs(child, stack)
379-
}
380-
clearSchemaRefs(s.Not, stack)
381-
if s.Items != nil {
382-
clearSchemaRefs(s.Items, stack)
383-
}
384-
for _, prop := range s.Properties {
385-
clearSchemaRefs(prop, stack)
386-
}
387-
if s.AdditionalProperties.Schema != nil {
388-
clearSchemaRefs(s.AdditionalProperties.Schema, stack)
389-
}
390-
delete(stack, s)
391-
}

utils/component/openapi_generator_test.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package component
22

33
import (
44
"encoding/json"
5+
"reflect"
56
"strings"
67
"testing"
78

@@ -305,7 +306,7 @@ func TestGetResolvedManifest_AllOf(t *testing.T) {
305306
}
306307
}
307308

308-
func TestClearSchemaRefs(t *testing.T) {
309+
func TestWalkAndClearRefs(t *testing.T) {
309310
tests := []struct {
310311
name string
311312
sr *openapi3.SchemaRef
@@ -322,24 +323,26 @@ func TestClearSchemaRefs(t *testing.T) {
322323

323324
for _, tt := range tests {
324325
t.Run(tt.name, func(t *testing.T) {
325-
visited := make(map[*openapi3.Schema]bool)
326-
clearSchemaRefs(tt.sr, visited)
326+
visited := make(map[uintptr]bool)
327+
schemaStack := make(map[uintptr]bool)
328+
walkAndClearRefs(reflect.ValueOf(tt.sr), visited, schemaStack)
327329
if tt.sr != nil && tt.sr.Ref != "" {
328330
t.Errorf("Ref = %q, want empty", tt.sr.Ref)
329331
}
330332
})
331333
}
332334
}
333335

334-
func TestClearSchemaRefs_Circular(t *testing.T) {
336+
func TestWalkAndClearRefs_Circular(t *testing.T) {
335337
// Build a circular reference: A -> B -> A
336338
a := &openapi3.SchemaRef{Ref: "#/components/schemas/A", Value: &openapi3.Schema{}}
337339
b := &openapi3.SchemaRef{Ref: "#/components/schemas/B", Value: &openapi3.Schema{}}
338340
a.Value.Properties = openapi3.Schemas{"b": b}
339341
b.Value.Properties = openapi3.Schemas{"a": a}
340342

341-
stack := make(map[*openapi3.Schema]bool)
342-
clearSchemaRefs(a, stack) // must not hang or panic
343+
visited := make(map[uintptr]bool)
344+
schemaStack := make(map[uintptr]bool)
345+
walkAndClearRefs(reflect.ValueOf(a), visited, schemaStack) // must not hang or panic
343346

344347
if a.Ref != "" {
345348
t.Errorf("a.Ref = %q, want empty", a.Ref)
@@ -354,15 +357,16 @@ func TestClearSchemaRefs_Circular(t *testing.T) {
354357
}
355358
}
356359

357-
func TestClearSchemaRefs_SelfReference(t *testing.T) {
360+
func TestWalkAndClearRefs_SelfReference(t *testing.T) {
358361
// Schema that references itself (like JSONSchemaProps).
359362
self := &openapi3.SchemaRef{Value: &openapi3.Schema{
360363
Type: &openapi3.Types{"object"},
361364
}}
362365
self.Value.Properties = openapi3.Schemas{"nested": self}
363366

364-
stack := make(map[*openapi3.Schema]bool)
365-
clearSchemaRefs(self, stack)
367+
visited := make(map[uintptr]bool)
368+
schemaStack := make(map[uintptr]bool)
369+
walkAndClearRefs(reflect.ValueOf(self), visited, schemaStack)
366370

367371
// The self-referencing property should be replaced, breaking the cycle.
368372
if _, err := json.Marshal(self); err != nil {

0 commit comments

Comments
 (0)