Skip to content

Commit 3d916b8

Browse files
committed
feat: Alert users when they have requested a header type which is not allowed
1 parent 9252125 commit 3d916b8

File tree

4 files changed

+81
-12
lines changed

4 files changed

+81
-12
lines changed

openapi_python_client/parser/openapi.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,6 @@ def add_parameters(
287287
if isinstance(param, oai.Reference) or param.param_schema is None:
288288
continue
289289

290-
if param.param_in == oai.ParameterLocation.PATH and not param.required:
291-
return ParseError(data=param, detail="Path parameter must be required"), schemas
292-
293290
unique_param = (param.name, param.param_in)
294291
if unique_param in unique_parameters:
295292
duplication_detail = (
@@ -300,7 +297,7 @@ def add_parameters(
300297
return ParseError(data=data, detail=duplication_detail), schemas
301298
unique_parameters.add(unique_param)
302299

303-
prop, schemas = property_from_data(
300+
prop, new_schemas = property_from_data(
304301
name=param.name,
305302
required=param.required,
306303
data=param.param_schema,
@@ -310,6 +307,11 @@ def add_parameters(
310307
)
311308
if isinstance(prop, ParseError):
312309
return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), schemas
310+
location_error = prop.validate_location(param.param_in)
311+
if location_error is not None:
312+
location_error.data = param
313+
return location_error, schemas
314+
schemas = new_schemas
313315
if prop.name in parameters_by_location[param.param_in]:
314316
# This parameter was defined in the Operation, so ignore the PathItem definition
315317
continue

openapi_python_client/parser/properties/__init__.py

+24
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class StringProperty(Property):
4949
pattern: Optional[str] = None
5050
_type_string: ClassVar[str] = "str"
5151
_json_type_string: ClassVar[str] = "str"
52+
_allowed_locations: ClassVar[Set[oai.ParameterLocation]] = {
53+
oai.ParameterLocation.QUERY,
54+
oai.ParameterLocation.PATH,
55+
oai.ParameterLocation.COOKIE,
56+
oai.ParameterLocation.HEADER,
57+
}
5258

5359

5460
@attr.s(auto_attribs=True, frozen=True)
@@ -123,6 +129,12 @@ class FloatProperty(Property):
123129

124130
_type_string: ClassVar[str] = "float"
125131
_json_type_string: ClassVar[str] = "float"
132+
_allowed_locations: ClassVar[Set[oai.ParameterLocation]] = {
133+
oai.ParameterLocation.QUERY,
134+
oai.ParameterLocation.PATH,
135+
oai.ParameterLocation.COOKIE,
136+
oai.ParameterLocation.HEADER,
137+
}
126138
template: ClassVar[str] = "float_property.py.jinja"
127139

128140

@@ -132,6 +144,12 @@ class IntProperty(Property):
132144

133145
_type_string: ClassVar[str] = "int"
134146
_json_type_string: ClassVar[str] = "int"
147+
_allowed_locations: ClassVar[Set[oai.ParameterLocation]] = {
148+
oai.ParameterLocation.QUERY,
149+
oai.ParameterLocation.PATH,
150+
oai.ParameterLocation.COOKIE,
151+
oai.ParameterLocation.HEADER,
152+
}
135153
template: ClassVar[str] = "int_property.py.jinja"
136154

137155

@@ -141,6 +159,12 @@ class BooleanProperty(Property):
141159

142160
_type_string: ClassVar[str] = "bool"
143161
_json_type_string: ClassVar[str] = "bool"
162+
_allowed_locations: ClassVar[Set[oai.ParameterLocation]] = {
163+
oai.ParameterLocation.QUERY,
164+
oai.ParameterLocation.PATH,
165+
oai.ParameterLocation.COOKIE,
166+
oai.ParameterLocation.HEADER,
167+
}
144168
template: ClassVar[str] = "boolean_property.py.jinja"
145169

146170

openapi_python_client/parser/properties/property.py

+15
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import attr
66

77
from ... import Config
8+
from ... import schema as oai
89
from ...utils import PythonIdentifier
10+
from ..errors import ParseError
911

1012

1113
@attr.s(auto_attribs=True, frozen=True)
@@ -28,6 +30,11 @@ class Property:
2830
nullable: bool
2931
_type_string: ClassVar[str] = ""
3032
_json_type_string: ClassVar[str] = "" # Type of the property after JSON serialization
33+
_allowed_locations: ClassVar[Set[oai.ParameterLocation]] = {
34+
oai.ParameterLocation.QUERY,
35+
oai.ParameterLocation.PATH,
36+
oai.ParameterLocation.COOKIE,
37+
}
3138
default: Optional[str] = attr.ib()
3239
python_name: PythonIdentifier
3340
description: Optional[str] = attr.ib()
@@ -36,6 +43,14 @@ class Property:
3643
template: ClassVar[str] = "any_property.py.jinja"
3744
json_is_dict: ClassVar[bool] = False
3845

46+
def validate_location(self, location: oai.ParameterLocation) -> Optional[ParseError]:
47+
"""Returns an error if this type of property is not allowed in the given location"""
48+
if location not in self._allowed_locations:
49+
return ParseError(detail=f"{self.get_type_string()} is not allowed in {location}")
50+
if location == oai.ParameterLocation.PATH and not self.required:
51+
return ParseError(detail="Path parameter must be required")
52+
return None
53+
3954
def set_python_name(self, new_name: str, config: Config) -> None:
4055
"""Mutates this Property to set a new python_name.
4156

tests/test_parser/test_openapi.py

+36-8
Original file line numberDiff line numberDiff line change
@@ -507,9 +507,38 @@ def test_add_parameters_parse_error(self, mocker):
507507
)
508508
assert result == (
509509
ParseError(data=parse_error.data, detail=f"cannot parse parameter of endpoint {endpoint.name}"),
510-
property_schemas,
510+
initial_schemas,
511511
)
512512

513+
@pytest.mark.parametrize(
514+
"data_type, allowed",
515+
[
516+
(oai.DataType.STRING, True),
517+
(oai.DataType.INTEGER, True),
518+
(oai.DataType.NUMBER, True),
519+
(oai.DataType.BOOLEAN, True),
520+
(oai.DataType.ARRAY, False),
521+
(oai.DataType.OBJECT, False),
522+
],
523+
)
524+
def test_add_parameters_header_types(self, data_type, allowed):
525+
from openapi_python_client.parser.openapi import Endpoint
526+
527+
endpoint = self.make_endpoint()
528+
initial_schemas = Schemas()
529+
param = oai.Parameter.construct(
530+
name="test", required=True, param_schema=oai.Schema(type=data_type), param_in=oai.ParameterLocation.HEADER
531+
)
532+
config = Config()
533+
534+
result = Endpoint.add_parameters(
535+
endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=initial_schemas, config=config
536+
)
537+
if allowed:
538+
assert isinstance(result[0], Endpoint)
539+
else:
540+
assert isinstance(result[0], ParseError)
541+
513542
def test__add_parameters_parse_error_on_non_required_path_param(self):
514543
endpoint = self.make_endpoint()
515544
param = oai.Parameter.construct(
@@ -534,13 +563,12 @@ def test_validation_error_when_location_not_supported(self, mocker):
534563
def test__add_parameters_with_location_postfix_conflict1(self, mocker, property_factory):
535564
"""Checks when the PythonIdentifier of new parameter already used."""
536565
from openapi_python_client.parser.openapi import Endpoint
537-
from openapi_python_client.parser.properties import Property
538566

539567
endpoint = self.make_endpoint()
540568

541-
path_prop_conflicted = property_factory(name="prop_name_path", required=False, nullable=False, default=None)
542-
query_prop = property_factory(name="prop_name", required=False, nullable=False, default=None)
543-
path_prop = property_factory(name="prop_name", required=False, nullable=False, default=None)
569+
path_prop_conflicted = property_factory(name="prop_name_path", required=True, nullable=False, default=None)
570+
query_prop = property_factory(name="prop_name", required=True, nullable=False, default=None)
571+
path_prop = property_factory(name="prop_name", required=True, nullable=False, default=None)
544572

545573
schemas_1 = mocker.MagicMock()
546574
schemas_2 = mocker.MagicMock()
@@ -582,9 +610,9 @@ def test__add_parameters_with_location_postfix_conflict2(self, mocker, property_
582610
from openapi_python_client.parser.openapi import Endpoint
583611

584612
endpoint = self.make_endpoint()
585-
path_prop_conflicted = property_factory(name="prop_name_path", required=False, nullable=False, default=None)
586-
path_prop = property_factory(name="prop_name", required=False, nullable=False, default=None)
587-
query_prop = property_factory(name="prop_name", required=False, nullable=False, default=None)
613+
path_prop_conflicted = property_factory(name="prop_name_path", required=True, nullable=False, default=None)
614+
path_prop = property_factory(name="prop_name", required=True, nullable=False, default=None)
615+
query_prop = property_factory(name="prop_name", required=True, nullable=False, default=None)
588616
schemas_1 = mocker.MagicMock()
589617
schemas_2 = mocker.MagicMock()
590618
schemas_3 = mocker.MagicMock()

0 commit comments

Comments
 (0)