Skip to content

Commit

Permalink
Merge pull request #88 from dnephin/refactor_model
Browse files Browse the repository at this point in the history
Expose Validators instead of schemas and resolvers
  • Loading branch information
striglia committed Mar 24, 2015
2 parents 6e5a07b + 606fbed commit a7ea69a
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 280 deletions.
2 changes: 1 addition & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ Changelog
* Added ``generate_resource_listing`` configuration option to allow
pyramid_swagger to generate the ``apis`` section of the resource listing.
* Bug fix for issues relating to ``void`` responses (See `Issue 79`_)
* Added support for header validation.

.. _Issue 79: https://github.com/striglia/pyramid_swagger/issues/79


1.4.0 (2015-01-27)
++++++++++++++++++

Expand Down
8 changes: 4 additions & 4 deletions pyramid_swagger/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ def compile_swagger_schema(schema_dir, resource_listing):
:returns: a SwaggerSchema object
"""
mapping = build_schema_mapping(schema_dir, resource_listing)
schema_resolvers = ingest_resources(mapping, schema_dir)
return SwaggerSchema(resource_listing, mapping, schema_resolvers)
resource_validators = ingest_resources(mapping, schema_dir)
return SwaggerSchema(resource_listing, mapping, resource_validators)


def validate_swagger_schema(schema_dir, resource_listing):
Expand Down Expand Up @@ -161,8 +161,8 @@ def ingest_resources(mapping, schema_dir):
:type mapping: dict
:param schema_dir: the directory schema files live inside
:type schema_dir: string
:returns: A list of :class:`pyramid_swagger.load_schema.SchemaAndResolver`
objects
:returns: A list of mapping from :class:`RequestMatcher` to
:class:`ValidatorMap`
"""
ingested_resources = []
for name, filepath in mapping.items():
Expand Down
208 changes: 107 additions & 101 deletions pyramid_swagger/load_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,18 @@

import simplejson
from jsonschema import RefResolver
from jsonschema.validators import Draft3Validator, Draft4Validator

from pyramid_swagger.model import partial_path_match

def extract_query_param_schema(schema):

EXTENDED_TYPES = {
'float': (float,),
'int': (int,),
}


def build_param_schema(schema, param_type):
"""Turn a swagger endpoint schema into an equivalent one to validate our
request.
Expand Down Expand Up @@ -38,53 +47,23 @@ def extract_query_param_schema(schema):
properties = dict(
(s['name'], strip_swagger_markup(s))
for s in schema['parameters']
if s['paramType'] == 'query'
if s['paramType'] == param_type
)
# Generate a jsonschema that describes the set of all query parameters. We
# can then validate this against dict(request.params).
if properties:
return {
'type': 'object',
'properties': properties,
'additionalProperties': False,
}
else:
return None


def extract_path_schema(schema):
"""Extract a schema for path variables for an endpoint.
As an example, this would take this swagger schema:
{
"paramType": "path",
"type": "string",
"enum": ["foo", "bar"],
"required": true
}
To this jsonschema:
{
"type": "string",
"enum": ["foo", "bar"],
}
Which we can then validate against a JSON object we construct from the
pyramid request.
"""
properties = dict(
(s['name'], strip_swagger_markup(s))
for s in schema['parameters']
if s['paramType'] == 'path'
)
if properties:
return {
'type': 'object',
'properties': properties,
'additionalProperties': False,
# Allow extra headers. Most HTTP requests will have headers which
# are outside the scope of the spec (like `Host`, or `User-Agent`)
'additionalProperties': param_type == 'header',
}
else:
return None


# TODO: do this with jsonschema directly
def extract_body_schema(schema, models_schema):
"""Turn a swagger endpoint schema into an equivalent one to validate our
request.
Expand Down Expand Up @@ -128,6 +107,7 @@ def extract_body_schema(schema, models_schema):
return None


# TODO: do this with jsonschema directly
def strip_swagger_markup(schema):
"""Turn a swagger URL parameter schema into a raw jsonschema.
Expand All @@ -146,7 +126,7 @@ def strip_swagger_markup(schema):

def get_model_resolver(schema):
"""
Gets the schema and a RefResolver. RefResolver's will resolve "$ref:
Get a RefResolver. RefResolver's will resolve "$ref:
ObjectType" entries in the schema, which are used to describe more complex
objects.
Expand All @@ -160,49 +140,97 @@ def get_model_resolver(schema):
return RefResolver('', '', models)


class SchemaMap(namedtuple(
'SchemaMap', [
'request_query_schema',
'request_path_schema',
'request_body_schema',
'response_body_schema'
])):
class ValidatorMap(namedtuple('_VMap', 'query path headers body response')):
"""
A SchemaMap contains a mapping from incoming paths to schemas for request
queries, request bodies, and responses. This requires some precomputation
but means we can do fast query-time validation without having to walk over
the schema.
A data object with validators for each part of the request and response
objects. Each field is a :class:`SchemaValidator`.
"""
__slots__ = ()

@classmethod
def from_operation(cls, operation, models, resolver):
args = []
for schema, validator in [
(build_param_schema(operation, 'query'), Draft3Validator),
(build_param_schema(operation, 'path'), Draft3Validator),
(build_param_schema(operation, 'header'), Draft3Validator),
(extract_body_schema(operation, models), Draft4Validator),
(extract_response_body_schema(operation, models),
Draft4Validator),
]:
args.append(SchemaValidator.from_schema(
schema,
resolver,
validator))

return cls(*args)


class SchemaValidator(object):
"""A Validator used by :mod:`pyramid_swagger.tween` to validate a
field from the request or response.
:param schema: a :class:`dict` jsonschema that was used by the
validator
:param valdiator: a Validator which a func:`validate` method
for validating a field from a request or response. This
will often be a :class:`jsonschema.validator.Validator`.
"""

def build_request_to_schemas_map(schema):
"""Take the swagger schema and build a map from incoming path to a
jsonschema for requests and responses."""
request_to_schema = {}
def __init__(self, schema, validator):
self.schema = schema
self.validator = validator

@classmethod
def from_schema(cls, schema, resolver, validator_class):
return cls(
schema,
validator_class(schema, resolver=resolver, types=EXTENDED_TYPES))

def validate(self, values):
"""Validate a :class:`dict` of values. If `self.schema` is falsy this
is a noop.
"""
if not self.schema:
return
self.validator.validate(values)


def build_request_to_validator_map(schema, resolver):
"""Build a mapping from :class:`RequestMatcher` to :class:`ValidatorMap`
for each operation in the API spec. This mapping may be used to retrieve
the appropriate validators for a request.
"""
schema_models = schema.get('models', {})
for api in schema['apis']:
path = api['path']
for operation in api['operations']:
# Now that we have the necessary info for this particular
# path/method combination, build our dict.
key = (path, operation['method'])
request_to_schema[key] = SchemaMap(
request_query_schema=extract_query_param_schema(operation),
request_path_schema=extract_path_schema(operation),
request_body_schema=extract_body_schema(
operation,
schema_models
),
response_body_schema=extract_response_body_schema(
operation,
schema_models
),
)

return request_to_schema
return dict(
(
RequestMatcher(api['path'], operation['method']),
ValidatorMap.from_operation(operation, schema_models, resolver)
)
for api in schema['apis']
for operation in api['operations']
)


class RequestMatcher(object):
"""Match a :class:`pyramid.request.Request` to a swagger Operation"""

def __init__(self, path, method):
self.path = path
self.method = method

def matches(self, request):
"""
:param request: a :class:`pyramid.request.Request`
:returns: True if this matcher matches the request, False otherwise
"""
return (
partial_path_match(request.path, self.path) and
request.method == self.method
)


# TODO: do this with jsonschema directly
def extract_response_body_schema(operation, schema_models):
if operation['type'] in schema_models:
return extract_validatable_type(operation['type'], schema_models)
Expand All @@ -212,15 +240,14 @@ def extract_response_body_schema(operation, schema_models):
'maximum', 'items', 'uniqueItems'
)

schema = dict([
return dict([
(field, operation[field])
for field in acceptable_fields
if field in operation
])

return schema


# TODO: do this with jsonschema directly
def extract_validatable_type(type_name, models):
"""Returns a jsonschema-compatible typename from the Swagger type.
Expand All @@ -237,34 +264,13 @@ def extract_validatable_type(type_name, models):
return {'type': type_name}


class SchemaAndResolver(namedtuple(
'SAR',
['request_to_schema_map', 'resolver'])):
__slots__ = ()


def load_schema(schema_path):
"""Prepare the schema so we can make fast validation comparisons.
The prepared schema will be a map:
key: (swagger_path, method) e.g. ('/v1/reverse', 'GET')
value: a SchemaMap
For any request, you just need to:
1) Validate {k, v for k, v in query.params} against
request_query_schema
2) Validate request body against request_body_schema
3) Validate response body against response_body_schema
"""Prepare the api specification for request and response validation.
Response and request bodies will need to be transformed as indicated by
their content type (e.g. simplejson.loads if you have application/json
type).
:returns: SchemaAndResolver
:returns: a mapping from :class:`RequestMatcher` to :class:`ValidatorMap`
for every operation in the api specification.
:rtype: dict
"""
with open(schema_path, 'r') as schema_file:
schema = simplejson.load(schema_file)
return SchemaAndResolver(
request_to_schema_map=build_request_to_schemas_map(schema),
resolver=get_model_resolver(schema),
)
return build_request_to_validator_map(schema, get_model_resolver(schema))
51 changes: 25 additions & 26 deletions pyramid_swagger/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,39 @@ class SwaggerSchema(object):
This object contains data structures representing your Swagger schema
and exposes methods for efficiently finding the relevant schemas for a
Pyramid request.
"""
def __init__(self, resource_listing, api_declarations, schema_resolvers):
"""Store schema_resolvers for later use.
:param resource_listing: A swagger resource listing
:type resource_listing: dict
:param api_declarations: Map from resource name to filepath of its api
declaration
:type api_declarations: dict
:param resource_validators: a list of resolvers, one per Swagger resource
:type resource_validators: list of mappings from :class:`RequestMatcher`
to :class:`ValidatorMap`
for every operation in the api specification.
"""

:param resource_listing: A swagger resource listing
:type resource_listing: dict
:param api_declarations: Map from resource name to filepath of its api
declaration
:type api_declarations: dict
:param schema_resolvers: a list of resolvers, one per Swagger resource
:type schema_resolvers: list of
pyramid_swagger.load_schema.SchemaAndResolver objects
"""
def __init__(
self,
resource_listing,
api_declarations,
resource_validators):
self.resource_listing = resource_listing
self.api_declarations = api_declarations
self.schema_resolvers = schema_resolvers
self.resource_validators = resource_validators

def schema_and_resolver_for_request(self, request):
"""Takes a request and returns the relevant schema, ready for
validation.
def validators_for_request(self, request):
"""Takes a request and returns a validator mapping for the request.
:param request: A Pyramid request to fetch schemas for
:type request: pyramid.request.Request
:returns: (schema_map, resolver) for this particular request
:rtype: A tuple of (load_schema.SchemaMap, jsonschema.Resolver)
:type request: :class:`pyramid.request.Request`
:returns: a :class:`pyramid_swagger.load_schema.ValidatorMap` which can
be used to validate `request`
"""
for schema_resolver in self.schema_resolvers:
request_to_schema_map = schema_resolver.request_to_schema_map
resolver = schema_resolver.resolver
for (path, method), schema_map in request_to_schema_map.items():
if partial_path_match(request.path, path) \
and method == request.method:
return (schema_map, resolver)
for resource_validator in self.resource_validators:
for matcher, validator_map in resource_validator.items():
if matcher.matches(request):
return validator_map

raise PathNotMatchedError(
'Could not find the relevant path ({0}) in the Swagger schema. '
Expand Down
Loading

0 comments on commit a7ea69a

Please sign in to comment.