Skip to content

Commit dd59e4c

Browse files
committedAug 5, 2020
Improve error message when OpenAPI document fails validation (closes #129)
1 parent 15c2fd5 commit dd59e4c

File tree

5 files changed

+98
-7
lines changed

5 files changed

+98
-7
lines changed
 

‎CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## 0.5.1 - Unreleased
99
### Fixes
1010
- Relative paths are now allowed in securitySchemes/OAuthFlow/tokenUrl (#130).
11+
- Schema validation errors will no longer print a stack trace (#131).
1112

1213

1314
## 0.5.0 - 2020-08-05

‎openapi_python_client/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import subprocess
66
import sys
77
from pathlib import Path
8-
from typing import Any, Dict, Optional, Sequence
8+
from typing import Any, Dict, Optional, Sequence, Union
99

1010
import httpx
1111
import yaml
@@ -25,9 +25,11 @@
2525
__version__ = version(__package__)
2626

2727

28-
def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> _Project:
28+
def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> Union[_Project, GeneratorError]:
2929
data_dict = _get_document(url=url, path=path)
3030
openapi = GeneratorData.from_dict(data_dict)
31+
if isinstance(openapi, GeneratorError):
32+
return openapi
3133
return _Project(openapi=openapi)
3234

3335

@@ -50,6 +52,8 @@ def create_new_client(*, url: Optional[str], path: Optional[Path]) -> Sequence[G
5052
A list containing any errors encountered when generating.
5153
"""
5254
project = _get_project_for_url_or_path(url=url, path=path)
55+
if isinstance(project, GeneratorError):
56+
return [project]
5357
return project.build()
5458

5559

@@ -61,6 +65,8 @@ def update_existing_client(*, url: Optional[str], path: Optional[Path]) -> Seque
6165
A list containing any errors encountered when generating.
6266
"""
6367
project = _get_project_for_url_or_path(url=url, path=path)
68+
if isinstance(project, GeneratorError):
69+
return [project]
6470
return project.update()
6571

6672

‎openapi_python_client/parser/openapi.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from enum import Enum
66
from typing import Any, Dict, List, Optional, Set, Union
77

8+
from pydantic import ValidationError
9+
810
from .. import schema as oai
9-
from .errors import ParseError, PropertyError
11+
from .errors import GeneratorError, ParseError, PropertyError
1012
from .properties import EnumProperty, Property, property_from_data
1113
from .reference import Reference
1214
from .responses import ListRefResponse, RefResponse, Response, response_from_data
@@ -288,9 +290,12 @@ class GeneratorData:
288290
enums: Dict[str, EnumProperty]
289291

290292
@staticmethod
291-
def from_dict(d: Dict[str, Dict[str, Any]]) -> GeneratorData:
293+
def from_dict(d: Dict[str, Dict[str, Any]]) -> Union[GeneratorData, GeneratorError]:
292294
""" Create an OpenAPI from dict """
293-
openapi = oai.OpenAPI.parse_obj(d)
295+
try:
296+
openapi = oai.OpenAPI.parse_obj(d)
297+
except ValidationError as e:
298+
return GeneratorError(header="Failed to parse OpenAPI document", detail=str(e))
294299
if openapi.components is None or openapi.components.schemas is None:
295300
schemas = Schemas()
296301
else:

‎tests/test___init__.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ def test__get_project_for_url_or_path(mocker):
2525
assert project == _Project()
2626

2727

28+
def test__get_project_for_url_or_path_generator_error(mocker):
29+
data_dict = mocker.MagicMock()
30+
_get_document = mocker.patch("openapi_python_client._get_document", return_value=data_dict)
31+
error = GeneratorError()
32+
from_dict = mocker.patch("openapi_python_client.parser.GeneratorData.from_dict", return_value=error)
33+
_Project = mocker.patch("openapi_python_client._Project")
34+
url = mocker.MagicMock()
35+
path = mocker.MagicMock()
36+
37+
from openapi_python_client import _get_project_for_url_or_path
38+
39+
project = _get_project_for_url_or_path(url=url, path=path)
40+
41+
_get_document.assert_called_once_with(url=url, path=path)
42+
from_dict.assert_called_once_with(data_dict)
43+
_Project.assert_not_called()
44+
assert project == error
45+
46+
2847
def test_create_new_client(mocker):
2948
project = mocker.MagicMock()
3049
_get_project_for_url_or_path = mocker.patch(
@@ -35,10 +54,27 @@ def test_create_new_client(mocker):
3554

3655
from openapi_python_client import create_new_client
3756

38-
create_new_client(url=url, path=path)
57+
result = create_new_client(url=url, path=path)
3958

4059
_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
4160
project.build.assert_called_once()
61+
assert result == project.build.return_value
62+
63+
64+
def test_create_new_client_project_error(mocker):
65+
error = GeneratorError()
66+
_get_project_for_url_or_path = mocker.patch(
67+
"openapi_python_client._get_project_for_url_or_path", return_value=error
68+
)
69+
url = mocker.MagicMock()
70+
path = mocker.MagicMock()
71+
72+
from openapi_python_client import create_new_client
73+
74+
result = create_new_client(url=url, path=path)
75+
76+
_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
77+
assert result == [error]
4278

4379

4480
def test_update_existing_client(mocker):
@@ -51,10 +87,27 @@ def test_update_existing_client(mocker):
5187

5288
from openapi_python_client import update_existing_client
5389

54-
update_existing_client(url=url, path=path)
90+
result = update_existing_client(url=url, path=path)
5591

5692
_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
5793
project.update.assert_called_once()
94+
assert result == project.update.return_value
95+
96+
97+
def test_update_existing_client_project_error(mocker):
98+
error = GeneratorError()
99+
_get_project_for_url_or_path = mocker.patch(
100+
"openapi_python_client._get_project_for_url_or_path", return_value=error
101+
)
102+
url = mocker.MagicMock()
103+
path = mocker.MagicMock()
104+
105+
from openapi_python_client import update_existing_client
106+
107+
result = update_existing_client(url=url, path=path)
108+
109+
_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
110+
assert result == [error]
58111

59112

60113
class TestGetJson:

‎tests/test_openapi_parser/test_openapi.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
from pydantic import ValidationError
2+
from pydantic.error_wrappers import ErrorWrapper
3+
14
import openapi_python_client.schema as oai
5+
from openapi_python_client import GeneratorError
26
from openapi_python_client.parser.errors import ParseError
37

48
MODULE_NAME = "openapi_python_client.parser.openapi"
@@ -40,6 +44,28 @@ def test_from_dict(self, mocker):
4044
Schemas.build.assert_not_called()
4145
assert generator_data.schemas == Schemas()
4246

47+
def test_from_dict_invalid_schema(self, mocker):
48+
Schemas = mocker.patch(f"{MODULE_NAME}.Schemas")
49+
50+
in_dict = {}
51+
52+
from openapi_python_client.parser.openapi import GeneratorData
53+
54+
generator_data = GeneratorData.from_dict(in_dict)
55+
56+
assert generator_data == GeneratorError(
57+
header="Failed to parse OpenAPI document",
58+
detail=(
59+
"2 validation errors for OpenAPI\n"
60+
"info\n"
61+
" field required (type=value_error.missing)\n"
62+
"paths\n"
63+
" field required (type=value_error.missing)"
64+
),
65+
)
66+
Schemas.build.assert_not_called()
67+
Schemas.assert_not_called()
68+
4369

4470
class TestModel:
4571
def test_from_data(self, mocker):

0 commit comments

Comments
 (0)