Repository: ogen-go/ogen
Pull request #560 (2022-09-11 17:31:46)

fix(parser): validate component map key as spec says

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/560?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
Base: **72.59**% // Head: **72.66**% // Increases project coverage by **`+0.06%`** :tada:
> Coverage data is based on head [(`5620304`)](https://codecov.io/gh/ogen-go/ogen/pull/560?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) compared to base [(`428f6c0`)](https://codecov.io/gh/ogen-go/ogen/commit/428f6c043d77f52fd71a574c584283110e16dea7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> Patch coverage: 93.61% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #560      +/-   ##
==========================================
+ Coverage   72.59%   72.66%   +0.06%     
==========================================
  Files         157      157              
  Lines       11036    11082      +46     
==========================================
+ Hits         8012     8053      +41     
- Misses       2606     2609       +3     
- Partials      418      420       +2     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [openapi/parser/parse\_components.go](https://codecov.io/gh/ogen-go/ogen/pull/560/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VfY29tcG9uZW50cy5nbw==) | `85.98% <93.61%> (+5.65%)` | :arrow_up: |
| [gen/gen\_contents.go](https://codecov.io/gh/ogen-go/ogen/pull/560/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2dlbl9jb250ZW50cy5nbw==) | `72.10% <0.00%> (-0.69%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/560?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/openapi/parser/parse_components.go b/openapi/parser/parse_components.go
index 783514cf..a9728b8a 100644
--- a/openapi/parser/parse_components.go
+++ b/openapi/parser/parse_components.go
@@ -1,14 +1,78 @@
 package parser
 
 import (
+	"regexp"
+
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
 )
 
+var componentsKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9.\-_]+$`)
+
+// validateComponentsKey validates components key.
+//
+// Spec says:
+//
+//	All the fixed fields declared above are objects that MUST use keys that
+//	match the regular expression: ^[a-zA-Z0-9\.\-_]+$.
+//
+// See https://spec.openapis.org/oas/v3.1.0#components-object.
+func validateComponentsKey[Object any](p *parser, m map[string]Object, locator location.Locator) error {
+	for name := range m {
+		if !componentsKeyRegex.MatchString(name) {
+			locator := locator.Key(name)
+			err := errors.Errorf("invalid name: %q doesn't match %q", name, componentsKeyRegex)
+			return p.wrapLocation("", locator, err)
+		}
+	}
+	return nil
+}
+
+// validateComponentsKeys validates components keys.
+//
+// See validateComponentsKey comment.
+func validateComponentsKeys(p *parser, c *ogen.Components) error {
+	if c == nil {
+		return nil
+	}
+	if err := validateComponentsKey(p, c.Schemas, c.Locator.Field("schemas")); err != nil {
+		return errors.Wrap(err, "schemas")
+	}
+	if err := validateComponentsKey(p, c.Responses, c.Locator.Field("responses")); err != nil {
+		return errors.Wrap(err, "responses")
+	}
+	if err := validateComponentsKey(p, c.Parameters, c.Locator.Field("parameters")); err != nil {
+		return errors.Wrap(err, "parameters")
+	}
+	if err := validateComponentsKey(p, c.Examples, c.Locator.Field("examples")); err != nil {
+		return errors.Wrap(err, "examples")
+	}
+	if err := validateComponentsKey(p, c.RequestBodies, c.Locator.Field("requestBodies")); err != nil {
+		return errors.Wrap(err, "requestBodies")
+	}
+	if err := validateComponentsKey(p, c.Headers, c.Locator.Field("headers")); err != nil {
+		return errors.Wrap(err, "headers")
+	}
+	if err := validateComponentsKey(p, c.SecuritySchemes, c.Locator.Field("securitySchemes")); err != nil {
+		return errors.Wrap(err, "securitySchemes")
+	}
+	if err := validateComponentsKey(p, c.Links, c.Locator.Field("links")); err != nil {
+		return errors.Wrap(err, "links")
+	}
+	if err := validateComponentsKey(p, c.Callbacks, c.Locator.Field("callbacks")); err != nil {
+		return errors.Wrap(err, "callbacks")
+	}
+	if err := validateComponentsKey(p, c.PathItems, c.Locator.Field("pathItems")); err != nil {
+		return errors.Wrap(err, "pathItems")
+	}
+	return nil
+}
+
 func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rerr error) {
 	if c == nil {
 		return &openapi.Components{
@@ -23,6 +87,10 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 		rerr = p.wrapLocation("", c.Locator, rerr)
 	}()
 
+	if err := validateComponentsKeys(p, c); err != nil {
+		return nil, err
+	}
+
 	result := &openapi.Components{
 		Schemas:       make(map[string]*jsonschema.Schema, len(c.Schemas)),
 		Responses:     make(map[string]*openapi.Response, len(c.Responses)),
@@ -32,7 +100,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 	}
 	wrapErr := func(component, name string, err error) error {
 		loc := c.Locator.Field(component).Field(name)
-		err = errors.Wrapf(err, "schemas: %q", name)
+		err = errors.Wrapf(err, "%s: %q", component, name)
 		return p.wrapLocation("", loc, err)
 	}
 


Test patch:
diff --git a/openapi/parser/_testdata/negative/components/invalid_callbacks_component_name.json b/openapi/parser/_testdata/negative/components/invalid_callbacks_component_name.json
new file mode 100644
index 00000000..90190fac
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_callbacks_component_name.json
@@ -0,0 +1,33 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "callbacks": {
+      "foo!bar": {
+        "http://localhost:1000": {
+          "get": {
+            "responses": {
+              "200": {
+                "description": "User info"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_examples_component_name.json b/openapi/parser/_testdata/negative/components/invalid_examples_component_name.json
new file mode 100644
index 00000000..1b940369
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_examples_component_name.json
@@ -0,0 +1,25 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "examples": {
+      "foo!bar": {
+        "value": true
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_headers_component_name.json b/openapi/parser/_testdata/negative/components/invalid_headers_component_name.json
new file mode 100644
index 00000000..164999b8
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_headers_component_name.json
@@ -0,0 +1,27 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "headers": {
+      "foo!bar": {
+        "schema": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_links_component_name.json b/openapi/parser/_testdata/negative/components/invalid_links_component_name.json
new file mode 100644
index 00000000..87873c0a
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_links_component_name.json
@@ -0,0 +1,25 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "links": {
+      "foo!bar": {
+        "operationId": "foo"
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_parameters_component_name.json b/openapi/parser/_testdata/negative/components/invalid_parameters_component_name.json
new file mode 100644
index 00000000..c2fc7084
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_parameters_component_name.json
@@ -0,0 +1,29 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "parameters": {
+      "foo!bar": {
+        "name": "foobar",
+        "in": "query",
+        "schema": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_pathItems_component_name.json b/openapi/parser/_testdata/negative/components/invalid_pathItems_component_name.json
new file mode 100644
index 00000000..ec76e22f
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_pathItems_component_name.json
@@ -0,0 +1,31 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "pathItems": {
+      "foo!bar": {
+        "get": {
+          "responses": {
+            "200": {
+              "description": "User info"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_requestBodies_component_name.json b/openapi/parser/_testdata/negative/components/invalid_requestBodies_component_name.json
new file mode 100644
index 00000000..9fb95025
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_requestBodies_component_name.json
@@ -0,0 +1,31 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "requestBodies": {
+      "foo!bar": {
+        "content": {
+          "application/json": {
+            "schema": {
+              "type": "string"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_responses_component_name.json b/openapi/parser/_testdata/negative/components/invalid_responses_component_name.json
new file mode 100644
index 00000000..ded52211
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_responses_component_name.json
@@ -0,0 +1,25 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "responses": {
+      "foo!bar": {
+        "description": "User info"
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_schemas_component_name.json b/openapi/parser/_testdata/negative/components/invalid_schemas_component_name.json
new file mode 100644
index 00000000..f2f866da
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_schemas_component_name.json
@@ -0,0 +1,25 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "foo!bar": {
+        "type": "string"
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/components/invalid_securitySchemes_component_name.json b/openapi/parser/_testdata/negative/components/invalid_securitySchemes_component_name.json
new file mode 100644
index 00000000..20921fde
--- /dev/null
+++ b/openapi/parser/_testdata/negative/components/invalid_securitySchemes_component_name.json
@@ -0,0 +1,27 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "foo!bar": {
+        "type": "apiKey",
+        "name": "Authorization",
+        "in": "header"
+      }
+    }
+  }
+}