Skip to content

Commit fe39bae

Browse files
committed
Added support for enum schemas. Closes openapi-generators#102
1 parent 2b52f41 commit fe39bae

File tree

11 files changed

+83
-45
lines changed

11 files changed

+83
-45
lines changed

end_to_end_tests/fastapi_app/openapi.json

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@
4141
"title": "An Enum Value",
4242
"type": "array",
4343
"items": {
44-
"enum": [
45-
"FIRST_VALUE",
46-
"SECOND_VALUE"
47-
]
44+
"$ref": "#/components/schemas/AnEnum"
4845
}
4946
},
5047
"name": "an_enum_value",
@@ -150,22 +147,15 @@
150147
"type": "object",
151148
"properties": {
152149
"an_enum_value": {
153-
"title": "An Enum Value",
154-
"enum": [
155-
"FIRST_VALUE",
156-
"SECOND_VALUE"
157-
]
150+
"$ref": "#/components/schemas/AnEnum"
158151
},
159152
"nested_list_of_enums": {
160153
"title": "Nested List Of Enums",
161154
"type": "array",
162155
"items": {
163156
"type": "array",
164157
"items": {
165-
"enum": [
166-
"DIFFERENT",
167-
"OTHER"
168-
]
158+
"$ref": "#/components/schemas/DifferentEnum"
169159
}
170160
},
171161
"default": []
@@ -199,6 +189,14 @@
199189
},
200190
"description": "A Model for testing all the ways custom objects can be used "
201191
},
192+
"AnEnum": {
193+
"title": "AnEnum",
194+
"enum": [
195+
"FIRST_VALUE",
196+
"SECOND_VALUE"
197+
],
198+
"description": "For testing Enums in all the ways they can be used "
199+
},
202200
"Body_upload_file_tests_upload_post": {
203201
"title": "Body_upload_file_tests_upload_post",
204202
"required": [
@@ -213,6 +211,14 @@
213211
}
214212
}
215213
},
214+
"DifferentEnum": {
215+
"title": "DifferentEnum",
216+
"enum": [
217+
"DIFFERENT",
218+
"OTHER"
219+
],
220+
"description": "An enumeration."
221+
},
216222
"HTTPValidationError": {
217223
"title": "HTTPValidationError",
218224
"type": "object",

end_to_end_tests/golden-master/my_test_api_client/api/users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from ..client import AuthenticatedClient, Client
88
from ..errors import ApiResponseError
99
from ..models.a_model import AModel
10-
from ..models.an_enum_value import AnEnumValue
10+
from ..models.an_enum import AnEnum
1111
from ..models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost
1212
from ..models.http_validation_error import HTTPValidationError
1313

1414

1515
def get_user_list(
16-
*, client: Client, an_enum_value: List[AnEnumValue], some_date: Union[date, datetime],
16+
*, client: Client, an_enum_value: List[AnEnum], some_date: Union[date, datetime],
1717
) -> Union[
1818
List[AModel], HTTPValidationError,
1919
]:

end_to_end_tests/golden-master/my_test_api_client/async_api/users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from ..client import AuthenticatedClient, Client
88
from ..errors import ApiResponseError
99
from ..models.a_model import AModel
10-
from ..models.an_enum_value import AnEnumValue
10+
from ..models.an_enum import AnEnum
1111
from ..models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost
1212
from ..models.http_validation_error import HTTPValidationError
1313

1414

1515
async def get_user_list(
16-
*, client: Client, an_enum_value: List[AnEnumValue], some_date: Union[date, datetime],
16+
*, client: Client, an_enum_value: List[AnEnum], some_date: Union[date, datetime],
1717
) -> Union[
1818
List[AModel], HTTPValidationError,
1919
]:
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
""" Contains all the data models used in inputs/outputs """
22

33
from .a_model import AModel
4-
from .an_enum_value import AnEnumValue
5-
from .an_enum_value1 import AnEnumValue1
4+
from .an_enum import AnEnum
65
from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost
6+
from .different_enum import DifferentEnum
77
from .http_validation_error import HTTPValidationError
88
from .types import *
99
from .validation_error import ValidationError

end_to_end_tests/golden-master/my_test_api_client/models/a_model.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
from datetime import date, datetime
55
from typing import Any, Dict, List, Optional, Union, cast
66

7-
from .an_enum_value import AnEnumValue
8-
from .an_enum_value1 import AnEnumValue1
7+
from .an_enum import AnEnum
8+
from .different_enum import DifferentEnum
99

1010

1111
@dataclass
1212
class AModel:
1313
""" A Model for testing all the ways custom objects can be used """
1414

15-
an_enum_value: AnEnumValue
15+
an_enum_value: AnEnum
1616
a_camel_date_time: Union[datetime, date]
1717
a_date: date
18-
nested_list_of_enums: Optional[List[List[AnEnumValue1]]] = field(
19-
default_factory=lambda: cast(Optional[List[List[AnEnumValue1]]], [])
18+
nested_list_of_enums: Optional[List[List[DifferentEnum]]] = field(
19+
default_factory=lambda: cast(Optional[List[List[DifferentEnum]]], [])
2020
)
2121
some_dict: Optional[Dict[Any, Any]] = field(default_factory=lambda: cast(Optional[Dict[Any, Any]], {}))
2222

@@ -56,7 +56,7 @@ def to_dict(self) -> Dict[str, Any]:
5656

5757
@staticmethod
5858
def from_dict(d: Dict[str, Any]) -> AModel:
59-
an_enum_value = AnEnumValue(d["an_enum_value"])
59+
an_enum_value = AnEnum(d["an_enum_value"])
6060

6161
def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime, date]:
6262
a_camel_date_time: Union[datetime, date]
@@ -78,7 +78,7 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime, date]:
7878
for nested_list_of_enums_item_data in d.get("nested_list_of_enums") or []:
7979
nested_list_of_enums_item = []
8080
for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data:
81-
nested_list_of_enums_item_item = AnEnumValue1(nested_list_of_enums_item_item_data)
81+
nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data)
8282

8383
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
8484

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
4+
class AnEnum(str, Enum):
5+
FIRST_VALUE = "FIRST_VALUE"
6+
SECOND_VALUE = "SECOND_VALUE"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
4+
class DifferentEnum(str, Enum):
5+
DIFFERENT = "DIFFERENT"
6+
OTHER = "OTHER"

openapi_python_client/parser/openapi.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,14 @@ class Model:
208208
relative_imports: Set[str]
209209

210210
@staticmethod
211-
def from_data(*, data: Union[oai.Reference, oai.Schema], name: str) -> Union[Model, ParseError]:
211+
def from_data(*, data: oai.Schema, name: str) -> Union[Model, ParseError]:
212212
""" A single Model from its OAI data
213213
214214
Args:
215215
data: Data of a single Schema
216216
name: Name by which the schema is referenced, such as a model name.
217217
Used to infer the type name if a `title` property is not available.
218218
"""
219-
if isinstance(data, oai.Reference):
220-
return ParseError(data=data, detail="Reference schemas are not supported.")
221219
required_set = set(data.required or [])
222220
required_properties: List[Property] = []
223221
optional_properties: List[Property] = []
@@ -258,6 +256,18 @@ def build(*, schemas: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas:
258256
""" Get a list of Schemas from an OpenAPI dict """
259257
result = Schemas()
260258
for name, data in schemas.items():
259+
if isinstance(data, oai.Reference):
260+
result.errors.append(ParseError(data=data, detail="Reference schemas are not supported."))
261+
continue
262+
if data.enum is not None:
263+
EnumProperty(
264+
name=name,
265+
title=data.title or name,
266+
required=True,
267+
default=data.default,
268+
values=EnumProperty.values_from_list(data.enum),
269+
)
270+
continue
261271
s = Model.from_data(data=data, name=name)
262272
if isinstance(s, ParseError):
263273
result.errors.append(s)

openapi_python_client/parser/properties.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ def get_all_enums() -> Dict[str, EnumProperty]:
263263
""" Get all the EnumProperties that have been registered keyed by class name """
264264
return _existing_enums
265265

266+
@staticmethod
267+
def get_enum(name: str) -> Optional[EnumProperty]:
268+
""" Get all the EnumProperties that have been registered keyed by class name """
269+
return _existing_enums.get(name)
270+
266271
def get_type_string(self) -> str:
267272
""" Get a string representation of type that should be used when declaring this property """
268273

@@ -304,7 +309,12 @@ class RefProperty(Property):
304309

305310
reference: Reference
306311

307-
template: ClassVar[str] = "ref_property.pyi"
312+
@property
313+
def template(self) -> str: # type: ignore
314+
enum = EnumProperty.get_enum(self.reference.class_name)
315+
if enum:
316+
return "enum_property.pyi"
317+
return "ref_property.pyi"
308318

309319
def get_type_string(self) -> str:
310320
""" Get a string representation of type that should be used when declaring this property """

tests/test_cli.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pathlib import PosixPath
1+
from pathlib import Path
22
from unittest.mock import MagicMock
33

44
import pytest
@@ -35,8 +35,8 @@ def test_config(mocker, _create_new_client):
3535
result = runner.invoke(app, [f"--config={config_path}", "generate", f"--path={path}"], catch_exceptions=False)
3636

3737
assert result.exit_code == 0
38-
load_config.assert_called_once_with(path=PosixPath(config_path))
39-
_create_new_client.assert_called_once_with(url=None, path=PosixPath(path))
38+
load_config.assert_called_once_with(path=Path(config_path))
39+
_create_new_client.assert_called_once_with(url=None, path=Path(path))
4040

4141

4242
def test_bad_config(mocker, _create_new_client):
@@ -50,7 +50,7 @@ def test_bad_config(mocker, _create_new_client):
5050

5151
assert result.exit_code == 2
5252
assert "Unable to parse config" in result.stdout
53-
load_config.assert_called_once_with(path=PosixPath(config_path))
53+
load_config.assert_called_once_with(path=Path(config_path))
5454
_create_new_client.assert_not_called()
5555

5656

@@ -87,7 +87,7 @@ def test_generate_path(self, _create_new_client):
8787
result = runner.invoke(app, ["generate", f"--path={path}"])
8888

8989
assert result.exit_code == 0
90-
_create_new_client.assert_called_once_with(url=None, path=PosixPath(path))
90+
_create_new_client.assert_called_once_with(url=None, path=Path(path))
9191

9292
def test_generate_handle_errors(self, _create_new_client):
9393
_create_new_client.return_value = [GeneratorError(detail="this is a message")]
@@ -166,4 +166,4 @@ def test_update_path(self, _update_existing_client):
166166
result = runner.invoke(app, ["update", f"--path={path}"])
167167

168168
assert result.exit_code == 0
169-
_update_existing_client.assert_called_once_with(url=None, path=PosixPath(path))
169+
_update_existing_client.assert_called_once_with(url=None, path=Path(path))

tests/test_openapi_parser/test_openapi.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,11 @@ def test_from_data_property_parse_error(self, mocker):
105105

106106
assert result == parse_error
107107

108-
def test_from_data_parse_error_on_reference(self):
109-
from openapi_python_client.parser.openapi import Model
110-
111-
data = oai.Reference.construct()
112-
assert Model.from_data(data=data, name="") == ParseError(
113-
data=data, detail="Reference schemas are not supported."
114-
)
115-
116108

117109
class TestSchemas:
118110
def test_build(self, mocker):
119111
from_data = mocker.patch(f"{MODULE_NAME}.Model.from_data")
120-
in_data = {1: mocker.MagicMock(), 2: mocker.MagicMock(), 3: mocker.MagicMock()}
112+
in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)}
121113
schema_1 = mocker.MagicMock()
122114
schema_2 = mocker.MagicMock()
123115
error = ParseError()
@@ -132,6 +124,14 @@ def test_build(self, mocker):
132124
models={schema_1.reference.class_name: schema_1, schema_2.reference.class_name: schema_2,}, errors=[error]
133125
)
134126

127+
def test_build_parse_error_on_reference(self):
128+
from openapi_python_client.parser.openapi import Schemas
129+
130+
ref_schema = oai.Reference.construct()
131+
in_data = {1: ref_schema}
132+
result = Schemas.build(schemas=in_data)
133+
assert result.errors[0] == ParseError(data=ref_schema, detail="Reference schemas are not supported.")
134+
135135

136136
class TestEndpoint:
137137
def test_parse_request_form_body(self, mocker):

0 commit comments

Comments
 (0)