Skip to content

Improve error message when OpenAPI document fails validation #131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 0.5.1 - Unreleased
### Fixes
- Relative paths are now allowed in securitySchemes/OAuthFlow/tokenUrl (#130).
- Schema validation errors will no longer print a stack trace (#131).


## 0.5.0 - 2020-08-05
Expand Down
10 changes: 8 additions & 2 deletions openapi_python_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, Optional, Sequence
from typing import Any, Dict, Optional, Sequence, Union

import httpx
import yaml
Expand All @@ -25,9 +25,11 @@
__version__ = version(__package__)


def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> _Project:
def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> Union[_Project, GeneratorError]:
data_dict = _get_document(url=url, path=path)
openapi = GeneratorData.from_dict(data_dict)
if isinstance(openapi, GeneratorError):
return openapi
return _Project(openapi=openapi)


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


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


Expand Down
11 changes: 8 additions & 3 deletions openapi_python_client/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from enum import Enum
from typing import Any, Dict, List, Optional, Set, Union

from pydantic import ValidationError

from .. import schema as oai
from .errors import ParseError, PropertyError
from .errors import GeneratorError, ParseError, PropertyError
from .properties import EnumProperty, Property, property_from_data
from .reference import Reference
from .responses import ListRefResponse, RefResponse, Response, response_from_data
Expand Down Expand Up @@ -288,9 +290,12 @@ class GeneratorData:
enums: Dict[str, EnumProperty]

@staticmethod
def from_dict(d: Dict[str, Dict[str, Any]]) -> GeneratorData:
def from_dict(d: Dict[str, Dict[str, Any]]) -> Union[GeneratorData, GeneratorError]:
""" Create an OpenAPI from dict """
openapi = oai.OpenAPI.parse_obj(d)
try:
openapi = oai.OpenAPI.parse_obj(d)
except ValidationError as e:
return GeneratorError(header="Failed to parse OpenAPI document", detail=str(e))
if openapi.components is None or openapi.components.schemas is None:
schemas = Schemas()
else:
Expand Down
57 changes: 55 additions & 2 deletions tests/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ def test__get_project_for_url_or_path(mocker):
assert project == _Project()


def test__get_project_for_url_or_path_generator_error(mocker):
data_dict = mocker.MagicMock()
_get_document = mocker.patch("openapi_python_client._get_document", return_value=data_dict)
error = GeneratorError()
from_dict = mocker.patch("openapi_python_client.parser.GeneratorData.from_dict", return_value=error)
_Project = mocker.patch("openapi_python_client._Project")
url = mocker.MagicMock()
path = mocker.MagicMock()

from openapi_python_client import _get_project_for_url_or_path

project = _get_project_for_url_or_path(url=url, path=path)

_get_document.assert_called_once_with(url=url, path=path)
from_dict.assert_called_once_with(data_dict)
_Project.assert_not_called()
assert project == error


def test_create_new_client(mocker):
project = mocker.MagicMock()
_get_project_for_url_or_path = mocker.patch(
Expand All @@ -35,10 +54,27 @@ def test_create_new_client(mocker):

from openapi_python_client import create_new_client

create_new_client(url=url, path=path)
result = create_new_client(url=url, path=path)

_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
project.build.assert_called_once()
assert result == project.build.return_value


def test_create_new_client_project_error(mocker):
error = GeneratorError()
_get_project_for_url_or_path = mocker.patch(
"openapi_python_client._get_project_for_url_or_path", return_value=error
)
url = mocker.MagicMock()
path = mocker.MagicMock()

from openapi_python_client import create_new_client

result = create_new_client(url=url, path=path)

_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
assert result == [error]


def test_update_existing_client(mocker):
Expand All @@ -51,10 +87,27 @@ def test_update_existing_client(mocker):

from openapi_python_client import update_existing_client

update_existing_client(url=url, path=path)
result = update_existing_client(url=url, path=path)

_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
project.update.assert_called_once()
assert result == project.update.return_value


def test_update_existing_client_project_error(mocker):
error = GeneratorError()
_get_project_for_url_or_path = mocker.patch(
"openapi_python_client._get_project_for_url_or_path", return_value=error
)
url = mocker.MagicMock()
path = mocker.MagicMock()

from openapi_python_client import update_existing_client

result = update_existing_client(url=url, path=path)

_get_project_for_url_or_path.assert_called_once_with(url=url, path=path)
assert result == [error]


class TestGetJson:
Expand Down
26 changes: 26 additions & 0 deletions tests/test_openapi_parser/test_openapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from pydantic import ValidationError
from pydantic.error_wrappers import ErrorWrapper

import openapi_python_client.schema as oai
from openapi_python_client import GeneratorError
from openapi_python_client.parser.errors import ParseError

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

def test_from_dict_invalid_schema(self, mocker):
Schemas = mocker.patch(f"{MODULE_NAME}.Schemas")

in_dict = {}

from openapi_python_client.parser.openapi import GeneratorData

generator_data = GeneratorData.from_dict(in_dict)

assert generator_data == GeneratorError(
header="Failed to parse OpenAPI document",
detail=(
"2 validation errors for OpenAPI\n"
"info\n"
" field required (type=value_error.missing)\n"
"paths\n"
" field required (type=value_error.missing)"
),
)
Schemas.build.assert_not_called()
Schemas.assert_not_called()


class TestModel:
def test_from_data(self, mocker):
Expand Down