diff --git a/README.rst b/README.rst index 0aece3ad..e508646f 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ function should look like: app = Minik() @app.route('/hello/{name}') - def hello_view(name): + def hello_view(name: str): if name == 'FINDME': # Returns a 400 status code with the message as the body. @@ -52,7 +52,7 @@ define the methods, every single HTTP method will be allowed by default. app = Minik() @app.route('/events/{location}') - def events_view(location): + def events_view(location: str): # This route will be invoked for GET, POST, PUT, DELETE... return {'data': ['granfondo MD', 'Silver Spring Century']} @@ -61,6 +61,23 @@ define the methods, every single HTTP method will be allowed by default. create_event(app.request.json_body) return {'result': 'complete'} +The microframework also includes a set of convenient decorator methods for the +case in which a view is associated with a single HTTP method. + +.. code-block:: python + + from minik.core import Minik + + app = Minik() + + @app.get('/events/{location}') + def get_view(location: str): + return {'data': ['granfondo MD', 'Silver Spring Century']} + + @app.post('/events') + def post_view(): + create_event(app.request.json_body) + return {'result': 'complete'} Route Parameter Validation ************************** diff --git a/docs/index.rst b/docs/index.rst index e309b356..548bd96c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ define the methods, every single HTTP method will be allowed by default. app = Minik() @app.route('/events/{location}') - def events_view(location): + def events_view(location: str): # This route will be invoked for GET, POST, PUT, DELETE... return {'data': ['granfondo MD', 'Silver Spring Century']} @@ -47,6 +47,25 @@ define the methods, every single HTTP method will be allowed by default. create_event(app.request.json_body) return {'result': 'complete'} +The microframework also includes a set of convenient decorator methods for the +case in which a view is associated with a single HTTP method. + +.. code-block:: python + + from minik.core import Minik + + app = Minik() + + @app.get('/events/{location}') + def get_view(location: str): + return {'data': ['granfondo MD', 'Silver Spring Century']} + + @app.post('/events') + def post_view(): + create_event(app.request.json_body) + return {'result': 'complete'} + + Route Validation **************** Using the `function annotations`_, you can specify the type of value you are expecting diff --git a/minik/core.py b/minik/core.py index 080fce1b..c9422cc4 100644 --- a/minik/core.py +++ b/minik/core.py @@ -42,12 +42,26 @@ def sample_view(event_type): """ - def __init__(self): + def __init__(self, **kwargs): self._routes = defaultdict(list) + def get(self, path, **kwargs): + return self.route(path, methods=['GET'], **kwargs) + + def post(self, path, **kwargs): + return self.route(path, methods=['POST'], **kwargs) + + def put(self, path, **kwargs): + return self.route(path, methods=['PUT'], **kwargs) + + def delete(self, path, **kwargs): + return self.route(path, methods=['DELETE'], **kwargs) + def route(self, path, **kwargs): """ The decorator function used to associate a given route with a view function. + + :param path: The endpoint associated with a given view. """ def _register_view(view_func): @@ -86,7 +100,7 @@ def __call__(self, event, context): response = JsonResponse({'error_message': str(pe)}, status_code=pe.status_code) except Exception as te: tracer = ''.join(traceback.format_exc()) - # self.logger.error(tracer) + print(tracer) response = JsonResponse({'error_message': str(te), 'trace': tracer}, status_code=500) return response.to_dict() diff --git a/tests/test_routing_decorators.py b/tests/test_routing_decorators.py new file mode 100644 index 00000000..f17bafb2 --- /dev/null +++ b/tests/test_routing_decorators.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +""" + test_routing_decorators.py + :copyright: © 2019 by the EAB Tech team. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import json +import pytest +from unittest.mock import MagicMock +from minik.core import Minik + + +sample_app = Minik() +context = MagicMock() + + +@sample_app.route('/activity_no_method') +def no_method_view(): + return {'method': sample_app.request.method.lower()} + + +@sample_app.post('/activity') +def post_view(): + return {'id': 2, 'message': 'success'} + + +@sample_app.get('/activity/{activity_id}', validators=[True]) +def get_view(activity_id: int): + return {'id': activity_id, 'type': 'cycling '} + + +@sample_app.put('/activity/{activity_id}') +def put_view(activity_id: int): + return {'updated': True} + + +@sample_app.delete('/activity/{activity_id}') +def delete_view(activity_id: int): + return {'removed': True} + + +def test_routing_for_http_post(create_router_event): + + event = create_router_event('/activity', + method='POST', + body={'type': 'cycle', 'distance': 15}) + + response = sample_app(event, context) + expected_response = post_view() + + assert response['body'] == json.dumps(expected_response) + + +@pytest.mark.parametrize("http_method, expected_response", [ + ('GET', get_view(123)), + ('PUT', put_view(123)), + ('DELETE', delete_view(123)), +]) +def test_routing_for_http_get(create_router_event, http_method, expected_response): + """ + Validate that a view defined for a GET request is correctly evaluated when + the route + method match the signature. + """ + + event = create_router_event('/activity/{activity_id}', + method=http_method, + pathParameters={'activity_id': 123}) + + response = sample_app(event, context) + + assert response['body'] == json.dumps(expected_response)