Skip to content
This repository was archived by the owner on Mar 18, 2019. It is now read-only.

Commit 055fce9

Browse files
committed
First pass at OpenAPI support
1 parent eb0cd25 commit 055fce9

File tree

3 files changed

+151
-7
lines changed

3 files changed

+151
-7
lines changed

coreapi/codecs/base.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,35 @@
44

55
# Helper functions to get an expected type from a dictionary.
66

7-
def _get_string(item, key):
7+
def dereference(lookup_string, struct):
8+
"""
9+
Dereference a JSON pointer.
10+
http://tools.ietf.org/html/rfc6901
11+
"""
12+
keys = lookup_string.strip('#/').split('/')
13+
node = struct
14+
for key in keys:
15+
node = _get_dict(node, key)
16+
return node
17+
18+
19+
def _get_string(item, key, default=''):
820
value = item.get(key)
9-
return value if isinstance(value, string_types) else ''
21+
return value if isinstance(value, string_types) else default
1022

1123

12-
def _get_dict(item, key):
24+
def _get_dict(item, key, default={}, dereference_using=None):
1325
value = item.get(key)
14-
return value if isinstance(value, dict) else {}
26+
if isinstance(value, dict):
27+
if dereference_using and ('$ref' in value) and (len(value) == 1):
28+
return dereference(value['$ref'], dereference_using)
29+
return value
30+
return default.copy()
1531

1632

17-
def _get_list(item, key):
33+
def _get_list(item, key, default=[]):
1834
value = item.get(key)
19-
return value if isinstance(value, list) else []
35+
return value if isinstance(value, list) else list(default)
2036

2137

2238
def _get_bool(item, key, default=False):
@@ -30,6 +46,10 @@ def get_dicts(item):
3046
return [value for value in item if isinstance(value, dict)]
3147

3248

49+
def get_strings(item):
50+
return [value for value in item if isinstance(value, string_types)]
51+
52+
3353
class BaseCodec(itypes.Object):
3454
media_type = None
3555

coreapi/codecs/hyperschema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def _get_content(data, base_url, ref):
5959

6060
if schema_type == ['object'] and schema_properties:
6161
fields += [
62-
Field(name=key, required=key in schema_required)
62+
Field(name=key, required=(key in schema_required))
6363
for key in schema_properties.keys()
6464
]
6565
if rel == 'self':

coreapi/codecs/openapi.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from coreapi.codecs.base import BaseCodec, _get_string, _get_dict, _get_list, _get_bool, get_strings, get_dicts
2+
from coreapi.compat import urlparse
3+
from coreapi.document import Document, Link, Array, Object, Field, Error
4+
from coreapi.exceptions import ParseError
5+
import json
6+
7+
8+
def expand_schema(schema):
9+
schema_type = schema.get('type')
10+
schema_properties = _get_dict(schema, 'properties')
11+
schema_required = _get_list(schema, 'required')
12+
if ((schema_type == ['object']) or (schema_type == 'object')) and schema_properties:
13+
return [
14+
(key, key in schema_required)
15+
for key in schema_properties.keys()
16+
]
17+
return None
18+
19+
20+
def _get_document_base_url(data, base_url=None):
21+
prefered_schemes = ['http', 'https']
22+
if base_url:
23+
url_components = urlparse.urlparse(base_url)
24+
default_host = url_components.netloc
25+
default_scheme = url_components.scheme
26+
else:
27+
default_host = ''
28+
default_scheme = None
29+
30+
host = _get_string(data, 'host', default=default_host)
31+
path = _get_string(data, 'basePath', default='/')
32+
33+
if not host:
34+
# No host is provided, and we do not have an initial URL.
35+
return path
36+
37+
schemes = _get_list(data, 'schemes')
38+
39+
if not schemes:
40+
# No schemes provided, use the initial URL, or a fallback.
41+
scheme = default_scheme or prefered_schemes[0]
42+
elif default_scheme in schemes:
43+
# Schemes provided, the initial URL matches one of them.
44+
scheme = default_scheme
45+
else:
46+
# Schemes provided, the initial URL does not match, pick a fallback.
47+
for scheme in prefered_schemes:
48+
if scheme in schemes:
49+
break
50+
else:
51+
raise ParseError('Unsupported transport schemes "%s"' % schemes)
52+
53+
return '%s://%s/%s' % (scheme, host, path.lstrip('/'))
54+
55+
56+
def _parse_document(data, base_url=None):
57+
schema_url = base_url
58+
base_url = _get_document_base_url(data, base_url)
59+
info = _get_dict(data, 'info')
60+
title = _get_string(info, 'title')
61+
paths = _get_dict(data, 'paths')
62+
content = {}
63+
for path in paths.keys():
64+
url = urlparse.urljoin(base_url, path)
65+
spec = _get_dict(paths, path)
66+
default_parameters = get_dicts(_get_list(spec, 'parameters'))
67+
for action in spec.keys():
68+
action = action.lower()
69+
if action not in ('get', 'put', 'post', 'delete', 'options', 'head', 'patch'):
70+
continue
71+
operation = _get_dict(spec, action)
72+
73+
# Determine any fields on the link.
74+
fields = []
75+
parameters = get_dicts(_get_list(operation, 'parameters'))
76+
for parameter in parameters:
77+
name = _get_string(parameter, 'name')
78+
location = _get_string(parameter, 'in')
79+
required = _get_bool(parameter, 'required')
80+
field = Field(name=name, location=location, required=required)
81+
fields.append(field)
82+
if location == 'body':
83+
schema = _get_dict(parameter, 'schema', dereference_using=data)
84+
expanded = expand_schema(schema)
85+
if expanded is not None:
86+
fields = [
87+
Field(name=name, location='form', required=required)
88+
for name, required in expanded
89+
]
90+
else:
91+
fields = [Field(name=name, required=True)]
92+
link = Link(url=url, action=action, fields=fields)
93+
94+
# Add the link to the document content.
95+
tags = get_strings(_get_list(operation, 'tags'))
96+
operation_id = _get_string(operation, 'operationId')
97+
if tags:
98+
for tag in tags:
99+
if tag not in content:
100+
content[tag] = {}
101+
content[tag][operation_id] = link
102+
else:
103+
content[operation_id] = link
104+
105+
return Document(url=schema_url, title=title, content=content)
106+
107+
108+
class OpenAPICodec(BaseCodec):
109+
media_type = "application/json"
110+
111+
def load(self, bytes, base_url=None):
112+
"""
113+
Takes a bytestring and returns a document.
114+
"""
115+
try:
116+
data = json.loads(bytes.decode('utf-8'))
117+
except ValueError as exc:
118+
raise ParseError('Malformed JSON. %s' % exc)
119+
120+
doc = _parse_document(data, base_url)
121+
if not isinstance(doc, Document):
122+
raise ParseError('Top level node must be a document.')
123+
124+
return doc

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy