Skip to content

Commit c579fac

Browse files
committed
Resolved #503
components with ‘x-‘ prefix are now supported.
1 parent c9ce5d7 commit c579fac

File tree

3 files changed

+309
-8
lines changed

3 files changed

+309
-8
lines changed

datamodel/high/base/example.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type Example struct {
3333
Description string `json:"description,omitempty" yaml:"description,omitempty"`
3434
Value *yaml.Node `json:"value,omitempty" yaml:"value,omitempty"`
3535
ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"`
36-
DataValue *yaml.Node `json:"dataValue,omitempty" yaml:"dataValue,omitempty"` // OpenAPI 3.2+ dataValue field
36+
DataValue *yaml.Node `json:"dataValue,omitempty" yaml:"dataValue,omitempty"` // OpenAPI 3.2+ dataValue field
3737
SerializedValue string `json:"serializedValue,omitempty" yaml:"serializedValue,omitempty"` // OpenAPI 3.2+ serializedValue field
3838
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
3939
low *lowBase.Example
@@ -105,9 +105,9 @@ func (e *Example) MarshalYAMLInline() (interface{}, error) {
105105
// resolve external reference if present
106106
if e.low != nil {
107107
// buildLowExample never returns an error, so we can ignore it
108-
rendered, _ := high.RenderExternalRef(e.low, buildLowExample, NewExample)
109-
if rendered != nil {
110-
return rendered, nil
108+
rendered, err := high.RenderExternalRef(e.low, buildLowExample, NewExample)
109+
if rendered != nil || err != nil {
110+
return rendered, err
111111
}
112112
}
113113

datamodel/low/v3/components.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,6 @@ func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, labe
299299
currentLabel = node
300300
continue
301301
}
302-
// only check for lowercase extensions as 'X-' is still valid as a key (annoyingly).
303-
if strings.HasPrefix(currentLabel.Value, "x-") {
304-
continue
305-
}
306302

307303
select {
308304
case in <- componentInput{

datamodel/low/v3/components_test.go

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,308 @@ func TestComponents_MediaTypes(t *testing.T) {
427427
hash2 := n.Hash()
428428
assert.NotEqual(t, hash1, hash2)
429429
}
430+
431+
// TestComponents_XPrefixedComponentNames tests that component names starting with x- are correctly
432+
// parsed as components and not incorrectly filtered out as extensions.
433+
// This is a regression test for https://github.com/pb33f/libopenapi/issues/503
434+
func TestComponents_XPrefixedComponentNames(t *testing.T) {
435+
low.ClearHashCache()
436+
437+
yml := `schemas:
438+
x-custom-schema:
439+
type: object
440+
description: A schema with x- prefix
441+
RegularSchema:
442+
type: string
443+
parameters:
444+
x-custom-param:
445+
name: x-custom-param
446+
in: header
447+
schema:
448+
type: string
449+
regular-param:
450+
name: regular-param
451+
in: query
452+
schema:
453+
type: string
454+
responses:
455+
x-custom-response:
456+
description: A response with x- prefix
457+
headers:
458+
x-rate-limit:
459+
schema:
460+
type: integer
461+
description: Rate limit header
462+
examples:
463+
x-custom-example:
464+
value: example-value
465+
description: An example with x- prefix
466+
requestBodies:
467+
x-custom-body:
468+
description: A request body with x- prefix
469+
content:
470+
application/json:
471+
schema:
472+
type: object
473+
securitySchemes:
474+
x-custom-auth:
475+
type: apiKey
476+
name: X-API-Key
477+
in: header
478+
description: Custom auth scheme
479+
links:
480+
x-custom-link:
481+
description: A link with x- prefix
482+
callbacks:
483+
x-custom-callback:
484+
'{$request.body#/callbackUrl}':
485+
post:
486+
description: Callback operation
487+
pathItems:
488+
/x-custom-path:
489+
get:
490+
description: A path item with x- prefix
491+
mediaTypes:
492+
x-custom-media:
493+
schema:
494+
type: object
495+
description: Custom media type`
496+
497+
var idxNode yaml.Node
498+
mErr := yaml.Unmarshal([]byte(yml), &idxNode)
499+
assert.NoError(t, mErr)
500+
idx := index.NewSpecIndex(&idxNode)
501+
502+
var n Components
503+
err := low.BuildModel(idxNode.Content[0], &n)
504+
assert.NoError(t, err)
505+
506+
err = n.Build(context.Background(), idxNode.Content[0], idx)
507+
assert.NoError(t, err)
508+
509+
// Test x-prefixed schemas are included
510+
xSchema := n.FindSchema("x-custom-schema")
511+
assert.NotNil(t, xSchema, "x-custom-schema should be found")
512+
assert.Equal(t, "A schema with x- prefix", xSchema.Value.Schema().Description.Value)
513+
514+
regularSchema := n.FindSchema("RegularSchema")
515+
assert.NotNil(t, regularSchema, "RegularSchema should also be found")
516+
517+
// Test x-prefixed parameters are included
518+
xParam := n.FindParameter("x-custom-param")
519+
assert.NotNil(t, xParam, "x-custom-param should be found")
520+
assert.Equal(t, "x-custom-param", xParam.Value.Name.Value)
521+
assert.Equal(t, "header", xParam.Value.In.Value)
522+
523+
regularParam := n.FindParameter("regular-param")
524+
assert.NotNil(t, regularParam, "regular-param should also be found")
525+
526+
// Test x-prefixed responses are included
527+
xResponse := n.FindResponse("x-custom-response")
528+
assert.NotNil(t, xResponse, "x-custom-response should be found")
529+
assert.Equal(t, "A response with x- prefix", xResponse.Value.Description.Value)
530+
531+
// Test x-prefixed headers are included
532+
xHeader := n.FindHeader("x-rate-limit")
533+
assert.NotNil(t, xHeader, "x-rate-limit should be found")
534+
assert.Equal(t, "Rate limit header", xHeader.Value.Description.Value)
535+
536+
// Test x-prefixed examples are included
537+
xExample := n.FindExample("x-custom-example")
538+
assert.NotNil(t, xExample, "x-custom-example should be found")
539+
assert.Equal(t, "An example with x- prefix", xExample.Value.Description.Value)
540+
541+
// Test x-prefixed request bodies are included
542+
xRequestBody := n.FindRequestBody("x-custom-body")
543+
assert.NotNil(t, xRequestBody, "x-custom-body should be found")
544+
assert.Equal(t, "A request body with x- prefix", xRequestBody.Value.Description.Value)
545+
546+
// Test x-prefixed security schemes are included
547+
xSecurityScheme := n.FindSecurityScheme("x-custom-auth")
548+
assert.NotNil(t, xSecurityScheme, "x-custom-auth should be found")
549+
assert.Equal(t, "Custom auth scheme", xSecurityScheme.Value.Description.Value)
550+
assert.Equal(t, "apiKey", xSecurityScheme.Value.Type.Value)
551+
552+
// Test x-prefixed links are included
553+
xLink := n.FindLink("x-custom-link")
554+
assert.NotNil(t, xLink, "x-custom-link should be found")
555+
assert.Equal(t, "A link with x- prefix", xLink.Value.Description.Value)
556+
557+
// Test x-prefixed callbacks are included
558+
xCallback := n.FindCallback("x-custom-callback")
559+
assert.NotNil(t, xCallback, "x-custom-callback should be found")
560+
expr := xCallback.Value.FindExpression("{$request.body#/callbackUrl}")
561+
assert.NotNil(t, expr, "Callback expression should be found")
562+
assert.Equal(t, "Callback operation", expr.Value.Post.Value.Description.Value)
563+
564+
// Test x-prefixed path items are included
565+
xPathItem := n.FindPathItem("/x-custom-path")
566+
assert.NotNil(t, xPathItem, "/x-custom-path should be found")
567+
assert.Equal(t, "A path item with x- prefix", xPathItem.Value.Get.Value.Description.Value)
568+
569+
// Test x-prefixed media types are included
570+
xMediaType := n.FindMediaType("x-custom-media")
571+
assert.NotNil(t, xMediaType, "x-custom-media should be found")
572+
assert.Equal(t, "Custom media type", xMediaType.Value.Schema.Value.Schema().Description.Value)
573+
}
574+
575+
// TestComponents_XPrefixedWithUpperCase tests that both x- (lowercase) and X- (uppercase)
576+
// prefixed component names are correctly parsed.
577+
func TestComponents_XPrefixedWithUpperCase(t *testing.T) {
578+
low.ClearHashCache()
579+
580+
yml := `schemas:
581+
x-lowercase-schema:
582+
type: string
583+
description: lowercase x- prefix
584+
X-UPPERCASE-SCHEMA:
585+
type: string
586+
description: uppercase X- prefix
587+
parameters:
588+
x-lowercase-param:
589+
name: x-param
590+
in: header
591+
schema:
592+
type: string
593+
X-UPPERCASE-PARAM:
594+
name: X-param
595+
in: header
596+
schema:
597+
type: string`
598+
599+
var idxNode yaml.Node
600+
mErr := yaml.Unmarshal([]byte(yml), &idxNode)
601+
assert.NoError(t, mErr)
602+
idx := index.NewSpecIndex(&idxNode)
603+
604+
var n Components
605+
err := low.BuildModel(idxNode.Content[0], &n)
606+
assert.NoError(t, err)
607+
608+
err = n.Build(context.Background(), idxNode.Content[0], idx)
609+
assert.NoError(t, err)
610+
611+
// Test lowercase x- prefix
612+
xLowerSchema := n.FindSchema("x-lowercase-schema")
613+
assert.NotNil(t, xLowerSchema, "x-lowercase-schema should be found")
614+
assert.Equal(t, "lowercase x- prefix", xLowerSchema.Value.Schema().Description.Value)
615+
616+
// Test uppercase X- prefix
617+
xUpperSchema := n.FindSchema("X-UPPERCASE-SCHEMA")
618+
assert.NotNil(t, xUpperSchema, "X-UPPERCASE-SCHEMA should be found")
619+
assert.Equal(t, "uppercase X- prefix", xUpperSchema.Value.Schema().Description.Value)
620+
621+
// Test lowercase x- param
622+
xLowerParam := n.FindParameter("x-lowercase-param")
623+
assert.NotNil(t, xLowerParam, "x-lowercase-param should be found")
624+
625+
// Test uppercase X- param
626+
xUpperParam := n.FindParameter("X-UPPERCASE-PARAM")
627+
assert.NotNil(t, xUpperParam, "X-UPPERCASE-PARAM should be found")
628+
}
629+
630+
// TestComponents_XPrefixedExtensionsStillWork verifies that extensions at the Components level
631+
// (like x-custom-extension) are still captured correctly, while x-prefixed component names
632+
// within schemas/parameters/etc are also captured.
633+
func TestComponents_XPrefixedExtensionsStillWork(t *testing.T) {
634+
low.ClearHashCache()
635+
636+
yml := `x-components-extension: this is an extension at components level
637+
x-another-extension:
638+
nested: value
639+
schemas:
640+
x-custom-schema:
641+
type: object
642+
description: This is a schema, not an extension
643+
RegularSchema:
644+
type: string`
645+
646+
var idxNode yaml.Node
647+
mErr := yaml.Unmarshal([]byte(yml), &idxNode)
648+
assert.NoError(t, mErr)
649+
idx := index.NewSpecIndex(&idxNode)
650+
651+
var n Components
652+
err := low.BuildModel(idxNode.Content[0], &n)
653+
assert.NoError(t, err)
654+
655+
err = n.Build(context.Background(), idxNode.Content[0], idx)
656+
assert.NoError(t, err)
657+
658+
// Extensions at Components level should still work
659+
ext1 := n.FindExtension("x-components-extension")
660+
assert.NotNil(t, ext1, "x-components-extension should be found as extension")
661+
var ext1Val string
662+
_ = ext1.Value.Decode(&ext1Val)
663+
assert.Equal(t, "this is an extension at components level", ext1Val)
664+
665+
ext2 := n.FindExtension("x-another-extension")
666+
assert.NotNil(t, ext2, "x-another-extension should be found as extension")
667+
668+
// x-prefixed schemas should be found as schemas
669+
xSchema := n.FindSchema("x-custom-schema")
670+
assert.NotNil(t, xSchema, "x-custom-schema should be found as a schema")
671+
assert.Equal(t, "This is a schema, not an extension", xSchema.Value.Schema().Description.Value)
672+
673+
// Regular schemas also work
674+
regularSchema := n.FindSchema("RegularSchema")
675+
assert.NotNil(t, regularSchema, "RegularSchema should be found")
676+
}
677+
678+
// TestComponents_XPrefixedReferenceResolution tests that references to x-prefixed components
679+
// resolve correctly.
680+
func TestComponents_XPrefixedReferenceResolution(t *testing.T) {
681+
low.ClearHashCache()
682+
683+
yml := `schemas:
684+
x-base-schema:
685+
type: object
686+
properties:
687+
id:
688+
type: integer
689+
derived-schema:
690+
allOf:
691+
- $ref: '#/schemas/x-base-schema'
692+
- type: object
693+
properties:
694+
name:
695+
type: string
696+
parameters:
697+
x-auth-header:
698+
name: Authorization
699+
in: header
700+
schema:
701+
type: string
702+
uses-x-param:
703+
$ref: '#/parameters/x-auth-header'`
704+
705+
var idxNode yaml.Node
706+
mErr := yaml.Unmarshal([]byte(yml), &idxNode)
707+
assert.NoError(t, mErr)
708+
idx := index.NewSpecIndex(&idxNode)
709+
710+
var n Components
711+
err := low.BuildModel(idxNode.Content[0], &n)
712+
assert.NoError(t, err)
713+
714+
err = n.Build(context.Background(), idxNode.Content[0], idx)
715+
assert.NoError(t, err)
716+
717+
// The x-prefixed schema should be found
718+
xBaseSchema := n.FindSchema("x-base-schema")
719+
assert.NotNil(t, xBaseSchema, "x-base-schema should be found")
720+
721+
// The derived schema should also be found
722+
derivedSchema := n.FindSchema("derived-schema")
723+
assert.NotNil(t, derivedSchema, "derived-schema should be found")
724+
725+
// The x-prefixed parameter should be found
726+
xAuthHeader := n.FindParameter("x-auth-header")
727+
assert.NotNil(t, xAuthHeader, "x-auth-header should be found")
728+
assert.Equal(t, "Authorization", xAuthHeader.Value.Name.Value)
729+
730+
// The parameter that references x-auth-header should have reference
731+
usesXParam := n.FindParameter("uses-x-param")
732+
assert.NotNil(t, usesXParam, "uses-x-param should be found")
733+
assert.Equal(t, "#/parameters/x-auth-header", usesXParam.Value.GetReference())
734+
}

0 commit comments

Comments
 (0)