diff --git a/halfapi/half_domain.py b/halfapi/half_domain.py index 19c09c1..f8dc2ae 100644 --- a/halfapi/half_domain.py +++ b/halfapi/half_domain.py @@ -24,6 +24,7 @@ from .half_route import HalfRoute from .lib import acl as lib_acl from .lib.responses import PlainTextResponse from .lib.routes import JSONRoute +from .lib.schemas import param_docstring_default from .lib.domain import MissingAclError, PathError, UnknownPathParameterType, \ UndefinedRoute, UndefinedFunction, get_fct_name, route_decorator from .lib.domain_middleware import DomainMiddleware @@ -207,7 +208,8 @@ class HalfDomain(Starlette): def gen_routes(m_router: ModuleType, verb: str, path: List[str], - params: List[Dict]) -> Tuple[FunctionType, Dict]: + params: List[Dict], + path_param_docstrings: Dict[str, str] = {}) -> Tuple[FunctionType, Dict]: """ Returns a tuple of the function associatied to the verb and path arguments, and the dictionary of it's acls @@ -239,6 +241,13 @@ class HalfDomain(Starlette): fct_name = get_fct_name(verb, path[-1]) if hasattr(m_router, fct_name): fct = getattr(m_router, fct_name) + fct_docstring_obj = yaml.safe_load(fct.__doc__) + if 'parameters' not in fct_docstring_obj and path_param_docstrings: + fct_docstring_obj['parameters'] = list(map( + yaml.safe_load, + path_param_docstrings.values())) + + fct.__doc__ = yaml.dump(fct_docstring_obj) else: raise UndefinedFunction('{}.{}'.format(m_router.__name__, fct_name or '')) @@ -251,7 +260,7 @@ class HalfDomain(Starlette): @staticmethod - def gen_router_routes(m_router, path: List[str]) -> \ + def gen_router_routes(m_router, path: List[str], PATH_PARAMS={}) -> \ Iterator[Tuple[str, str, ModuleType, Coroutine, List]]: """ Recursive generator that parses a router (or a subrouter) @@ -279,17 +288,32 @@ class HalfDomain(Starlette): yield ('/'.join(filter(lambda x: len(x) > 0, path)), verb, m_router, - *HalfDomain.gen_routes(m_router, verb, path, params[verb]) + *HalfDomain.gen_routes(m_router, verb, path, params[verb], PATH_PARAMS) ) for subroute in params.get('SUBROUTES', []): - #logger.debug('Processing subroute **%s** - %s', subroute, m_router.__name__) + subroute_module = importlib.import_module(f'.{subroute}', m_router.__name__) param_match = re.fullmatch('^([A-Z_]+)_([a-z]+)$', subroute) + parameter_name = None if param_match is not None: try: + parameter_name = param_match.groups()[0].lower() + if parameter_name in PATH_PARAMS: + raise Exception(f'Duplicate parameter name in same path! {subroute} : {parameter_name}') + + parameter_type = param_match.groups()[1] path.append('{{{}:{}}}'.format( - param_match.groups()[0].lower(), - param_match.groups()[1])) + parameter_name, + parameter_type, + ) + ) + + + try: + PATH_PARAMS[parameter_name] = subroute_module.param_docstring + except AttributeError as exc: + PATH_PARAMS[parameter_name] = param_docstring_default(parameter_name, parameter_type) + except AssertionError as exc: raise UnknownPathParameterType(subroute) from exc else: @@ -297,14 +321,19 @@ class HalfDomain(Starlette): try: yield from HalfDomain.gen_router_routes( - importlib.import_module(f'.{subroute}', m_router.__name__), - path) + subroute_module, + path, + PATH_PARAMS + ) except ImportError as exc: logger.error('Failed to import subroute **{%s}**', subroute) raise exc path.pop() + if parameter_name: + PATH_PARAMS.pop(parameter_name) + path.pop() diff --git a/halfapi/lib/schemas.py b/halfapi/lib/schemas.py index eb243d2..5a98422 100644 --- a/halfapi/lib/schemas.py +++ b/halfapi/lib/schemas.py @@ -13,6 +13,7 @@ import os import importlib from typing import Dict, Coroutine, List from types import ModuleType +import yaml from starlette.schemas import SchemaGenerator @@ -114,3 +115,23 @@ def schema_csv_dict(csv: List[str], prefix='/') -> Dict: }) return schema_d + +def param_docstring_default(name, type): + """ Returns a default docstring in OpenAPI format for a path parameter + """ + type_map = { + 'str': 'string', + 'uuid': 'string', + 'path': 'string', + 'int': 'number', + 'float': 'number' + } + return yaml.dump({ + 'name': name, + 'in': 'path', + 'description': f'default description for path parameter {name}', + 'required': True, + 'schema': { + 'type': type_map[type] + } + }) diff --git a/tests/dummy_domain/routers/path_params/FIRST_str/__init__.py b/tests/dummy_domain/routers/path_params/FIRST_str/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/__init__.py b/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/__init__.py new file mode 100644 index 0000000..de6cdb4 --- /dev/null +++ b/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/__init__.py @@ -0,0 +1,8 @@ +param_docstring = """ +name: second +in: path +description: second parameter description test +required: true +schema: + type: string +""" diff --git a/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/two/THIRD_int/__init__.py b/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/two/THIRD_int/__init__.py new file mode 100644 index 0000000..7530ad6 --- /dev/null +++ b/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/two/THIRD_int/__init__.py @@ -0,0 +1,20 @@ +from uuid import UUID +from halfapi.lib import acl +ACLS = { + 'GET': [{'acl': acl.public}] +} + +def get(first, second, third): + """ + description: a Test route for path parameters + responses: + 200: + description: The test passed! + 500: + description: The test did not pass :( + """ + assert isintance(first, str) + assert isintance(second, UUID) + assert isintance(third, int) + + return '' diff --git a/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/two/__init__.py b/tests/dummy_domain/routers/path_params/FIRST_str/one/SECOND_uuid/two/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy_domain/routers/path_params/FIRST_str/one/__init__.py b/tests/dummy_domain/routers/path_params/FIRST_str/one/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy_domain/routers/path_params/__init__.py b/tests/dummy_domain/routers/path_params/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_domain.py b/tests/test_domain.py index 00b18ce..b569c46 100644 --- a/tests/test_domain.py +++ b/tests/test_domain.py @@ -1,6 +1,8 @@ import pytest from halfapi.testing.test_domain import TestDomain from pprint import pprint +import logging +logger = logging.getLogger() class TestDummyDomain(TestDomain): from .dummy_domain import domain @@ -77,3 +79,28 @@ class TestDummyDomain(TestDomain): res = self.client.request('post', '/arguments', json={ **arg_dict, 'z': True}) assert res.json() == {**arg_dict, 'z': True} + + def test_schema_path_params(self): + res = self.client.request('get', '/halfapi/schema') + schema = res.json() + + logger.debug(schema) + + assert len(schema['paths']) > 0 + route = schema['paths']['/path_params/{first}/one/{second}/two/{third}'] + + assert 'parameters' in route['get'] + parameters = route['get']['parameters'] + + assert len(parameters) == 3 + + param_map = { + elt['name']: elt + for elt in parameters + } + + assert param_map['second']['description'] == 'second parameter description test' + + + +