Skip to content

Commit

Permalink
Generate JSON hypermedia schema from an api
Browse files Browse the repository at this point in the history
  • Loading branch information
leahfitch committed Sep 25, 2014
1 parent 9ba86ff commit 81a0402
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 30 deletions.
15 changes: 9 additions & 6 deletions cellardoor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,29 @@ def __init__(self, collections=(), authenticators=(), storage=None):
else:
collection_classes = collections
entities = set()
self.collections_by_class_name = {}
collections_by_class_name = {}

for collection_cls in collection_classes:
entities.add(collection_cls.entity)
collection = collection_cls(storage)
self.collections_by_class_name[collection_cls.__name__] = collection
collections_by_class_name[collection_cls.__name__] = collection
setattr(self, collection_cls.plural_name, collection)

self.model = Model(storage, entities)

for collection in self.collections_by_class_name.values():
for collection in collections_by_class_name.values():
new_links = {}
if collection.links:
for k, v in collection.links.items():
if not isinstance(v, basestring):
v = v.__name__
referenced_collection = self.collections_by_class_name.get(v)
referenced_collection = collections_by_class_name.get(v)
new_links[k] = referenced_collection
collection.links = new_links

self.collections = collections_by_class_name.values()
self.entities = entities


def schema(self):
return to_jsonschema(self.model.entities)
def schema(self, base_url):
return to_jsonschema(self, base_url)
2 changes: 1 addition & 1 deletion cellardoor/integrations/falcon_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def add_to_falcon(falcon_api, cellardoor_api, views):
duplicate_field_error_with_views = functools.partial(duplicate_field_error, views_by_type)
falcon_api.add_error_handler(errors.DuplicateError, duplicate_field_error_with_views)

for collection in cellardoor_api.collections_by_class_name.values():
for collection in cellardoor_api.collections:
resource = Resource(collection, views_by_type)
resource.add_to_falcon(falcon_api)

132 changes: 113 additions & 19 deletions cellardoor/spec/jsonschema.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
from .. import model


class JSONSchemaSerializer(object):
class EntitySerializer(object):

fallbacks = (
model.Text, model.Integer, model.Float, model.DateTime,
model.Boolean, model.Enum, model.ListOf, model.OneOf
)

def create_schema(self, entities):
return {
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": self.get_definitions(entities)
}


def get_definitions(self, entities):
definitions = {}
for e in entities:
definitions[e.__name__] = self.get_definition(e)
return definitions


def get_definition(self, entity):
def create_schema(self, entity):
props = {}
required_props = []
for k,v in entity.fields.items():
Expand All @@ -39,6 +25,7 @@ def get_definition(self, entity):

def get_property(self, field):
prop = {}
prop['default'] = field.default
if field.help:
prop['description'] = field.help
type_name = field.__class__.__name__
Expand Down Expand Up @@ -114,11 +101,118 @@ def handle_Reference(self, field, prop):
prop['format'] = 'reference'
prop['schema'] = '#/definitions/%s' % field.entity.__name__



class APISerializer(object):

def create_schema(self, api, base_url, entity_serializer):
self.base_url = base_url
definitions = {}
for e in api.entities:
definitions[e.__name__] = entity_serializer.create_schema(e)

resources = {}
for collection in api.collections:
resources[collection.plural_name] = self.get_resource_schema(collection)

return {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"definitions": definitions,
"properties": resources
}


def get_resource_schema(self, collection):
links = {}

for method in collection.rules.enabled_methods:
fn = getattr(self, 'get_%s_link' % method)
links[method] = fn(collection)

return {
"links": links
}


def get_list_link(self, collection):
return {
'href': self.base_url + '/%s' % collection.plural_name,
'method': 'GET',
'rel': 'instances',
'title': 'List',
'targetSchema': {
'type': 'array',
'items': {
'$ref': self.entity_schema_ref(collection)
}
}
}


def get_get_link(self, collection):
return {
'href': self.base_url + '/%s/{id}' % collection.plural_name,
'method': 'GET',
'rel': 'instance',
'title': 'Details',
'targetSchema': { '$ref': self.entity_schema_ref(collection) }
}


def get_create_link(self, collection):
return {
'href': self.base_url + '/%s' % collection.plural_name,
'method': 'POST',
'rel': 'create',
'title': 'New',
'schema': { '$ref': self.entity_schema_ref(collection) },
'targetSchema': { '$ref': self.entity_schema_ref(collection) }
}


def get_update_link(self, collection):
return {
'href': self.base_url + '/%s/{id}' % collection.plural_name,
'method': 'PATCH',
'rel': 'update',
'title': 'Update',
'schema': {
'allOf': [ { '$ref': self.entity_schema_ref(collection) } ],
'required': []
},
'targetSchema': { '$ref': self.entity_schema_ref(collection) }
}


def get_replace_link(self, collection):
return {
'href': self.base_url + '/%s/{id}' % collection.plural_name,
'method': 'PUT',
'rel': 'replace',
'title': 'Replace',
'schema': { '$ref': self.entity_schema_ref(collection) },
'targetSchema': { '$ref': self.entity_schema_ref(collection) }
}


def get_delete_link(self, collection):
return {
'href': self.base_url + '/%s/{id}' % collection.plural_name,
'method': 'DELETE',
'rel': 'delete',
'title': 'Delete'
}


def entity_schema_ref(self, collection):
return '#/definitions/%s' % collection.entity.__class__.__name__


def to_jsonschema(entities, cls=JSONSchemaSerializer):
serializer = cls()
return serializer.create_schema(entities)
def to_jsonschema(api, base_url, api_cls=APISerializer, entity_cls=EntitySerializer):
api_serializer = api_cls()
entity_serializer = entity_cls()
return api_serializer.create_schema(api, base_url, entity_serializer)



2 changes: 1 addition & 1 deletion cellardoor/views/minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MinimalView(View):
)

def get_collection_response(self, req, objs):
return self.serialize(req, {'items':objs})
return self.serialize(req, objs)


def get_individual_response(self, req, obj):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_falcon_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def test_list(self):
)
result = json.loads(''.join(data))
self.assertEquals(self.srmock.status, '200 OK')
self.assertEquals(result, {'items':foos})
self.assertEquals(result, foos)
self.cellardoor.foos.list.assert_called_with(sort=['+name'], filter={'foo':23}, offset=7, limit=10, show_hidden=True, context={})


Expand Down
4 changes: 2 additions & 2 deletions tests/test_view_minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ def test_collection_response(self):
req = create_fake_request(headers={'accept':'application/json'})
content_type, result = view.get_collection_response(req, objs)
self.assertEquals(content_type, 'application/json')
self.assertEquals(result, json.dumps({'items':objs}))
self.assertEquals(result, json.dumps(objs))

req = create_fake_request(headers={'accept':'application/x-msgpack'})
content_type, result = view.get_collection_response(req, objs)
self.assertEquals(content_type, 'application/x-msgpack')
self.assertEquals(result, msgpack.packb({'items':objs}))
self.assertEquals(result, msgpack.packb(objs))


def test_individual_response(self):
Expand Down

0 comments on commit 81a0402

Please sign in to comment.