Skip to content

Commit 6b58d0b

Browse files
author
Jordi Sanchez
committed
feat: Adds support for parameter components and parameter references.
1 parent 26e7e0f commit 6b58d0b

File tree

8 files changed

+502
-96
lines changed

8 files changed

+502
-96
lines changed

openapi_python_client/parser/errors.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from enum import Enum
33
from typing import Optional
44

5-
__all__ = ["ErrorLevel", "GeneratorError", "ParseError", "PropertyError", "ValidationError"]
5+
__all__ = ["ErrorLevel", "GeneratorError", "ParseError", "PropertyError", "ValidationError", "ParameterError"]
66

77
from pydantic import BaseModel
88

@@ -39,5 +39,12 @@ class PropertyError(ParseError):
3939
header = "Problem creating a Property: "
4040

4141

42+
@dataclass
43+
class ParameterError(ParseError):
44+
"""Error raised when there's a problem creating a Parameter."""
45+
46+
header = "Problem creating a Parameter: "
47+
48+
4249
class ValidationError(Exception):
4350
"""Used internally to exit quickly from property parsing due to some internal exception."""

openapi_python_client/parser/openapi.py

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@
1212
from ..config import Config
1313
from ..utils import PythonIdentifier
1414
from .errors import GeneratorError, ParseError, PropertyError
15-
from .properties import Class, EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data
15+
from .properties import (
16+
Class,
17+
EnumProperty,
18+
ModelProperty,
19+
Parameters,
20+
Property,
21+
Schemas,
22+
build_parameters,
23+
build_schemas,
24+
property_from_data,
25+
)
26+
from .properties.schemas import parse_reference_path
1627
from .responses import Response, response_from_data
1728

1829
_PATH_PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)}")
@@ -33,8 +44,8 @@ class EndpointCollection:
3344

3445
@staticmethod
3546
def from_data(
36-
*, data: Dict[str, oai.PathItem], schemas: Schemas, config: Config
37-
) -> Tuple[Dict[utils.PythonIdentifier, "EndpointCollection"], Schemas]:
47+
*, data: Dict[str, oai.PathItem], schemas: Schemas, parameters: Parameters, config: Config
48+
) -> Tuple[Dict[utils.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]:
3849
"""Parse the openapi paths data to get EndpointCollections by tag"""
3950
endpoints_by_tag: Dict[utils.PythonIdentifier, EndpointCollection] = {}
4051

@@ -47,13 +58,19 @@ def from_data(
4758
continue
4859
tag = utils.PythonIdentifier(value=(operation.tags or ["default"])[0], prefix="tag")
4960
collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag))
50-
endpoint, schemas = Endpoint.from_data(
51-
data=operation, path=path, method=method, tag=tag, schemas=schemas, config=config
61+
endpoint, schemas, parameters = Endpoint.from_data(
62+
data=operation,
63+
path=path,
64+
method=method,
65+
tag=tag,
66+
schemas=schemas,
67+
parameters=parameters,
68+
config=config,
5269
)
5370
# Add `PathItem` parameters
5471
if not isinstance(endpoint, ParseError):
55-
endpoint, schemas = Endpoint.add_parameters(
56-
endpoint=endpoint, data=path_data, schemas=schemas, config=config
72+
endpoint, schemas, parameters = Endpoint.add_parameters(
73+
endpoint=endpoint, data=path_data, schemas=schemas, parameters=parameters, config=config
5774
)
5875
if not isinstance(endpoint, ParseError):
5976
endpoint = Endpoint.sort_parameters(endpoint=endpoint)
@@ -68,7 +85,7 @@ def from_data(
6885
collection.parse_errors.append(error)
6986
collection.endpoints.append(endpoint)
7087

71-
return endpoints_by_tag, schemas
88+
return endpoints_by_tag, schemas, parameters
7289

7390

7491
def generate_operation_id(*, path: str, method: str) -> str:
@@ -248,8 +265,13 @@ def _add_responses(
248265
# pylint: disable=too-many-return-statements
249266
@staticmethod
250267
def add_parameters(
251-
*, endpoint: "Endpoint", data: Union[oai.Operation, oai.PathItem], schemas: Schemas, config: Config
252-
) -> Tuple[Union["Endpoint", ParseError], Schemas]:
268+
*,
269+
endpoint: "Endpoint",
270+
data: Union[oai.Operation, oai.PathItem],
271+
schemas: Schemas,
272+
parameters: Parameters,
273+
config: Config,
274+
) -> Tuple[Union["Endpoint", ParseError], Schemas, Parameters]:
253275
"""Process the defined `parameters` for an Endpoint.
254276
255277
Any existing parameters will be ignored, so earlier instances of a parameter take precedence. PathItem
@@ -259,6 +281,7 @@ def add_parameters(
259281
endpoint: The endpoint to add parameters to.
260282
data: The Operation or PathItem to add parameters from.
261283
schemas: The cumulative Schemas of processing so far which should contain details for any references.
284+
parameters: The cumulative Parameters of processing so far which should contain details for any references.
262285
config: User-provided config for overrides within parameters.
263286
264287
Returns:
@@ -271,9 +294,10 @@ def add_parameters(
271294
- https://swagger.io/docs/specification/paths-and-operations/
272295
"""
273296

274-
endpoint = deepcopy(endpoint)
275297
if data.parameters is None:
276-
return endpoint, schemas
298+
return endpoint, schemas, parameters
299+
300+
endpoint = deepcopy(endpoint)
277301

278302
unique_parameters: Set[Tuple[str, oai.ParameterLocation]] = set()
279303
parameters_by_location = {
@@ -283,9 +307,22 @@ def add_parameters(
283307
oai.ParameterLocation.COOKIE: endpoint.cookie_parameters,
284308
}
285309

286-
for param in data.parameters:
287-
if isinstance(param, oai.Reference) or param.param_schema is None:
288-
continue
310+
for _param in data.parameters:
311+
param: oai.Parameter
312+
313+
if _param is None:
314+
return ParseError(data=data, detail="Null parameter provided."), schemas, parameters
315+
316+
if isinstance(_param, oai.Reference):
317+
ref_path = parse_reference_path(_param.ref)
318+
if isinstance(ref_path, ParseError):
319+
return ref_path, schemas, parameters
320+
_resolved_class = parameters.classes_by_reference.get(ref_path)
321+
if _resolved_class is None:
322+
return ParseError(data=data, detail=f"Reference `{ref_path}` not found."), schemas, parameters
323+
param = _resolved_class
324+
elif isinstance(_param, oai.Parameter):
325+
param = _param
289326

290327
unique_param = (param.name, param.param_in)
291328
if unique_param in unique_parameters:
@@ -294,9 +331,12 @@ def add_parameters(
294331
"A unique parameter is defined by a combination of a name and location. "
295332
f"Duplicated parameters named `{param.name}` detected in `{param.param_in}`."
296333
)
297-
return ParseError(data=data, detail=duplication_detail), schemas
334+
return ParseError(data=data, detail=duplication_detail), schemas, parameters
298335
unique_parameters.add(unique_param)
299336

337+
if param.param_schema is None:
338+
continue
339+
300340
prop, new_schemas = property_from_data(
301341
name=param.name,
302342
required=param.required,
@@ -305,13 +345,21 @@ def add_parameters(
305345
parent_name=endpoint.name,
306346
config=config,
307347
)
348+
308349
if isinstance(prop, ParseError):
309-
return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), schemas
350+
return (
351+
ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data),
352+
schemas,
353+
parameters,
354+
)
355+
356+
schemas = new_schemas
357+
310358
location_error = prop.validate_location(param.param_in)
311359
if location_error is not None:
312360
location_error.data = param
313-
return location_error, schemas
314-
schemas = new_schemas
361+
return location_error, schemas, parameters
362+
315363
if prop.name in parameters_by_location[param.param_in]:
316364
# This parameter was defined in the Operation, so ignore the PathItem definition
317365
continue
@@ -331,6 +379,7 @@ def add_parameters(
331379
data=data,
332380
),
333381
schemas,
382+
parameters,
334383
)
335384
endpoint.used_python_identifiers.add(existing_prop.python_name)
336385
prop.set_python_name(new_name=f"{param.name}_{param.param_in}", config=config)
@@ -341,6 +390,7 @@ def add_parameters(
341390
detail=f"Parameters with same Python identifier `{prop.python_name}` detected", data=data
342391
),
343392
schemas,
393+
parameters,
344394
)
345395
if param.param_in == oai.ParameterLocation.QUERY and (prop.nullable or not prop.required):
346396
# There is no NULL for query params, so nullable and not required are the same.
@@ -350,7 +400,7 @@ def add_parameters(
350400
endpoint.used_python_identifiers.add(prop.python_name)
351401
parameters_by_location[param.param_in][prop.name] = prop
352402

353-
return endpoint, schemas
403+
return endpoint, schemas, parameters
354404

355405
@staticmethod
356406
def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
@@ -382,8 +432,15 @@ def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
382432

383433
@staticmethod
384434
def from_data(
385-
*, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas, config: Config
386-
) -> Tuple[Union["Endpoint", ParseError], Schemas]:
435+
*,
436+
data: oai.Operation,
437+
path: str,
438+
method: str,
439+
tag: str,
440+
schemas: Schemas,
441+
parameters: Parameters,
442+
config: Config,
443+
) -> Tuple[Union["Endpoint", ParseError], Schemas, Parameters]:
387444
"""Construct an endpoint from the OpenAPI data"""
388445

389446
if data.operationId is None:
@@ -401,13 +458,15 @@ def from_data(
401458
tag=tag,
402459
)
403460

404-
result, schemas = Endpoint.add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config)
461+
result, schemas, parameters = Endpoint.add_parameters(
462+
endpoint=endpoint, data=data, schemas=schemas, parameters=parameters, config=config
463+
)
405464
if isinstance(result, ParseError):
406-
return result, schemas
465+
return result, schemas, parameters
407466
result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas, config=config)
408467
result, schemas = Endpoint._add_body(endpoint=result, data=data, schemas=schemas, config=config)
409468

410-
return result, schemas
469+
return result, schemas, parameters
411470

412471
def response_type(self) -> str:
413472
"""Get the Python type of any response from this endpoint"""
@@ -459,10 +518,15 @@ def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData",
459518
)
460519
return GeneratorError(header="Failed to parse OpenAPI document", detail=detail)
461520
schemas = Schemas()
521+
parameters = Parameters()
462522
if openapi.components and openapi.components.schemas:
463523
schemas = build_schemas(components=openapi.components.schemas, schemas=schemas, config=config)
464-
endpoint_collections_by_tag, schemas = EndpointCollection.from_data(
465-
data=openapi.paths, schemas=schemas, config=config
524+
if openapi.components and openapi.components.parameters:
525+
parameters = build_parameters(
526+
components=openapi.components.parameters, schemas=schemas, parameters=parameters, config=config
527+
)
528+
endpoint_collections_by_tag, schemas, parameters = EndpointCollection.from_data(
529+
data=openapi.paths, schemas=schemas, parameters=parameters, config=config
466530
)
467531

468532
enums = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty))

openapi_python_client/parser/properties/__init__.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"Class",
44
"EnumProperty",
55
"ModelProperty",
6+
"Parameters",
67
"Property",
78
"Schemas",
89
"build_schemas",
10+
"build_parameters",
911
"property_from_data",
1012
]
1113

@@ -17,12 +19,19 @@
1719
from ... import Config
1820
from ... import schema as oai
1921
from ... import utils
20-
from ..errors import ParseError, PropertyError, ValidationError
22+
from ..errors import ParameterError, ParseError, PropertyError, ValidationError
2123
from .converter import convert, convert_chain
2224
from .enum_property import EnumProperty
2325
from .model_property import ModelProperty, build_model_property
2426
from .property import Property
25-
from .schemas import Class, Schemas, parse_reference_path, update_schemas_with_data
27+
from .schemas import (
28+
Class,
29+
Parameters,
30+
Schemas,
31+
parse_reference_path,
32+
update_parameters_with_data,
33+
update_schemas_with_data,
34+
)
2635

2736

2837
@attr.s(auto_attribs=True, frozen=True)
@@ -728,3 +737,46 @@ def build_schemas(
728737

729738
schemas.errors.extend(errors)
730739
return schemas
740+
741+
742+
def build_parameters(
743+
*,
744+
components: Dict[str, Union[oai.Reference, oai.Parameter]],
745+
parameters: Parameters,
746+
schemas: Schemas,
747+
config: Config,
748+
) -> Parameters:
749+
"""Get a list of Schemas from an OpenAPI dict"""
750+
to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Parameter]]] = []
751+
if components is not None:
752+
to_process = components.items()
753+
still_making_progress = True
754+
errors: List[ParameterError] = []
755+
756+
# References could have forward References so keep going as long as we are making progress
757+
while still_making_progress:
758+
still_making_progress = False
759+
errors = []
760+
next_round = []
761+
# Only accumulate errors from the last round, since we might fix some along the way
762+
for name, data in to_process:
763+
if isinstance(data, oai.Reference):
764+
parameters.errors.append(PropertyError(data=data, detail="Reference schemas are not supported."))
765+
continue
766+
ref_path = parse_reference_path(f"#/components/parameters/{name}")
767+
if isinstance(ref_path, ParseError):
768+
parameters.errors.append(PropertyError(detail=ref_path.detail, data=data))
769+
continue
770+
parameters_or_err = update_parameters_with_data(
771+
ref_path=ref_path, data=data, schemas=schemas, parameters=parameters, config=config
772+
)
773+
if isinstance(parameters_or_err, ParameterError):
774+
next_round.append((name, data))
775+
errors.append(parameters_or_err)
776+
continue
777+
parameters = parameters_or_err
778+
still_making_progress = True
779+
to_process = next_round
780+
781+
parameters.errors.extend(errors)
782+
return parameters

0 commit comments

Comments
 (0)