Commit edd91574 authored by Ellery Newcomer's avatar Ellery Newcomer
Browse files

validate drs output against swagger schema

parent bc8a7ba5
......@@ -94,8 +94,7 @@ class DocumentationGenerator(object):
parameters = doc_parser.discover_parameters(
inspector=method_introspector)
if parameters:
operation['parameters'] = parameters
operation['parameters'] = parameters or []
if response_messages:
operation['responseMessages'] = response_messages
......@@ -290,14 +289,14 @@ class DocumentationGenerator(object):
if getattr(field, 'required', False):
data['required'].append(name)
data_type = get_data_type(field) or 'string'
data_type, data_format = get_data_type(field) or ('string', 'string')
if data_type == 'hidden':
continue
# guess format
data_format = 'string'
if data_type in BaseMethodIntrospector.PRIMITIVES:
data_format = BaseMethodIntrospector.PRIMITIVES.get(data_type)[0]
# data_format = 'string'
# if data_type in BaseMethodIntrospector.PRIMITIVES:
# data_format = BaseMethodIntrospector.PRIMITIVES.get(data_type)[0]
description = getattr(field, 'help_text', '')
if not description or description.strip() == '':
......
......@@ -412,14 +412,14 @@ class BaseMethodIntrospector(object):
if getattr(field, 'read_only', False):
continue
data_type = get_data_type(field) or 'string'
data_type, data_format = get_data_type(field) or ('string', 'string')
if data_type == 'hidden':
continue
# guess format
data_format = 'string'
if data_type in self.PRIMITIVES:
data_format = self.PRIMITIVES.get(data_type)[0]
# data_format = 'string'
# if data_type in self.PRIMITIVES:
# data_format = self.PRIMITIVES.get(data_type)[0]
f = {
'paramType': 'form',
......@@ -449,7 +449,7 @@ class BaseMethodIntrospector(object):
f['maximum'] = max_val
# ENUM options
if get_data_type(field) in ['multiple choice', 'choice']:
if get_data_type(field)[0] in ['multiple choice', 'choice']:
if isinstance(field.choices, list):
f['enum'] = [k for k, v in field.choices]
elif isinstance(field.choices, dict):
......@@ -461,48 +461,44 @@ class BaseMethodIntrospector(object):
def get_data_type(field):
# (in swagger 2.0 we might get to use the descriptive types..
from rest_framework import fields
if hasattr(field, 'type_label'):
if field.type_label == 'field':
return 'string'
else:
return field.type_label
elif isinstance(field, fields.BooleanField):
return 'boolean'
elif isinstance(field, fields.NullBooleanField):
return 'boolean'
elif isinstance(field, fields.URLField):
return 'url'
elif isinstance(field, fields.SlugField):
return 'slug'
if isinstance(field, fields.BooleanField):
return 'boolean', 'boolean'
elif hasattr(fields, 'NullBooleanField') and isinstance(field, fields.NullBooleanField):
return 'boolean', 'boolean'
# elif isinstance(field, fields.URLField):
# return 'string', 'string' # 'url'
# elif isinstance(field, fields.SlugField):
# return 'string', 'string', # 'slug'
elif isinstance(field, fields.ChoiceField):
return 'choice'
elif isinstance(field, fields.EmailField):
return 'email'
elif isinstance(field, fields.RegexField):
return 'regex'
return 'choice', 'choice'
# elif isinstance(field, fields.EmailField):
# return 'string', 'string' # 'email'
# elif isinstance(field, fields.RegexField):
# return 'string', 'string' # 'regex'
elif isinstance(field, fields.DateField):
return 'date'
return 'string', 'date'
elif isinstance(field, fields.DateTimeField):
return 'datetime'
elif isinstance(field, fields.TimeField):
return 'time'
return 'string', 'date-time' # 'datetime'
# elif isinstance(field, fields.TimeField):
# return 'string', 'string' # 'time'
elif isinstance(field, fields.IntegerField):
return 'integer'
return 'integer', 'int64' # 'integer'
elif isinstance(field, fields.FloatField):
return 'float'
elif isinstance(field, fields.DecimalField):
return 'decimal'
elif isinstance(field, fields.ImageField):
return 'image upload'
elif isinstance(field, fields.FileField):
return 'file upload'
elif isinstance(field, fields.CharField):
return 'string'
return 'number', 'float' # 'float'
# elif isinstance(field, fields.DecimalField):
# return 'string', 'string' #'decimal'
# elif isinstance(field, fields.ImageField):
# return 'string', 'string' # 'image upload'
# elif isinstance(field, fields.FileField):
# return 'string', 'string' # 'file upload'
# elif isinstance(field, fields.CharField):
# return 'string', 'string'
elif rest_framework.VERSION >= '3.0.0' and isinstance(field, fields.HiddenField):
return 'hidden'
return 'hidden', 'hidden'
else:
return 'string'
return 'string', 'string'
class APIViewIntrospector(BaseViewIntrospector):
......
import datetime
import functools
import os
import os.path
from mock import Mock, patch
from distutils.version import StrictVersion
......@@ -53,8 +55,8 @@ class MockApiView(APIView):
"""
Get method specific comments
"""
pass
pass
from rest_framework.views import Response
return Response("mock me maybe")
class NonApiView(View):
......@@ -686,7 +688,8 @@ class DocumentationGeneratorTest(TestCase, DocumentationGeneratorMixin):
self.assertEqual('my param', get['parameters'][0]['description'])
self.assertNotIn('my param', get['notes'])
post = [a for a in stuff[0]['operations'] if a['method'] == 'POST'][0]
self.assertNotIn('parameters', post)
self.assertIn('parameters', post)
self.assertEqual(post['parameters'], [])
self.assertIn('--iron-socks', post['notes'])
......@@ -1038,6 +1041,34 @@ class BaseViewIntrospectorTest(TestCase):
self.assertEqual('A Test View', introspector.get_description())
MY_CHOICES = (
('val1', "Value1"),
('val2', "Value2"),
('val3', "Value3"),
('val4', "Value4")
)
class KitchenSinkSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField(default=datetime.datetime.now)
expires = serializers.DateField()
expires_by = serializers.TimeField()
age = serializers.IntegerField()
flagged = serializers.BooleanField()
url = serializers.URLField()
slug = serializers.SlugField()
choice = serializers.ChoiceField(
choices=MY_CHOICES, default=MY_CHOICES[0][0])
regex = serializers.RegexField("[a-f]+")
float = serializers.FloatField()
decimal = serializers.DecimalField(max_digits=5, decimal_places=1)
file = serializers.FileField()
image = serializers.ImageField()
joop = serializers.PrimaryKeyRelatedField(queryset=1)
class BaseMethodIntrospectorTest(TestCase, DocumentationGeneratorMixin):
def make_introspector(self, view_class):
return make_apiview_introspector(view_class)
......@@ -1191,40 +1222,17 @@ class BaseMethodIntrospectorTest(TestCase, DocumentationGeneratorMixin):
self.assertNotIn("hidden", write_properties)
def test_build_form_parameters(self):
MY_CHOICES = (
('val1', "Value1"),
('val2', "Value2"),
('val3', "Value3"),
('val4', "Value4")
)
class SomeSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField(default=datetime.datetime.now)
expires = serializers.DateField()
expires_by = serializers.TimeField()
age = serializers.IntegerField()
flagged = serializers.BooleanField()
url = serializers.URLField()
slug = serializers.SlugField()
choice = serializers.ChoiceField(
choices=MY_CHOICES, default=MY_CHOICES[0][0])
regex = serializers.RegexField("[a-f]+")
float = serializers.FloatField()
decimal = serializers.DecimalField(max_digits=5, decimal_places=1)
file = serializers.FileField()
image = serializers.ImageField()
joop = serializers.PrimaryKeyRelatedField(queryset=1)
class SerializedAPI(ListCreateAPIView):
serializer_class = SomeSerializer
serializer_class = KitchenSinkSerializer
class_introspector = self.make_introspector2(SerializedAPI)
introspector = APIViewMethodIntrospector(class_introspector, 'POST')
params = introspector.build_form_parameters()
self.assertEqual(len(SomeSerializer().get_fields()), len(params))
self.assertEqual(
len(KitchenSinkSerializer().get_fields()),
len(params))
self.assertEqual(params[0]['name'], 'email')
url_patterns = patterns('', url(r'my-api/', SerializedAPI.as_view()))
......@@ -1232,25 +1240,30 @@ class BaseMethodIntrospectorTest(TestCase, DocumentationGeneratorMixin):
generator = self.get_documentation_generator()
apis = urlparser.get_apis(url_patterns)
models = generator.get_models(apis)
self.assertIn("SomeSerializer", models)
properties = models["SomeSerializer"]['properties']
self.assertEqual("email", properties["email"]["type"])
self.assertIn("KitchenSinkSerializer", models)
properties = models["KitchenSinkSerializer"]['properties']
self.assertEqual("string", properties["email"]["type"])
self.assertNotIn("format", properties["email"])
self.assertEqual("string", properties["content"]["type"])
self.assertEqual("datetime", properties["created"]["type"])
self.assertEqual("date", properties["expires"]["type"])
self.assertEqual("time", properties["expires_by"]["type"])
self.assertEqual("string", properties["created"]["type"])
self.assertEqual("date-time", properties["created"]["format"])
self.assertEqual("string", properties["expires"]["type"])
self.assertEqual("date", properties["expires"]["format"])
self.assertEqual("string", properties["expires_by"]["type"])
self.assertEqual("integer", properties["age"]["type"])
self.assertEqual("boolean", properties["flagged"]["type"])
self.assertEqual("url", properties["url"]["type"])
self.assertEqual("slug", properties["slug"]["type"])
self.assertEqual("string", properties["url"]["type"])
self.assertNotIn("format", properties["url"])
self.assertEqual("string", properties["slug"]["type"])
self.assertIn(
properties["choice"]["type"],
["choice", "multiple choice"])
self.assertEqual("regex", properties["regex"]["type"])
self.assertEqual("float", properties["float"]["type"])
self.assertEqual("decimal", properties["decimal"]["type"])
self.assertEqual("file upload", properties["file"]["type"])
self.assertEqual("image upload", properties["image"]["type"])
self.assertEqual("string", properties["regex"]["type"])
self.assertEqual("number", properties["float"]["type"])
self.assertEqual("float", properties["float"]["format"])
self.assertEqual("string", properties["decimal"]["type"])
self.assertEqual("string", properties["file"]["type"])
self.assertEqual("string", properties["image"]["type"])
self.assertEqual("string", properties["joop"]["type"])
def test_build_form_parameters_allowable_values(self):
......@@ -2512,3 +2525,55 @@ class TestAdvancedDecoratorIntrospection(TestCase, DocumentationGeneratorMixin):
cls.as_view = classonlymethod(new_as_view)
return cls
return wrapper
class Swagger1_2Tests(TestCase):
"""
build some swagger endpoints, and run swagger's JSON schema on the results.
"""
def setUp(self):
from json import loads
self.schemas = {}
schema_dir = 'schemas/v1.2'
for schema_file in [x for x in os.listdir(schema_dir)
if x.endswith('.json')]:
with open(os.path.join(schema_dir, schema_file)) as f:
schema = loads(f.read())
self.schemas[schema_file] = schema
def get_validator(self, schema_name):
from jsonschema import Draft4Validator
validator = Draft4Validator(self.schemas[schema_name + '.json'])
def http_handler(uri):
from django.utils.six.moves.urllib import parse
urp = parse.urlparse(uri)
paff = os.path.basename(urp.path)
return self.schemas[paff]
validator.resolver.handlers['http'] = http_handler
return validator
def test1(self):
class MockApiView(APIView):
def get_serializer_class(self):
return KitchenSinkSerializer
def get(self, request):
pass
self.url_patterns = patterns(
'',
url(r'^a-view/?$', MockApiView.as_view(), name='a test view'),
url(r'^swagger/', include('rest_framework_swagger.urls')),
)
urls = import_module(settings.ROOT_URLCONF)
urls.urlpatterns = self.url_patterns
validator = self.get_validator("resourceListing")
response = self.client.get("/swagger/api-docs/")
json = parse_json(response)
validator.validate(json)
validator = self.get_validator("apiDeclaration")
response = self.client.get("/swagger/api-docs/a-view")
json = parse_json(response)
self.assertIn("KitchenSinkSerializer", json['models'])
validator.validate(json)
# Swagger Specification JSON Schemas
The work on the JSON Schema for the Swagger Specification was donated to the community by [Francis Galiegue](https://github.com/fge)!
Keep in mind that due to some JSON Schema limitations, not all constraints can be described. The missing constraints will be listed here in the future.
{
"id": "http://swagger-api.github.io/schemas/v1.2/apiDeclaration.json#",
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [ "swaggerVersion", "basePath", "apis" ],
"properties": {
"swaggerVersion": { "enum": [ "1.2" ] },
"apiVersion": { "type": "string" },
"basePath": {
"type": "string",
"format": "uri",
"pattern": "^https?://"
},
"resourcePath": {
"type": "string",
"format": "uri",
"pattern": "^/"
},
"apis": {
"type": "array",
"items": { "$ref": "#/definitions/apiObject" }
},
"models": {
"type": "object",
"additionalProperties": {
"$ref": "modelsObject.json#"
}
},
"produces": { "$ref": "#/definitions/mimeTypeArray" },
"consumes": { "$ref": "#/definitions/mimeTypeArray" },
"authorizations": { "$ref": "authorizationObject.json#" }
},
"additionalProperties": false,
"definitions": {
"apiObject": {
"type": "object",
"required": [ "path", "operations" ],
"properties": {
"path": {
"type": "string",
"format": "uri-template",
"pattern": "^/"
},
"description": { "type": "string" },
"operations": {
"type": "array",
"items": { "$ref": "operationObject.json#" }
}
},
"additionalProperties": false
},
"mimeTypeArray": {
"type": "array",
"items": {
"type": "string",
"format": "mime-type"
},
"uniqueItems": true
}
}
}
{
"id": "http://swagger-api.github.io/schemas/v1.2/authorizationObject.json#",
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"additionalProperties": {
"oneOf": [
{
"$ref": "#/definitions/basicAuth"
},
{
"$ref": "#/definitions/apiKey"
},
{
"$ref": "#/definitions/oauth2"
}
]
},
"definitions": {
"basicAuth": {
"required": [ "type" ],
"properties": {
"type": { "enum": [ "basicAuth" ] }
},
"additionalProperties": false
},
"apiKey": {
"required": [ "type", "passAs", "keyname" ],
"properties": {
"type": { "enum": [ "apiKey" ] },
"passAs": { "enum": [ "header", "query" ] },
"keyname": { "type": "string" }
},
"additionalProperties": false
},
"oauth2": {
"type": "object",
"required": [ "type", "grantTypes" ],
"properties": {
"type": { "enum": [ "oauth2" ] },
"scopes": {
"type": "array",
"items": { "$ref": "#/definitions/oauth2Scope" }
},
"grantTypes": { "$ref": "oauth2GrantType.json#" }
},
"additionalProperties": false
},
"oauth2Scope": {
"type": "object",
"required": [ "scope" ],
"properties": {
"scope": { "type": "string" },
"description": { "type": "string" }
},
"additionalProperties": false
}
}
}
{
"id": "http://swagger-api.github.io/schemas/v1.2/dataType.json#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Data type as described by the specification (version 1.2)",
"type": "object",
"oneOf": [
{ "$ref": "#/definitions/refType" },
{ "$ref": "#/definitions/voidType" },
{ "$ref": "#/definitions/primitiveType" },
{ "$ref": "#/definitions/modelType" },
{ "$ref": "#/definitions/arrayType" }
],
"definitions": {
"refType": {
"required": [ "$ref" ],
"properties": {
"$ref": { "type": "string" }
},
"additionalProperties": false
},
"voidType": {
"enum": [ { "type": "void" } ]
},
"modelType": {
"required": [ "type" ],
"properties": {
"type": {
"type": "string",
"not": {
"enum": [ "boolean", "integer", "number", "string", "array" ]
}
}
},
"additionalProperties": false
},
"primitiveType": {
"required": [ "type" ],
"properties": {
"type": {
"enum": [ "boolean", "integer", "number", "string" ]
},
"format": { "type": "string" },
"defaultValue": {
"not": { "type": [ "array", "object", "null" ] }
},
"enum": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
},
"minimum": { "type": "string" },
"maximum": { "type": "string" }
},
"additionalProperties": false,
"dependencies": {
"format": {
"oneOf": [
{
"properties": {
"type": { "enum": [ "integer" ] },
"format": { "enum": [ "int32", "int64" ] }
}
},
{
"properties": {
"type": { "enum": [ "number" ] },
"format": { "enum": [ "float", "double" ] }
}
},
{
"properties": {
"type": { "enum": [ "string" ] },
"format": {
"enum": [ "byte", "date", "date-time" ]
}
}
}
]
},
"enum": {
"properties": {
"type": { "enum": [ "string" ] }
}
},
"minimum": {
"properties": {
"type": { "enum": [ "integer", "number" ] }
}
},
"maximum": {
"properties": {
"type": { "enum": [ "integer", "number" ] }
}
}
}
},
"arrayType": {
"required": [ "type", "items" ],
"properties": {
"type": { "enum": [ "array" ] },
"items": {
"type": "array",
"items": { "$ref": "#/definitions/itemsObject" }
},
"uniqueItems": { "type": "boolean" }
},
"additionalProperties": false
},
"itemsObject": {
"oneOf": [
{
"$ref": "#/definitions/refType"