Permalink
Fetching contributors…
Cannot retrieve contributors at this time
221 lines (165 sloc) 6.96 KB

User-Defined Formats

Primitive types in Swagger support an optional modifier property format as explained in detail in the Swagger Specification. With this feature, you can define your own domain specific formats and have validation and marshalling to/from python/json handled transparently.

Creating a user-defined format

This is best explained with a simple example. Let's create a user-defined format for CIDR notation.

In a Swagger spec, the schema-object for a CIDR would resemble:

{
    "type": "string",
    "format": "cidr",
    "description": "IPv4 CIDR"
}

In python, we'd like CIDRs to automatically be converted to a CIDR object that makes them easy to work with.

class CIDR(object):
    def __init__(self, cidr):
        """
        :param cidr: CIDR in string form.
        """
        self.cidr = cidr

    def overlaps(self, other_cidr):
        """Return true if other_cidr overlaps with this cidr"""
        ...

    def subnet_mask(self):
        """Return the subnet mask of this cidr"""
        ...

    ...

We would also like CIDRs to be validated by bravado-core whenever they are part of a HTTP request or response.

Create a bravado_core.formatter.SwaggerFormat to define the CIDR format:

from bravado_core.formatter import SwaggerFormat

def validate_cidr(cidr_string):
    if '/' not in cidr_string:
        raise SwaggerValidationError('CIDR {0} is invalid'.format(cidr_string))

cidr_format = SwaggerFormat(
    # name of the format as used in the Swagger spec
    format='cidr',

    # Callable to convert a python CIDR object to a string
    to_wire=lambda cidr_object: cidr_object.cidr,

    # Callable to convert a string to a python CIDR object
    to_python=lambda cidr_string: CIDR(cidr_string),

    # Callable to validate the cidr in string form
    validate=validate_cidr
)

Configuring user-defined formats

Now that we have a cidr_format, just pass it to a Spec as part of the config parameter on Spec creation.

from bravado_core.spec import Spec

spec_dict = json.loads(open('swagger.json', 'r').read())
config = {
    'validate_responses': True,
    'validate_requests': True,
    'formats': [cidr_format],
}
spec = Spec.from_dict(spec_dict, config=config)

All validation and processing of HTTP requests and responses will now use the configured format where appropriate.

Putting it all together

A simple example of passing a CIDR object to a request and getting a list of CIDR objects back from the response.

{
    "paths": {
        "/get_overlapping_cidrs": {
            "get": {
                "parameters": [
                    {
                        "name": "cidr",
                        "in": "query",
                        "type": "string",
                        "format": "cidr"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "List of overlapping cidrs",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "string",
                                "format": "cidr"
                            }
                        }
                    }
                }
            }
        }
    }
}
from bravado_core.spec import Spec
from bravado_core.response import unmarshal_response
from bravado_core.param import marshal_param

# Retrieve the swagger spec from the server and json.load() it
spec_dict = ...

# Create cidr_format add it to the config dict
config = ...

# Create a bravado_core.spec.Spec
swagger_spec = Spec.from_dict(spec_dict, config=config)

# Get the operation to invoke
op = swagger_spec.get_op_for_request('GET', '/get_overlapping_cidrs')

# Get the Param that represents the cidr query parameter
cidr_param = op.params.get('cidr')

# Create a CIDR object - to_wire() will be called on this during marshalling
cidr_object = CIDR('192.168.1.1/24')
request_dict = {}

# Marshal the cidr_object into the request_dict.
marshal_param(cidr_param, cidr_object, request_dict)

# Lots of hand-wavey stuff here - use whatever http client you have to
# send the request and receive a response
response = http_client.send(request_dict)

# Extract the list of cidrs
cidrs = unmarshal_response(response)

# Verify cidrs are CIDR objects and not strings
for cidr in cidrs:
    assert type(cidr) == CIDR

Overriding built-in formats is also possible with a user-defined format

By default format 'double' is internally converted to a float in python. This runs the risk of being imprecise due to the way floats are handled. If you would like to instead use the decimal.Decimal() type for more precision, you can override this built-in format. See Also: Floating Point Precision.

You'd define the type:

from decimal import Decimal
import re

is_decimal = re.compile(r'^\d+(?:\.\d+)?$')
def validate_decimaltype(x):
  """Validate input is a str in valid decimal format"""
  if not (isinstance(x, str) and is_decimal.match(x)):
      raise bravado_core.exception.SwaggerValidationError()

mydouble = SwaggerFormat(
  format='double',
  to_wire=lambda x: str(x) if isinstance(x, Decimal) else str(Decimal(x)),
  to_python=lambda x: x if isinstance(x, Decimal) else Decimal(x),
  validate=validate_decimaltype,
  description="model format double internally as Decimal()"
)

Then in your config block you include this format:

config = {
    'formats': [mydouble],
    ...
}

# Create a bravado_core.spec.Spec
swagger_spec = Spec.from_dict(spec_dict, config=config)

Note about using precise Decimal format in Spec

The above works when the openapi schema is written as string(double) e.g. the spec passes the value as string on the wire and format is double. If the spec said it was a number(double), it is likely that json will first convert the number from the wire to a float and then pass that into Decimal() with unguaranteed precision. The calls to json would need use_decimals=True for that to work.