Skip to content

Commit

Permalink
Merge pull request #107 from kleijnweb/feature/issue-98
Browse files Browse the repository at this point in the history
Sensible defaults for controller resolution
  • Loading branch information
hjacobs committed Dec 8, 2015
2 parents cb6e61b + 47db7a7 commit a7e42fe
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 72 deletions.
1 change: 1 addition & 0 deletions connexion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
from .problem import problem
from .decorators.produces import NoContent
import werkzeug.exceptions as exceptions
from .resolver import *

__version__ = '0.13'
10 changes: 5 additions & 5 deletions connexion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import yaml
from .operation import Operation
from . import utils
from . import resolver

MODULE_PATH = pathlib.Path(__file__).absolute().parent
SWAGGER_UI_PATH = MODULE_PATH / 'vendor' / 'swagger-ui'
Expand All @@ -33,7 +34,7 @@ class Api:
"""

def __init__(self, swagger_yaml_path, base_url=None, arguments=None, swagger_ui=None, swagger_path=None,
swagger_url=None, validate_responses=False, resolver=utils.get_function_from_name):
swagger_url=None, validate_responses=False, resolver=resolver.Resolver()):
"""
:type swagger_yaml_path: pathlib.Path
:type base_url: str | None
Expand Down Expand Up @@ -110,10 +111,9 @@ def add_operation(self, method, path, swagger_operation):
:type path: str
:type swagger_operation: dict
"""
operation = Operation(method=method, path=path, operation=swagger_operation,
app_produces=self.produces, app_security=self.security,
security_definitions=self.security_definitions, definitions=self.definitions,
parameter_definitions=self.parameter_definitions,
operation = Operation(method=method, path=path, operation=swagger_operation, app_produces=self.produces,
app_security=self.security, security_definitions=self.security_definitions,
definitions=self.definitions, parameter_definitions=self.parameter_definitions,
validate_responses=self.validate_responses, resolver=self.resolver)
operation_id = operation.operation_id
logger.debug('... Adding %s -> %s', method.upper(), operation_id, extra=vars(operation))
Expand Down
11 changes: 7 additions & 4 deletions connexion/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@

import logging
import pathlib

import flask
import werkzeug.exceptions

from .problem import problem
from .api import Api
from .utils import get_function_from_name
from connexion.resolver import Resolver

logger = logging.getLogger('connexion.app')

Expand Down Expand Up @@ -68,6 +66,7 @@ def __init__(self, import_name, port=None, specification_dir='', server=None, ar
self.port = port
self.server = server or 'flask'
self.debug = debug
self.import_name = import_name
self.arguments = arguments or {}
self.swagger_ui = swagger_ui
self.swagger_path = swagger_path
Expand All @@ -83,7 +82,7 @@ def common_error_handler(exception):
return problem(title=exception.name, detail=exception.description, status=exception.code)

def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None, swagger_path=None,
swagger_url=None, validate_responses=False, resolver=get_function_from_name):
swagger_url=None, validate_responses=False, resolver=Resolver()):
"""
Adds an API to the application based on a swagger file
Expand All @@ -101,8 +100,12 @@ def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None,
:type swagger_url: string | None
:param validate_responses: True enables validation. Validation errors generate HTTP 500 responses.
:type validate_responses: bool
:param resolver: Operation resolver.
:type resolver: Resolver | types.FunctionType
:rtype: Api
"""
resolver = Resolver(resolver) if hasattr(resolver, '__call__') else resolver

swagger_ui = swagger_ui if swagger_ui is not None else self.swagger_ui
swagger_path = swagger_path if swagger_path is not None else self.swagger_path
swagger_url = swagger_url if swagger_url is not None else self.swagger_url
Expand Down
23 changes: 8 additions & 15 deletions connexion/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .decorators.security import security_passthrough, verify_oauth
from .decorators.validation import RequestBodyValidator, ParameterValidator
from .decorators.metrics import UWSGIMetricsCollector
from.decorators.response import ResponseValidator
from .decorators.response import ResponseValidator
from .exceptions import InvalidSpecification
from .utils import flaskify_endpoint, produces_json

Expand All @@ -31,8 +31,8 @@ class Operation:
A single API operation on a path.
"""

def __init__(self, method, path, operation, app_produces, app_security,
security_definitions, definitions, parameter_definitions, resolver, validate_responses=False):
def __init__(self, method, path, operation, app_produces, app_security, security_definitions, definitions,
parameter_definitions, resolver, validate_responses=False):
"""
This class uses the OperationID identify the module and function that will handle the operation
Expand Down Expand Up @@ -74,25 +74,18 @@ def __init__(self, method, path, operation, app_produces, app_security,
'parameters': self.parameter_definitions
}
self.validate_responses = validate_responses

self.operation = operation
operation_id = operation['operationId']

router_controller = operation.get('x-swagger-router-controller')

self.operation_id = self.detect_controller(operation_id, router_controller)
# todo support definition references
# todo support references to application level parameters
self.parameters = list(self.resolve_parameters(operation.get('parameters', [])))
self.produces = operation.get('produces', app_produces)
self.endpoint_name = flaskify_endpoint(self.operation_id)
self.security = operation.get('security', app_security)
self.__undecorated_function = resolver(self.operation_id)
self.produces = operation.get('produces', app_produces)

def detect_controller(self, operation_id, router_controller):
if router_controller is None:
return operation_id
return router_controller + '.' + operation_id
resolution = resolver.resolve(self)
self.operation_id = resolution.operation_id
self.endpoint_name = flaskify_endpoint(self.operation_id)
self.__undecorated_function = resolution.function

def resolve_reference(self, schema):
schema = schema.copy() # avoid changing the original schema
Expand Down
132 changes: 132 additions & 0 deletions connexion/resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""
Copyright 2015 Zalando SE
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 logging
import re
import connexion.utils as utils

logger = logging.getLogger('connexion.resolver')


class Resolution:
def __init__(self, function, operation_id):
"""
Represents the result of operation resolution
:param function: The endpoint function
:type function: types.FunctionType
"""
self.function = function
self.operation_id = operation_id


class Resolver:
def __init__(self, function_resolver=utils.get_function_from_name):
"""
Standard resolver
:param function_resolver: Function that resolves functions using an operationId
:type function_resolver: types.FunctionType
"""
self.function_resolver = function_resolver

def resolve(self, operation):
"""
Default operation resolver
:type operation: connexion.operation.Operation
"""
operation_id = self.resolve_operation_id(operation)
return Resolution(self.resolve_function_from_operation_id(operation_id), operation_id)

def resolve_operation_id(self, operation):
"""
Default operationId resolver
:type operation: connexion.operation.Operation
"""
spec = operation.operation
operation_id = spec.get('operationId')
x_router_controller = spec.get('x-swagger-router-controller')
if x_router_controller is None:
return operation_id
return x_router_controller + '.' + operation_id

def resolve_function_from_operation_id(self, operation_id):
"""
Invokes the function_resolver
:type operation_id: str
"""
return self.function_resolver(operation_id)


class RestyResolver(Resolver):
"""
Resolves endpoint functions using REST semantics (unless overridden by specifying operationId)
"""

def __init__(self, default_module_name, collection_endpoint_name='search'):
"""
:param default_module_name: Default module name for operations
:type default_module_name: str
"""
Resolver.__init__(self)
self.default_module_name = default_module_name
self.collection_endpoint_name = collection_endpoint_name

def resolve_operation_id(self, operation):
"""
Resolves the operationId using REST semantics unless explicitly configured in the spec
:type operation: connexion.operation.Operation
"""
if operation.operation.get('operationId'):
return Resolver.resolve_operation_id(self, operation)

return self.resolve_operation_id_using_rest_semantics(operation)

def resolve_operation_id_using_rest_semantics(self, operation):
"""
Resolves the operationId using REST semantics
:type operation: connexion.operation.Operation
"""
path_match = re.search(
'^/?(?P<resource_name>(\w(?<!/))*)(?P<trailing_slash>/*)(?P<extended_path>.*)$', operation.path
)

def get_controller_name():
x_router_controller = operation.operation.get('x-swagger-router-controller')

name = self.default_module_name

if x_router_controller:
name = x_router_controller

elif path_match.group('resource_name'):
name += '.' + path_match.group('resource_name')

return name

def get_function_name():
method = operation.method

is_collection_endpoint = \
method == 'GET' \
and path_match.group('resource_name') \
and not path_match.group('extended_path')

return self.collection_endpoint_name if is_collection_endpoint else method.lower()

return get_controller_name() + '.' + get_function_name()
8 changes: 4 additions & 4 deletions connexion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ def deep_getattr(obj, attr):
return functools.reduce(getattr, attr.split('.'), obj)


def get_function_from_name(operation_id):
def get_function_from_name(function_name):
"""
Default operation resolver, tries to get function by fully qualified name (e.g. "mymodule.myobj.myfunc")
Tries to get function by fully qualified name (e.g. "mymodule.myobj.myfunc")
:type operation_id: str
:type function_name: str
"""
module_name, attr_path = operation_id.rsplit('.', 1)
module_name, attr_path = function_name.rsplit('.', 1)
module = None

while not module:
Expand Down
2 changes: 2 additions & 0 deletions tests/fakeapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def get():
return ''
11 changes: 11 additions & 0 deletions tests/fakeapi/hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ def test_method(self):

class_instance = DummyClass()

def get():
return ''

def search():
return ''

def list():
return ''

def post():
return ''

def post_greeting(name):
data = {'greeting': 'Hello {name}'.format(name=name)}
Expand Down
2 changes: 0 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

import pathlib

from connexion.api import Api

TEST_FOLDER = pathlib.Path(__file__).parent
Expand Down
6 changes: 6 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ def app():
return app


def test_add_api_with_function_resolver_function_is_wrapped():
app = App(__name__, specification_dir=SPEC_FOLDER)
api = app.add_api('api.yaml', resolver=lambda oid: (lambda foo: 'bar'))
assert api.resolver.resolve_function_from_operation_id('faux')('bah') == 'bar'


def test_app_with_relative_path():
# Create the app with a realative path and run the test_app testcase below.
app = App(__name__, 5001, SPEC_FOLDER.relative_to(TEST_FOLDER),
Expand Down

0 comments on commit a7e42fe

Please sign in to comment.