Skip to content

Commit

Permalink
api: adding schema api endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
dlamotte committed Dec 3, 2013
1 parent f7f6a13 commit 9937451
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 11 deletions.
46 changes: 45 additions & 1 deletion krankshaft/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,36 @@ def post(self, request):

return view(request, *args, **kwargs)

@property
def schema(self):
views = {}
for view in self.registered_views:
try:
name = view.name
except AttributeError:
name = view.__name__

try:
view_schema = view.schema
except AttributeError:
view_schema = self.schema_default(view)

views[name] = view_schema

return {
'resources': views,
}

def schema_default(self, view):
return {
'doc': view.__doc__,
'endpoint': {},
'url': '',
}

def schema_view(self, request):
return self.serialize(request, 200, self.schema)

def serialize(self, request, status, obj,
content_type=None
, opts=None
Expand Down Expand Up @@ -822,14 +852,28 @@ def urls(self):

from django.conf.urls import include, patterns, url

urlpatterns = patterns('', *urlpatterns)
urlpatterns = self.urls_local + patterns('', *urlpatterns)
if self.name:
urlpatterns = patterns('',
url(r'^%s/' % self.name, include(urlpatterns)),
)

return urlpatterns

# TODO "me" endpoint which dumps information about authned?
@property
def urls_local(self):
'''
Returns the list of urls for this specific api.
'''
from django.conf.urls import patterns, url

return patterns('',
url(r'^schema/$', self.wrap(self.schema_view),
name=self.endpoint('schema')
),
)

def wrap(self, view_or_resource=None, register=False, url=None, **opts):
'''wrap(myview) -> wrapped_view
Expand Down
3 changes: 2 additions & 1 deletion krankshaft/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,8 @@ def get_field(self, model, accessor):
except FieldDoesNotExist as exc:
raise self.Issues([str(exc)])

def is_indexed(self, field):
@classmethod
def is_indexed(cls, field):
return field.db_index or field.primary_key

def make_meta(self, limit, offset):
Expand Down
74 changes: 65 additions & 9 deletions krankshaft/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ def routes(self):
ie:
prefix = r'^%s/' % self.name
return (
('name_of_url', prefix + r'endpoint/$', {
('endpoint', prefix + r'endpoint/$', {
'method': self.view,
...
}),
Expand All @@ -580,25 +580,75 @@ def routes(self):
del list_methods['put']

return (
(self.endpoint('list'), prefix + '$',
self.allowed('list', list_methods)
('list', prefix + '$',
self.allowed('list', list_methods),
(),
),
(self.endpoint('single'), prefix + '(?P<id>[^/]+)/$',
('single', prefix + '(?P<id>[^/]+)/$',
self.allowed('single', {
'delete': self.delete,
'get': self.get,
'put': self.put,
})
}),
('id', ),
),
(self.endpoint('set'), prefix + 'set/(?P<idset>[^/](?:[^/]|;)*)/$',
('set', prefix + 'set/(?P<idset>[^/](?:[^/]|;)*)/$',
self.allowed('set', {
'delete': self.delete_set,
'get': self.get_set,
'put': self.put_set,
})
}),
('idset', ),
),
)

@property
def schema(self):
schema = self.api.schema_default(self)

for endpoint, regex, methods, params in self.routes:
schema['endpoint'][endpoint] = {
'allow': [
method.upper()
for method in methods.keys()
],
'docs': {
method.upper(): view.__doc__
for method, view in methods.iteritems()
},
'params': params,
'url': self.reverse(endpoint, args=[
':' + param
for param in params
]),
}

schema['fields'] = {}
for field in self.fields:
d = schema['fields'][field.name] = {
'help_text':
field.help_text
if isinstance(field.help_text, basestring)
else '',
# XXX manytomany fields are a functional proxy??
'indexed': self.Query.is_indexed(field),
'nullable': field.null,
'type': field.__class__.__name__,
}

if field.choices:
d['choices'] = [
(value, display)
for value, display in field.choices
]

if hasattr(field, 'max_length'):
d['max_length'] = field.max_length

schema['url'] = self.reverse('list')

return schema

def serialize(self, instance):
'''serialize(instance) -> {...}
Expand Down Expand Up @@ -725,8 +775,14 @@ def urls(self):
from django.conf.urls import patterns, include, url

return patterns('', *[
url(regex, self.api.wrap(self.make_view(name, methods)), name=name)
for name, regex, methods in self.routes
url(
regex,
self.api.wrap(
self.make_view(self.endpoint(endpoint), methods)
),
name=self.endpoint(endpoint)
)
for endpoint, regex, methods, params in self.routes
])


Expand Down
70 changes: 70 additions & 0 deletions tests/krankshaft/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class Fake1Resource(DjangoModelResource):
class Fake2Resource(DjangoModelResource):
model = Fake2

@self.api1
def plainview(request):
return self.api1.response(request, 200)

super(APITest, self)._pre_setup()

# make sure cache is clear
Expand Down Expand Up @@ -485,6 +489,72 @@ def test_response(self):
'text/plain'
)

def test_schema(self):
response = self.client.get('/app1/api/v1/schema/')
assert response.status_code == 200
assert json.loads(response.content) == {
'resources': {
'fake1': {
'doc': None,
'endpoint': {
'list': {
'allow': ['PUT', 'POST', 'DELETE', 'GET'],
'docs': {
'DELETE': '\n Deletes the instances pointed to by the given query (specified by\n the query string).\n\n /resource/?field=value\n\n ',
'GET': '\n Get the serialized value of a list of objects of a given query\n (specified as the query string).\n\n /resource/?field=value\n\n ',
'POST': '\n Create a new object.\n\n /resource/\n\n ',
'PUT': '\n Given a single dictionary of fields and values, update all the instances\n with the given field/values.\n\n Ideally, a subset of all fields is given. Enables mass update to\n an unknown set of objects (those matching a query).\n\n /resource/?field=value\n\n '
},
'params': [],
'url': '/app1/api/v1/fake1/'
},
'set': {
'allow': ['PUT', 'DELETE', 'GET'],
'docs': {
'DELETE': '\n Delete the given set of objects (specified in the path as an id list\n separated by a semi-colon).\n\n /resource/set/1;2;3/\n\n ',
'GET': '\n Get the serialized value of a set of objects given by a list of ids\n separated by a semi-colon.\n\n /resource/set/1;2;3/\n\n ',
'PUT': '\n Update a set of objects.\n\n /resource/set/1;2;3/\n\n '
},
'params': ['idset'],
'url': '/app1/api/v1/fake1/set/:idset/'
},
'single': {
'allow': ['PUT', 'DELETE', 'GET'],
'docs': {
'DELETE': '\n Deletes the object pointed to by id.\n\n /resource/1/\n\n ',
'GET': '\n Get the serialized value of an object.\n\n /resource/1/\n\n ',
'PUT': '\n Update an object.\n\n /resource/1/\n\n '
},
'params': ['id'],
'url': '/app1/api/v1/fake1/:id/'
},
},
'fields': {
'id': {
'help_text': '',
'indexed': True,
'max_length': None,
'nullable': False,
'type': 'AutoField'
},
'name': {
'help_text': '',
'indexed': False,
'max_length': 20,
'nullable': False,
'type': 'CharField'
}
},
'url': '/app1/api/v1/fake1/'
},
'plainview': {
'url': '',
'doc': None,
'endpoint': {}
},
}
}

def test_serialize(self):
data = {'one': 1}
request = self.make_request(HTTP_ACCEPT='application/json; indent=4')
Expand Down

0 comments on commit 9937451

Please sign in to comment.