Skip to content

Commit e7d0a7e

Browse files
committed
feat: clear $ref fields across entire OpenAPI document
1 parent d1a2555 commit e7d0a7e

File tree

2 files changed

+309
-4
lines changed

2 files changed

+309
-4
lines changed

utils/component/openapi_generator.go

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,17 +196,143 @@ func getResolvedManifest(manifest string) (string, error) {
196196
if doc.Components == nil || len(doc.Components.Schemas) == 0 {
197197
return "", ErrNoSchemasFound
198198
}
199-
stack := make(map[*openapi3.Schema]bool)
200-
for _, schemaRef := range doc.Components.Schemas {
201-
clearSchemaRefs(schemaRef, stack)
202-
}
199+
clearDocRefs(doc)
203200
resolved, err := json.Marshal(doc)
204201
if err != nil {
205202
return "", err
206203
}
207204
return string(resolved), nil
208205
}
209206

207+
// clearDocRefs clears $ref strings across the entire OpenAPI document,
208+
// including paths, components/parameters, components/headers,
209+
// components/requestBodies, components/responses, and components/callbacks.
210+
func clearDocRefs(doc *openapi3.T) {
211+
stack := make(map[*openapi3.Schema]bool)
212+
213+
if doc.Components != nil {
214+
for _, sr := range doc.Components.Schemas {
215+
clearSchemaRefs(sr, stack)
216+
}
217+
for _, pr := range doc.Components.Parameters {
218+
clearParameterRefs(pr, stack)
219+
}
220+
for _, hr := range doc.Components.Headers {
221+
clearHeaderRefs(hr, stack)
222+
}
223+
for _, rb := range doc.Components.RequestBodies {
224+
clearRequestBodyRefs(rb, stack)
225+
}
226+
for _, rr := range doc.Components.Responses {
227+
clearResponseRefs(rr, stack)
228+
}
229+
for _, cr := range doc.Components.Callbacks {
230+
clearCallbackRefs(cr, stack)
231+
}
232+
}
233+
234+
if doc.Paths != nil {
235+
for _, pathItem := range doc.Paths.Map() {
236+
clearPathItemRefs(pathItem, stack)
237+
}
238+
}
239+
}
240+
241+
func clearContentRefs(content openapi3.Content, stack map[*openapi3.Schema]bool) {
242+
for _, mt := range content {
243+
if mt != nil {
244+
clearSchemaRefs(mt.Schema, stack)
245+
}
246+
}
247+
}
248+
249+
func clearParameterRefs(pr *openapi3.ParameterRef, stack map[*openapi3.Schema]bool) {
250+
if pr == nil {
251+
return
252+
}
253+
pr.Ref = ""
254+
if pr.Value != nil {
255+
clearSchemaRefs(pr.Value.Schema, stack)
256+
clearContentRefs(pr.Value.Content, stack)
257+
}
258+
}
259+
260+
func clearHeaderRefs(hr *openapi3.HeaderRef, stack map[*openapi3.Schema]bool) {
261+
if hr == nil {
262+
return
263+
}
264+
hr.Ref = ""
265+
if hr.Value != nil {
266+
clearSchemaRefs(hr.Value.Schema, stack)
267+
clearContentRefs(hr.Value.Content, stack)
268+
}
269+
}
270+
271+
func clearRequestBodyRefs(rb *openapi3.RequestBodyRef, stack map[*openapi3.Schema]bool) {
272+
if rb == nil {
273+
return
274+
}
275+
rb.Ref = ""
276+
if rb.Value != nil {
277+
clearContentRefs(rb.Value.Content, stack)
278+
}
279+
}
280+
281+
func clearResponseRefs(rr *openapi3.ResponseRef, stack map[*openapi3.Schema]bool) {
282+
if rr == nil {
283+
return
284+
}
285+
rr.Ref = ""
286+
if rr.Value != nil {
287+
clearContentRefs(rr.Value.Content, stack)
288+
for _, hr := range rr.Value.Headers {
289+
clearHeaderRefs(hr, stack)
290+
}
291+
}
292+
}
293+
294+
func clearCallbackRefs(cr *openapi3.CallbackRef, stack map[*openapi3.Schema]bool) {
295+
if cr == nil {
296+
return
297+
}
298+
cr.Ref = ""
299+
if cr.Value != nil {
300+
for _, pathItem := range cr.Value.Map() {
301+
clearPathItemRefs(pathItem, stack)
302+
}
303+
}
304+
}
305+
306+
func clearPathItemRefs(pathItem *openapi3.PathItem, stack map[*openapi3.Schema]bool) {
307+
if pathItem == nil {
308+
return
309+
}
310+
for _, pr := range pathItem.Parameters {
311+
clearParameterRefs(pr, stack)
312+
}
313+
for _, op := range pathItem.Operations() {
314+
clearOperationRefs(op, stack)
315+
}
316+
}
317+
318+
func clearOperationRefs(op *openapi3.Operation, stack map[*openapi3.Schema]bool) {
319+
if op == nil {
320+
return
321+
}
322+
for _, pr := range op.Parameters {
323+
clearParameterRefs(pr, stack)
324+
}
325+
clearRequestBodyRefs(op.RequestBody, stack)
326+
if op.Responses != nil {
327+
for _, rr := range op.Responses.Map() {
328+
clearResponseRefs(rr, stack)
329+
}
330+
}
331+
for _, cr := range op.Callbacks {
332+
clearCallbackRefs(cr, stack)
333+
}
334+
}
335+
210336
// clearSchemaRefs recursively clears $ref strings on all nested SchemaRefs
211337
// so that json.Marshal outputs fully inlined schemas. The stack set tracks
212338
// Schema values (not SchemaRef pointers) on the current recursion path to

utils/component/openapi_generator_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,185 @@ func TestClearSchemaRefs_SelfReference(t *testing.T) {
370370
}
371371
}
372372

373+
func TestGetResolvedManifest_PathsAndComponents(t *testing.T) {
374+
input := `{
375+
"openapi": "3.0.0",
376+
"info": {"title": "test", "version": "1.0"},
377+
"paths": {
378+
"/pets": {
379+
"get": {
380+
"parameters": [
381+
{"$ref": "#/components/parameters/LimitParam"}
382+
],
383+
"responses": {
384+
"200": {
385+
"$ref": "#/components/responses/PetList"
386+
}
387+
}
388+
},
389+
"post": {
390+
"requestBody": {
391+
"$ref": "#/components/requestBodies/PetBody"
392+
},
393+
"responses": {
394+
"201": {
395+
"description": "created"
396+
}
397+
}
398+
}
399+
}
400+
},
401+
"components": {
402+
"schemas": {
403+
"Pet": {
404+
"type": "object",
405+
"properties": {
406+
"name": {"type": "string"}
407+
}
408+
}
409+
},
410+
"parameters": {
411+
"LimitParam": {
412+
"name": "limit",
413+
"in": "query",
414+
"schema": {"type": "integer"}
415+
}
416+
},
417+
"requestBodies": {
418+
"PetBody": {
419+
"content": {
420+
"application/json": {
421+
"schema": {"$ref": "#/components/schemas/Pet"}
422+
}
423+
}
424+
}
425+
},
426+
"responses": {
427+
"PetList": {
428+
"description": "A list of pets",
429+
"content": {
430+
"application/json": {
431+
"schema": {
432+
"type": "array",
433+
"items": {"$ref": "#/components/schemas/Pet"}
434+
}
435+
}
436+
}
437+
}
438+
}
439+
}
440+
}`
441+
442+
out, err := getResolvedManifest(input)
443+
if err != nil {
444+
t.Fatalf("unexpected error: %v", err)
445+
}
446+
447+
var parsed map[string]any
448+
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
449+
t.Fatalf("output is not valid JSON: %v", err)
450+
}
451+
452+
// Verify parameter ref in path is resolved.
453+
param := navigatePath(t, parsed, "paths./pets.get.parameters")
454+
params, ok := param.([]any)
455+
if !ok || len(params) == 0 {
456+
t.Fatal("expected parameters array")
457+
}
458+
pm := params[0].(map[string]any)
459+
if _, hasRef := pm["$ref"]; hasRef {
460+
t.Error("parameter $ref still present in path")
461+
}
462+
if pm["name"] != "limit" {
463+
t.Errorf("parameter name = %v, want limit", pm["name"])
464+
}
465+
466+
// Verify response ref in path is resolved.
467+
resp := navigatePath(t, parsed, "paths./pets.get.responses.200").(map[string]any)
468+
if _, hasRef := resp["$ref"]; hasRef {
469+
t.Error("response $ref still present in path")
470+
}
471+
if resp["description"] != "A list of pets" {
472+
t.Errorf("response description = %v, want 'A list of pets'", resp["description"])
473+
}
474+
475+
// Verify schema ref inside response content is resolved.
476+
items := navigatePath(t, parsed, "paths./pets.get.responses.200.content.application/json.schema.items").(map[string]any)
477+
if _, hasRef := items["$ref"]; hasRef {
478+
t.Error("schema $ref in response items still present")
479+
}
480+
if items["type"] != "object" {
481+
t.Errorf("items type = %v, want object", items["type"])
482+
}
483+
484+
// Verify requestBody ref in path is resolved.
485+
rb := navigatePath(t, parsed, "paths./pets.post.requestBody").(map[string]any)
486+
if _, hasRef := rb["$ref"]; hasRef {
487+
t.Error("requestBody $ref still present in path")
488+
}
489+
490+
// Verify schema ref inside requestBody content is resolved.
491+
rbSchema := navigatePath(t, parsed, "paths./pets.post.requestBody.content.application/json.schema").(map[string]any)
492+
if _, hasRef := rbSchema["$ref"]; hasRef {
493+
t.Error("schema $ref in requestBody content still present")
494+
}
495+
if rbSchema["type"] != "object" {
496+
t.Errorf("requestBody schema type = %v, want object", rbSchema["type"])
497+
}
498+
}
499+
500+
func TestGetResolvedManifest_HeaderRefs(t *testing.T) {
501+
input := `{
502+
"openapi": "3.0.0",
503+
"info": {"title": "test", "version": "1.0"},
504+
"paths": {
505+
"/items": {
506+
"get": {
507+
"responses": {
508+
"200": {
509+
"description": "OK",
510+
"headers": {
511+
"X-Rate-Limit": {
512+
"$ref": "#/components/headers/RateLimit"
513+
}
514+
}
515+
}
516+
}
517+
}
518+
}
519+
},
520+
"components": {
521+
"schemas": {
522+
"Placeholder": {"type": "string"}
523+
},
524+
"headers": {
525+
"RateLimit": {
526+
"schema": {"type": "integer"}
527+
}
528+
}
529+
}
530+
}`
531+
532+
out, err := getResolvedManifest(input)
533+
if err != nil {
534+
t.Fatalf("unexpected error: %v", err)
535+
}
536+
537+
var parsed map[string]any
538+
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
539+
t.Fatalf("output is not valid JSON: %v", err)
540+
}
541+
542+
header := navigatePath(t, parsed, "paths./items.get.responses.200.headers.X-Rate-Limit").(map[string]any)
543+
if _, hasRef := header["$ref"]; hasRef {
544+
t.Error("header $ref still present")
545+
}
546+
schema := header["schema"].(map[string]any)
547+
if schema["type"] != "integer" {
548+
t.Errorf("header schema type = %v, want integer", schema["type"])
549+
}
550+
}
551+
373552
// navigatePath walks a dot-separated path through nested maps.
374553
func navigatePath(t *testing.T, data map[string]any, path string) any {
375554
t.Helper()

0 commit comments

Comments
 (0)