-
-
Notifications
You must be signed in to change notification settings - Fork 98
Add underOpenAPIExamplePath function to filter schema IDs in examples #549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
195dcb0
8ad1353
47d4446
1e586fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,6 @@ import ( | |
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| "slices" | ||
| "sort" | ||
| "strconv" | ||
| "strings" | ||
|
|
@@ -57,6 +56,20 @@ func isArrayOfSchemaContainingNode(v string) bool { | |
| return false | ||
| } | ||
|
|
||
| // underOpenAPIExamplePath reports whether seenPath is under an OpenAPI example or examples | ||
| // keyword (sample data, not schema). A segment named "example" or "examples" that is preceded | ||
| // by "properties" or "patternProperties" is a schema property name, not an OpenAPI keyword. | ||
| func underOpenAPIExamplePath(seenPath []string) bool { | ||
| for i, p := range seenPath { | ||
| if p == "example" || p == "examples" { | ||
| if i == 0 || (seenPath[i-1] != "properties" && seenPath[i-1] != "patternProperties") { | ||
| return true | ||
| } | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // ExtractRefs will return a deduplicated slice of references for every unique ref found in the document. | ||
| // The total number of refs, will generally be much higher, you can extract those from GetRawReferenceCount() | ||
| func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node, seenPath []string, level int, poly bool, pName string) []*Reference { | ||
|
|
@@ -77,7 +90,7 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node | |
|
|
||
| // Check if THIS node has a $id and update scope for processing children | ||
| // This must happen before iterating children so they see the updated scope | ||
| if node.Kind == yaml.MappingNode { | ||
| if node.Kind == yaml.MappingNode && !underOpenAPIExamplePath(seenPath) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skipping the node-level |
||
| if nodeId := FindSchemaIdInNode(node); nodeId != "" { | ||
| resolvedNodeId, _ := ResolveSchemaId(nodeId, parentBaseUri) | ||
| if resolvedNodeId == "" { | ||
|
|
@@ -170,11 +183,13 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node | |
| if len(seenPath) > 0 { | ||
| skip := false | ||
|
|
||
| // iterate through the path and look for an item named 'examples' or 'example' | ||
| for _, p := range seenPath { | ||
| // iterate through the path and look for an OpenAPI example/examples keyword or extension | ||
| for j, p := range seenPath { | ||
| if p == "examples" || p == "example" { | ||
| skip = true | ||
| break | ||
| if j == 0 || (seenPath[j-1] != "properties" && seenPath[j-1] != "patternProperties") { | ||
| skip = true | ||
| break | ||
| } | ||
| } | ||
| // look for any extension in the path and ignore it | ||
| if strings.HasPrefix(p, "x-") { | ||
|
|
@@ -557,6 +572,9 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node | |
|
|
||
| // Detect and register JSON Schema 2020-12 $id declarations | ||
| if i%2 == 0 && n.Value == "$id" { | ||
| if underOpenAPIExamplePath(seenPath) { | ||
| continue | ||
| } | ||
| if len(node.Content) > i+1 && utils.IsNodeStringValue(node.Content[i+1]) { | ||
| idValue := node.Content[i+1].Value | ||
| idNode := node.Content[i+1] | ||
|
|
@@ -661,7 +679,7 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node | |
| prev = n.Value | ||
| continue | ||
| } | ||
| if !slices.Contains(seenPath, "example") && !slices.Contains(seenPath, "examples") { | ||
| if !underOpenAPIExamplePath(seenPath) { | ||
| ref := &DescriptionReference{ | ||
| ParentNode: parent, | ||
| Content: node.Content[i+1].Value, | ||
|
|
@@ -690,7 +708,7 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node | |
| continue | ||
| } | ||
|
|
||
| if slices.Contains(seenPath, "example") || slices.Contains(seenPath, "examples") { | ||
| if underOpenAPIExamplePath(seenPath) { | ||
| continue | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -750,6 +750,224 @@ components: | |
| assert.Contains(t, petEntry.DefinitionPath, "Pet") | ||
| } | ||
|
|
||
| func TestSchemaId_IgnoredUnderExampleAndExamples(t *testing.T) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests only cover |
||
| t.Run("example_payload", func(t *testing.T) { | ||
| spec := `openapi: "3.1.0" | ||
| info: | ||
| title: Test API | ||
| version: 1.0.0 | ||
| paths: | ||
| /pets: | ||
| get: | ||
| responses: | ||
| "200": | ||
| description: ok | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $id: "https://example.com/schemas/pet.json" | ||
| type: object | ||
| properties: | ||
| id: | ||
| type: string | ||
| example: | ||
| $id: "https://example.com/should-not-register" | ||
| id: "1" | ||
| ` | ||
| var rootNode yaml.Node | ||
| err := yaml.Unmarshal([]byte(spec), &rootNode) | ||
| assert.NoError(t, err) | ||
|
|
||
| config := CreateClosedAPIIndexConfig() | ||
| config.SpecAbsolutePath = "https://example.com/openapi.yaml" | ||
| index := NewSpecIndexWithConfig(&rootNode, config) | ||
| assert.NotNil(t, index) | ||
|
|
||
| allIds := index.GetAllSchemaIds() | ||
| assert.Len(t, allIds, 1) | ||
| assert.NotNil(t, allIds["https://example.com/schemas/pet.json"]) | ||
| assert.Nil(t, allIds["https://example.com/should-not-register"]) | ||
| }) | ||
|
|
||
| t.Run("examples_named_value", func(t *testing.T) { | ||
| spec := `openapi: "3.1.0" | ||
| info: | ||
| title: Test API | ||
| version: 1.0.0 | ||
| paths: | ||
| /widgets: | ||
| get: | ||
| responses: | ||
| "200": | ||
| description: ok | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $id: "https://example.com/schemas/widget.json" | ||
| type: object | ||
| examples: | ||
| sample: | ||
| value: | ||
| $id: "https://example.com/fake-from-examples" | ||
| foo: bar | ||
| ` | ||
| var rootNode yaml.Node | ||
| err := yaml.Unmarshal([]byte(spec), &rootNode) | ||
| assert.NoError(t, err) | ||
|
|
||
| config := CreateClosedAPIIndexConfig() | ||
| config.SpecAbsolutePath = "https://example.com/openapi.yaml" | ||
| index := NewSpecIndexWithConfig(&rootNode, config) | ||
| assert.NotNil(t, index) | ||
|
|
||
| allIds := index.GetAllSchemaIds() | ||
| assert.Len(t, allIds, 1) | ||
| assert.NotNil(t, allIds["https://example.com/schemas/widget.json"]) | ||
| assert.Nil(t, allIds["https://example.com/fake-from-examples"]) | ||
| }) | ||
|
|
||
| t.Run("invalid_id_in_example_no_index_error", func(t *testing.T) { | ||
| spec := `openapi: "3.1.0" | ||
| info: | ||
| title: Test API | ||
| version: 1.0.0 | ||
| paths: | ||
| /x: | ||
| get: | ||
| responses: | ||
| "200": | ||
| description: ok | ||
| content: | ||
| application/json: | ||
| schema: | ||
| type: object | ||
| example: | ||
| $id: "https://bad.com/schema#fragment" | ||
| k: v | ||
| ` | ||
| var rootNode yaml.Node | ||
| err := yaml.Unmarshal([]byte(spec), &rootNode) | ||
| assert.NoError(t, err) | ||
|
|
||
| config := CreateClosedAPIIndexConfig() | ||
| config.SpecAbsolutePath = "https://example.com/openapi.yaml" | ||
| index := NewSpecIndexWithConfig(&rootNode, config) | ||
| assert.NotNil(t, index) | ||
|
|
||
| assert.Len(t, index.GetAllSchemaIds(), 0) | ||
| for _, e := range index.GetReferenceIndexErrors() { | ||
| assert.False(t, strings.Contains(e.Error(), "invalid $id"), | ||
| "$id inside example must not be validated as schema $id: %v", e) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| func TestSchemaId_NotIgnoredUnderPropertiesExample(t *testing.T) { | ||
| t.Run("property_named_example", func(t *testing.T) { | ||
| spec := `openapi: "3.1.0" | ||
| info: | ||
| title: Test API | ||
| version: 1.0.0 | ||
| components: | ||
| schemas: | ||
| MySchema: | ||
| $id: "https://example.com/schemas/myschema.json" | ||
| type: object | ||
| properties: | ||
| example: | ||
| $id: "https://example.com/schemas/example-prop.json" | ||
| type: object | ||
| properties: | ||
| id: | ||
| type: string | ||
| ` | ||
| var rootNode yaml.Node | ||
| err := yaml.Unmarshal([]byte(spec), &rootNode) | ||
| assert.NoError(t, err) | ||
|
|
||
| config := CreateClosedAPIIndexConfig() | ||
| config.SpecAbsolutePath = "https://example.com/openapi.yaml" | ||
| index := NewSpecIndexWithConfig(&rootNode, config) | ||
| assert.NotNil(t, index) | ||
|
|
||
| allIds := index.GetAllSchemaIds() | ||
| assert.Len(t, allIds, 2) | ||
| assert.NotNil(t, allIds["https://example.com/schemas/myschema.json"]) | ||
| assert.NotNil(t, allIds["https://example.com/schemas/example-prop.json"]) | ||
| }) | ||
|
|
||
| t.Run("property_named_examples", func(t *testing.T) { | ||
| spec := `openapi: "3.1.0" | ||
| info: | ||
| title: Test API | ||
| version: 1.0.0 | ||
| components: | ||
| schemas: | ||
| MySchema: | ||
| type: object | ||
| properties: | ||
| examples: | ||
| $id: "https://example.com/schemas/examples-prop.json" | ||
| type: object | ||
| properties: | ||
| list: | ||
| type: array | ||
| ` | ||
| var rootNode yaml.Node | ||
| err := yaml.Unmarshal([]byte(spec), &rootNode) | ||
| assert.NoError(t, err) | ||
|
|
||
| config := CreateClosedAPIIndexConfig() | ||
| config.SpecAbsolutePath = "https://example.com/openapi.yaml" | ||
| index := NewSpecIndexWithConfig(&rootNode, config) | ||
| assert.NotNil(t, index) | ||
|
|
||
| allIds := index.GetAllSchemaIds() | ||
| assert.Len(t, allIds, 1) | ||
| assert.NotNil(t, allIds["https://example.com/schemas/examples-prop.json"]) | ||
| }) | ||
|
|
||
| t.Run("real_example_still_ignored", func(t *testing.T) { | ||
| spec := `openapi: "3.1.0" | ||
| info: | ||
| title: Test API | ||
| version: 1.0.0 | ||
| paths: | ||
| /pets: | ||
| get: | ||
| responses: | ||
| "200": | ||
| description: ok | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $id: "https://example.com/schemas/pet.json" | ||
| type: object | ||
| properties: | ||
| example: | ||
| $id: "https://example.com/schemas/example-prop.json" | ||
| type: string | ||
| example: | ||
| $id: "https://example.com/should-not-register" | ||
| id: "1" | ||
| ` | ||
| var rootNode yaml.Node | ||
| err := yaml.Unmarshal([]byte(spec), &rootNode) | ||
| assert.NoError(t, err) | ||
|
|
||
| config := CreateClosedAPIIndexConfig() | ||
| config.SpecAbsolutePath = "https://example.com/openapi.yaml" | ||
| index := NewSpecIndexWithConfig(&rootNode, config) | ||
| assert.NotNil(t, index) | ||
|
|
||
| allIds := index.GetAllSchemaIds() | ||
| assert.Len(t, allIds, 2) | ||
| assert.NotNil(t, allIds["https://example.com/schemas/pet.json"]) | ||
| assert.NotNil(t, allIds["https://example.com/schemas/example-prop.json"]) | ||
| assert.Nil(t, allIds["https://example.com/should-not-register"]) | ||
| }) | ||
| } | ||
|
|
||
| func TestSchemaId_ExtractionWithInvalidId(t *testing.T) { | ||
| // OpenAPI 3.1 spec with invalid $id (contains fragment) | ||
| spec := `openapi: "3.1.0" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This helper is too broad.
seenPathincludes ordinary schema property names, so treating any segment equal toexampleorexamplesas an OpenAPI sample payload will incorrectly suppress valid$idextraction for schemas under properties with those names.