Skip to content

Commit b9e8057

Browse files
committed
Ensure path parameters are ordered by appearance in path
1 parent fd5629f commit b9e8057

File tree

4 files changed

+92
-9
lines changed

4 files changed

+92
-9
lines changed

end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77

88

99
def _get_kwargs(
10-
param_1: str,
10+
param_4: str,
1111
param_2: int,
12+
param_1: str,
13+
param_3: int,
1214
*,
1315
client: Client,
1416
) -> Dict[str, Any]:
15-
url = "{}/multiple-path-parameters/{param1}/{param2}".format(client.base_url, param1=param_1, param2=param_2)
17+
url = "{}/multiple-path-parameters/{param4}/{param2}/{param1}/{param3}".format(
18+
client.base_url, param4=param_4, param2=param_2, param1=param_1, param3=param_3
19+
)
1620

1721
headers: Dict[str, Any] = client.get_headers()
1822
cookies: Dict[str, Any] = client.get_cookies()
@@ -35,14 +39,18 @@ def _build_response(*, response: httpx.Response) -> Response[None]:
3539

3640

3741
def sync_detailed(
38-
param_1: str,
42+
param_4: str,
3943
param_2: int,
44+
param_1: str,
45+
param_3: int,
4046
*,
4147
client: Client,
4248
) -> Response[None]:
4349
kwargs = _get_kwargs(
44-
param_1=param_1,
50+
param_4=param_4,
4551
param_2=param_2,
52+
param_1=param_1,
53+
param_3=param_3,
4654
client=client,
4755
)
4856

@@ -54,14 +62,18 @@ def sync_detailed(
5462

5563

5664
async def asyncio_detailed(
57-
param_1: str,
65+
param_4: str,
5866
param_2: int,
67+
param_1: str,
68+
param_3: int,
5969
*,
6070
client: Client,
6171
) -> Response[None]:
6272
kwargs = _get_kwargs(
63-
param_1=param_1,
73+
param_4=param_4,
6474
param_2=param_2,
75+
param_1=param_1,
76+
param_3=param_3,
6577
client=client,
6678
)
6779

end_to_end_tests/openapi.json

+21-3
Original file line numberDiff line numberDiff line change
@@ -795,8 +795,8 @@
795795
}
796796
}
797797
},
798-
"/multiple-path-parameters/{param1}/{param2}": {
799-
"description": "Test with multiple path parameters",
798+
"/multiple-path-parameters/{param4}/{param2}/{param1}/{param3}": {
799+
"description": "Test that multiple path parameters are ordered by appearance in path",
800800
"get": {
801801
"tags": [
802802
"parameters"
@@ -825,7 +825,25 @@
825825
"description": "Success"
826826
}
827827
}
828-
}
828+
},
829+
"parameters": [
830+
{
831+
"name": "param4",
832+
"in": "path",
833+
"required": true,
834+
"schema": {
835+
"type": "string"
836+
}
837+
},
838+
{
839+
"name": "param3",
840+
"in": "path",
841+
"required": true,
842+
"schema": {
843+
"type": "integer"
844+
}
845+
}
846+
]
829847
}
830848
},
831849
"components": {

openapi_python_client/parser/openapi.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import itertools
2+
import re
23
from copy import deepcopy
34
from dataclasses import dataclass, field
45
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
@@ -12,6 +13,8 @@
1213
from .properties import Class, EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data
1314
from .responses import Response, response_from_data
1415

16+
_PATH_PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)}")
17+
1518

1619
def import_string_from_class(class_: Class, prefix: str = "") -> str:
1720
"""Create a string which is used to import a reference"""
@@ -49,6 +52,8 @@ def from_data(
4952
endpoint, schemas = Endpoint._add_parameters(
5053
endpoint=endpoint, data=path_data, schemas=schemas, config=config
5154
)
55+
if not isinstance(endpoint, ParseError):
56+
endpoint = Endpoint._sort_parameters(endpoint=endpoint, path=path)
5257
if isinstance(endpoint, ParseError):
5358
endpoint.header = (
5459
f"ERROR parsing {method.upper()} {path} within {tag}. Endpoint will not be generated."
@@ -268,6 +273,20 @@ def _add_parameters(
268273

269274
return endpoint, schemas
270275

276+
@staticmethod
277+
def _sort_parameters(*, endpoint: "Endpoint", path: str) -> Union["Endpoint", ParseError]:
278+
endpoint = deepcopy(endpoint)
279+
parameters_form_path = re.findall(_PATH_PARAM_REGEX, path)
280+
path_parameter_names = [p.name for p in endpoint.path_parameters]
281+
if sorted(parameters_form_path) != sorted(path_parameter_names):
282+
return ParseError(
283+
data=endpoint.path_parameters,
284+
detail="Incorrect path templating (Path parameters do not match with path)",
285+
)
286+
287+
endpoint.path_parameters.sort(key=lambda p: parameters_form_path.index(p.name))
288+
return endpoint
289+
271290
@staticmethod
272291
def from_data(
273292
*, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas, config: Config

tests/test_parser/test_openapi.py

+34
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,40 @@ def test__add_parameters_duplicate_properties_different_location(self):
606606
assert result.query_parameters[0].python_name == "test_query"
607607
assert result.query_parameters[0].name == "test"
608608

609+
def test__sort_parameters(self, mocker):
610+
from openapi_python_client.parser.openapi import Endpoint
611+
612+
endpoint = self.make_endpoint()
613+
path = "/multiple-path-parameters/{param4}/{param2}/{param1}/{param3}"
614+
615+
for i in range(1, 5):
616+
param = oai.Parameter.construct(
617+
name=f"param{i}", required=True, param_schema=mocker.MagicMock(), param_in=oai.ParameterLocation.PATH
618+
)
619+
endpoint.path_parameters.append(param)
620+
621+
result = Endpoint._sort_parameters(endpoint=endpoint, path=path)
622+
result_names = [p.name for p in result.path_parameters]
623+
expected_names = [f"param{i}" for i in (4, 2, 1, 3)]
624+
625+
assert result_names == expected_names
626+
627+
def test__sort_parameters_invalid_path_templating(self, mocker):
628+
from openapi_python_client.parser.openapi import Endpoint
629+
630+
endpoint = self.make_endpoint()
631+
path = "/multiple-path-parameters/{param1}/{param2}"
632+
param = oai.Parameter.construct(
633+
name=f"param1", required=True, param_schema=mocker.MagicMock(), param_in=oai.ParameterLocation.PATH
634+
)
635+
endpoint.path_parameters.append(param)
636+
637+
result = Endpoint._sort_parameters(endpoint=endpoint, path=path)
638+
639+
assert isinstance(result, ParseError)
640+
assert result.data == [param]
641+
assert "Incorrect path templating" in result.detail
642+
609643
def test_from_data_bad_params(self, mocker):
610644
from openapi_python_client.parser.openapi import Endpoint
611645

0 commit comments

Comments
 (0)