Skip to content
This repository has been archived by the owner on Mar 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #100 from lendingblock/spec-tests
Browse files Browse the repository at this point in the history
Making doc requirements on openapi endpoints docs
  • Loading branch information
lsbardel authored Aug 15, 2018
2 parents 47e5da6 + a154e05 commit b80923c
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 107 deletions.
2 changes: 1 addition & 1 deletion openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Minimal OpenAPI asynchronous server application
"""

__version__ = '0.7.6'
__version__ = '0.7.7'
2 changes: 1 addition & 1 deletion openapi/data/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class FieldError(ErrorMessage):
class ValidationErrors(ErrorMessage):
"""Error message and list of errors for data fields
"""
errors: List[FieldError]
errors: List[FieldError] = data_field(description='List of field errors')


def error_response_schema(status):
Expand Down
6 changes: 6 additions & 0 deletions openapi/spec/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ class InvalidTypeException(Exception):

def __init__(self, field_type):
super().__init__(f'Cannot parse type {field_type}')


class InvalidSpecException(Exception):

def __init__(self, message):
super().__init__(message)
70 changes: 54 additions & 16 deletions openapi/spec/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from aiohttp import web
from dataclasses import dataclass, asdict, is_dataclass, field

from .exceptions import InvalidTypeException
from .exceptions import InvalidTypeException, InvalidSpecException
from .path import ApiPath
from .utils import load_yaml_from_docstring, trim_docstring
from ..data import fields
Expand Down Expand Up @@ -77,32 +77,38 @@ def parameters(self, Schema, default_in='path'):
params.append(entry)
return params

def field2json(self, field):
def field2json(self, field, validate_info=True):
field = fields.as_field(field)
mapping = self._fields_mapping.get(field.type, None)
enum = None
if not mapping:
if is_subclass(field.type, Enum):
mapping = dict(type='string')
enum = [e.name for e in field.type]
json_property = {'type': 'string', 'enum': enum}
elif is_subclass(field.type, List):
return self._list2json(field.type)
json_property = self._list2json(field.type)
elif is_subclass(field.type, Dict):
return self._map2json(field.type)
json_property = self._map2json(field.type)
elif is_dataclass(field.type):
return self.get_schema_ref(field.type)
json_property = self.get_schema_ref(field.type)
else:
raise InvalidTypeException(field.type)

json_property = {'type': mapping['type']}
mapping = {}
else:
json_property = {'type': mapping['type']}

meta = field.metadata
if meta.get(fields.DESCRIPTION):
json_property['description'] = meta.get(fields.DESCRIPTION)
field_description = meta.get(fields.DESCRIPTION)
if not field_description:
if validate_info:
raise InvalidSpecException(
f'Missing description for field {field.name}'
)
else:
json_property['description'] = field_description
fmt = meta.get(fields.FORMAT) or mapping.get(fields.FORMAT, None)
if fmt:
json_property[fields.FORMAT] = fmt
if enum:
json_property['enum'] = enum
validator = meta.get(fields.VALIDATOR)
# add additional parameters fields from validators
if isinstance(validator, fields.Validator):
Expand Down Expand Up @@ -140,7 +146,8 @@ def _list2json(self, field_type):
args = field_type.__args__
return {
'type': 'array',
'items': self.field2json(args[0]) if args else {'type': 'object'}
'items':
self.field2json(args[0], False) if args else {'type': 'object'}
}

def _map2json(self, field_type):
Expand All @@ -151,7 +158,7 @@ def _map2json(self, field_type):
if args:
if len(args) != 2 or args[0] != str:
raise InvalidTypeException(field_type)
spec['additionalProperties'] = self.field2json(args[1])
spec['additionalProperties'] = self.field2json(args[1], False)
return spec


Expand All @@ -174,7 +181,7 @@ class OpenApiSpec:
"""Open API document builder
"""
def __init__(self, info, default_content_type=None,
default_responses=None):
default_responses=None, allowed_tags=None):
self.schemas = {}
self.parameters = {}
self.responses = {}
Expand All @@ -189,6 +196,7 @@ def __init__(self, info, default_content_type=None,
paths=OrderedDict()
)
self.schemas_to_parse = set()
self.allowed_tags = allowed_tags

@property
def paths(self):
Expand Down Expand Up @@ -240,9 +248,24 @@ def _build_paths(self, app, public, private):
handler, app, public, private
)

self._validate_tags()

def _validate_tags(self):
for tag_name, tag_obj in self.tags.items():
if self.allowed_tags and tag_name not in self.allowed_tags:
raise InvalidSpecException(f'Tag {tag_name} not allowed')
if 'description' not in tag_obj:
raise InvalidSpecException(
f'Missing tag {tag_name} description'
)

def _build_path_object(self, handler, path_obj, public, private):
path_obj = load_yaml_from_docstring(handler.__doc__) or {}
tags = self._extend_tags(path_obj.pop('tags', None))
doc_tags = path_obj.pop('tags', None)
if not doc_tags:
raise InvalidSpecException(f'Missing tags docstring for {handler}')

tags = self._extend_tags(doc_tags)
if handler.path_schema:
p = SchemaParser()
path_obj['parameters'] = p.parameters(handler.path_schema)
Expand All @@ -269,6 +292,8 @@ def _build_path_object(self, handler, path_obj, public, private):
self._get_response_object(op_attrs, method_doc)
self._get_request_body_object(op_attrs, method_doc)
self._get_query_parameters(op_attrs, method_doc)
method_info = self._get_method_info(method_handler, method_doc)
method_doc.update(method_info)
method_doc['tags'] = list(mtags)
path_obj[method] = method_doc

Expand All @@ -285,6 +310,19 @@ def _get_schema_info(self, schema):
info['$ref'] = f'{SCHEMA_BASE_REF}{schema.__name__}'
return info

def _get_method_info(self, method_handler, method_doc):
summary = method_doc.get('summary')
if not summary:
raise InvalidSpecException(
f'Missing method summary for {method_handler}'
)
description = method_doc.get('description')
if not description:
raise InvalidSpecException(
f'Missing method description for {method_handler}'
)
return {'summary': summary, 'description': description}

def _get_response_object(self, op_attrs, doc):
response_schema = op_attrs.get('response_schema', None)
if response_schema is None:
Expand Down
Loading

0 comments on commit b80923c

Please sign in to comment.