mirror of
https://github.com/asterisk/asterisk.git
synced 2025-10-11 23:28:59 +00:00
This patch adds a RESTful HTTP interface to Asterisk.
The API itself is documented using Swagger, a lightweight mechanism for documenting RESTful API's using JSON. This allows us to use swagger-ui to provide executable documentation for the API, generate client bindings in different languages, and generate a lot of the boilerplate code for implementing the RESTful bindings. The API docs live in the rest-api/ directory. The RESTful bindings are generated from the Swagger API docs using a set of Mustache templates. The code generator is written in Python, and uses Pystache. Pystache has no dependencies, and be installed easily using pip. Code generation code lives in rest-api-templates/. The generated code reduces a lot of boilerplate when it comes to handling HTTP requests. It also helps us have greater consistency in the REST API. (closes issue ASTERISK-20891) Review: https://reviewboard.asterisk.org/r/2376/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@386232 65c4cc65-6c06-0410-ace0-fbb531ad65f3
This commit is contained in:
15
rest-api-templates/README.txt
Normal file
15
rest-api-templates/README.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
This directory contains templates and template processing code for generating
|
||||
HTTP bindings for the RESTful API's.
|
||||
|
||||
The RESTful API's are declared using [Swagger][swagger]. While Swagger provides
|
||||
a [code generating toolkit][swagger-codegen], it requires Java to run, which
|
||||
would be an unusual dependency to require for Asterisk developers.
|
||||
|
||||
This code generator is similar, but written in Python. Templates are processed
|
||||
by using [pystache][pystache], which is a fairly simply Python implementation of
|
||||
[mustache][mustache].
|
||||
|
||||
[swagger]: https://github.com/wordnik/swagger-core/wiki
|
||||
[swagger-codegen]: https://github.com/wordnik/swagger-codegen
|
||||
[pystache]: https://github.com/defunkt/pystache
|
||||
[mustache]: http://mustache.github.io/
|
179
rest-api-templates/asterisk_processor.py
Normal file
179
rest-api-templates/asterisk_processor.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#
|
||||
# Asterisk -- An open source telephony toolkit.
|
||||
#
|
||||
# Copyright (C) 2013, Digium, Inc.
|
||||
#
|
||||
# David M. Lee, II <dlee@digium.com>
|
||||
#
|
||||
# See http://www.asterisk.org for more information about
|
||||
# the Asterisk project. Please do not directly contact
|
||||
# any of the maintainers of this project for assistance;
|
||||
# the project provides a web site, mailing lists and IRC
|
||||
# channels for your use.
|
||||
#
|
||||
# This program is free software, distributed under the terms of
|
||||
# the GNU General Public License Version 2. See the LICENSE file
|
||||
# at the top of the source tree.
|
||||
#
|
||||
|
||||
"""Implementation of SwaggerPostProcessor which adds fields needed to generate
|
||||
Asterisk RESTful HTTP binding code.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from swagger_model import *
|
||||
|
||||
|
||||
def simple_name(name):
|
||||
"""Removes the {markers} from a path segement.
|
||||
|
||||
@param name: Swagger path segement, with {pathVar} markers.
|
||||
"""
|
||||
if name.startswith('{') and name.endswith('}'):
|
||||
return name[1:-1]
|
||||
return name
|
||||
|
||||
|
||||
def snakify(name):
|
||||
"""Helper to take a camelCase or dash-seperated name and make it
|
||||
snake_case.
|
||||
"""
|
||||
r = ''
|
||||
prior_lower = False
|
||||
for c in name:
|
||||
if c.isupper() and prior_lower:
|
||||
r += "_"
|
||||
if c is '-':
|
||||
c = '_'
|
||||
prior_lower = c.islower()
|
||||
r += c.lower()
|
||||
return r
|
||||
|
||||
|
||||
class PathSegment(Stringify):
|
||||
"""Tree representation of a Swagger API declaration.
|
||||
"""
|
||||
def __init__(self, name, parent):
|
||||
"""Ctor.
|
||||
|
||||
@param name: Name of this path segment. May have {pathVar} markers.
|
||||
@param parent: Parent PathSegment.
|
||||
"""
|
||||
#: Segment name, with {pathVar} markers removed
|
||||
self.name = simple_name(name)
|
||||
#: True if segment is a {pathVar}, else None.
|
||||
self.is_wildcard = None
|
||||
#: Underscore seperated name all ancestor segments
|
||||
self.full_name = None
|
||||
#: Dictionary of child PathSegements
|
||||
self.__children = OrderedDict()
|
||||
#: List of operations on this segement
|
||||
self.operations = []
|
||||
|
||||
if self.name != name:
|
||||
self.is_wildcard = True
|
||||
|
||||
if not self.name:
|
||||
assert(not parent)
|
||||
self.full_name = ''
|
||||
if not parent or not parent.name:
|
||||
self.full_name = name
|
||||
else:
|
||||
self.full_name = "%s_%s" % (parent.full_name, self.name)
|
||||
|
||||
def get_child(self, path):
|
||||
"""Walks decendents to get path, creating it if necessary.
|
||||
|
||||
@param path: List of path names.
|
||||
@return: PageSegment corresponding to path.
|
||||
"""
|
||||
assert simple_name(path[0]) == self.name
|
||||
if (len(path) == 1):
|
||||
return self
|
||||
child = self.__children.get(path[1])
|
||||
if not child:
|
||||
child = PathSegment(path[1], self)
|
||||
self.__children[path[1]] = child
|
||||
return child.get_child(path[1:])
|
||||
|
||||
def children(self):
|
||||
"""Gets list of children.
|
||||
"""
|
||||
return self.__children.values()
|
||||
|
||||
def num_children(self):
|
||||
"""Gets count of children.
|
||||
"""
|
||||
return len(self.__children)
|
||||
|
||||
|
||||
class AsteriskProcessor(SwaggerPostProcessor):
|
||||
"""A SwaggerPostProcessor which adds fields needed to generate Asterisk
|
||||
RESTful HTTP binding code.
|
||||
"""
|
||||
|
||||
#: How Swagger types map to C.
|
||||
type_mapping = {
|
||||
'string': 'const char *',
|
||||
'boolean': 'int',
|
||||
'number': 'int',
|
||||
'int': 'int',
|
||||
'long': 'long',
|
||||
'double': 'double',
|
||||
'float': 'float',
|
||||
}
|
||||
|
||||
#: String conversion functions for string to C type.
|
||||
convert_mapping = {
|
||||
'const char *': '',
|
||||
'int': 'atoi',
|
||||
'long': 'atol',
|
||||
'double': 'atof',
|
||||
}
|
||||
|
||||
def process_api(self, resource_api, context):
|
||||
# Derive a resource name from the API declaration's filename
|
||||
resource_api.name = re.sub('\..*', '',
|
||||
os.path.basename(resource_api.path))
|
||||
# Now in all caps, from include guard
|
||||
resource_api.name_caps = resource_api.name.upper()
|
||||
# Construct the PathSegement tree for the API.
|
||||
if resource_api.api_declaration:
|
||||
resource_api.root_path = PathSegment('', None)
|
||||
for api in resource_api.api_declaration.apis:
|
||||
segment = resource_api.root_path.get_child(api.path.split('/'))
|
||||
for operation in api.operations:
|
||||
segment.operations.append(operation)
|
||||
# Since every API path should start with /[resource], root should
|
||||
# have exactly one child.
|
||||
if resource_api.root_path.num_children() != 1:
|
||||
raise SwaggerError(
|
||||
"Should not mix resources in one API declaration", context)
|
||||
# root_path isn't needed any more
|
||||
resource_api.root_path = resource_api.root_path.children()[0]
|
||||
if resource_api.name != resource_api.root_path.name:
|
||||
raise SwaggerError(
|
||||
"API declaration name should match", context)
|
||||
resource_api.root_full_name = resource_api.root_path.full_name
|
||||
|
||||
def process_operation(self, operation, context):
|
||||
# Nicknames are camelcase, Asterisk coding is snake case
|
||||
operation.c_nickname = snakify(operation.nickname)
|
||||
operation.c_http_method = 'AST_HTTP_' + operation.http_method
|
||||
if not operation.summary.endswith("."):
|
||||
raise SwaggerError("Summary should end with .", context)
|
||||
|
||||
def process_parameter(self, parameter, context):
|
||||
if not parameter.data_type in self.type_mapping:
|
||||
raise SwaggerError(
|
||||
"Invalid parameter type %s" % paramter.data_type, context)
|
||||
# Parameter names are camelcase, Asterisk convention is snake case
|
||||
parameter.c_name = snakify(parameter.name)
|
||||
parameter.c_data_type = self.type_mapping[parameter.data_type]
|
||||
parameter.c_convert = self.convert_mapping[parameter.c_data_type]
|
||||
# You shouldn't put a space between 'char *' and the variable
|
||||
if parameter.c_data_type.endswith('*'):
|
||||
parameter.c_space = ''
|
||||
else:
|
||||
parameter.c_space = ' '
|
4
rest-api-templates/do-not-edit.mustache
Normal file
4
rest-api-templates/do-not-edit.mustache
Normal file
@@ -0,0 +1,4 @@
|
||||
{{! A partial for the big warning, so it's not in the template itself }}
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
* !!!!! DO NOT EDIT !!!!!
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
84
rest-api-templates/make_stasis_http_stubs.py
Executable file
84
rest-api-templates/make_stasis_http_stubs.py
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python
|
||||
# Asterisk -- An open source telephony toolkit.
|
||||
#
|
||||
# Copyright (C) 2013, Digium, Inc.
|
||||
#
|
||||
# David M. Lee, II <dlee@digium.com>
|
||||
#
|
||||
# See http://www.asterisk.org for more information about
|
||||
# the Asterisk project. Please do not directly contact
|
||||
# any of the maintainers of this project for assistance;
|
||||
# the project provides a web site, mailing lists and IRC
|
||||
# channels for your use.
|
||||
#
|
||||
# This program is free software, distributed under the terms of
|
||||
# the GNU General Public License Version 2. See the LICENSE file
|
||||
# at the top of the source tree.
|
||||
#
|
||||
|
||||
try:
|
||||
import pystache
|
||||
except ImportError:
|
||||
print >> sys.stderr, "Pystache required. Please sudo pip install pystache."
|
||||
|
||||
import os.path
|
||||
import pystache
|
||||
import sys
|
||||
|
||||
from asterisk_processor import AsteriskProcessor
|
||||
from optparse import OptionParser
|
||||
from swagger_model import *
|
||||
from transform import Transform
|
||||
|
||||
TOPDIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def rel(file):
|
||||
"""Helper to get a file relative to the script's directory
|
||||
|
||||
@parm file: Relative file path.
|
||||
"""
|
||||
return os.path.join(TOPDIR, file)
|
||||
|
||||
API_TRANSFORMS = [
|
||||
Transform(rel('res_stasis_http_resource.c.mustache'),
|
||||
'res_stasis_http_{{name}}.c'),
|
||||
Transform(rel('stasis_http_resource.h.mustache'),
|
||||
'stasis_http/resource_{{name}}.h'),
|
||||
Transform(rel('stasis_http_resource.c.mustache'),
|
||||
'stasis_http/resource_{{name}}.c', False),
|
||||
]
|
||||
|
||||
RESOURCES_TRANSFORMS = [
|
||||
Transform(rel('stasis_http.make.mustache'), 'stasis_http.make'),
|
||||
]
|
||||
|
||||
|
||||
def main(argv):
|
||||
parser = OptionParser(usage="Usage %prog [resources.json] [destdir]")
|
||||
|
||||
(options, args) = parser.parse_args(argv)
|
||||
|
||||
if len(args) != 3:
|
||||
parser.error("Wrong number of arguments")
|
||||
|
||||
source = args[1]
|
||||
dest_dir = args[2]
|
||||
renderer = pystache.Renderer(search_dirs=[TOPDIR], missing_tags='strict')
|
||||
processor = AsteriskProcessor()
|
||||
|
||||
# Build the models
|
||||
base_dir = os.path.dirname(source)
|
||||
resources = ResourceListing().load_file(source, processor)
|
||||
for api in resources.apis:
|
||||
api.load_api_declaration(base_dir, processor)
|
||||
|
||||
# Render the templates
|
||||
for api in resources.apis:
|
||||
for transform in API_TRANSFORMS:
|
||||
transform.render(renderer, api, dest_dir)
|
||||
for transform in RESOURCES_TRANSFORMS:
|
||||
transform.render(renderer, resources, dest_dir)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv) or 0)
|
261
rest-api-templates/odict.py
Normal file
261
rest-api-templates/odict.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# Downloaded from http://code.activestate.com/recipes/576693/
|
||||
# Licensed under the MIT License
|
||||
|
||||
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
|
||||
# Passes Python2.7's test suite and incorporates all the latest updates.
|
||||
|
||||
try:
|
||||
from thread import get_ident as _get_ident
|
||||
except ImportError:
|
||||
from dummy_thread import get_ident as _get_ident
|
||||
|
||||
try:
|
||||
from _abcoll import KeysView, ValuesView, ItemsView
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class OrderedDict(dict):
|
||||
'Dictionary that remembers insertion order'
|
||||
# An inherited dict maps keys to values.
|
||||
# The inherited dict provides __getitem__, __len__, __contains__, and get.
|
||||
# The remaining methods are order-aware.
|
||||
# Big-O running times for all methods are the same as for regular dictionaries.
|
||||
|
||||
# The internal self.__map dictionary maps keys to links in a doubly linked list.
|
||||
# The circular doubly linked list starts and ends with a sentinel element.
|
||||
# The sentinel element never gets deleted (this simplifies the algorithm).
|
||||
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
'''Initialize an ordered dictionary. Signature is the same as for
|
||||
regular dictionaries, but keyword arguments are not recommended
|
||||
because their insertion order is arbitrary.
|
||||
|
||||
'''
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__root
|
||||
except AttributeError:
|
||||
self.__root = root = [] # sentinel node
|
||||
root[:] = [root, root, None]
|
||||
self.__map = {}
|
||||
self.__update(*args, **kwds)
|
||||
|
||||
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
|
||||
'od.__setitem__(i, y) <==> od[i]=y'
|
||||
# Setting a new item creates a new link which goes at the end of the linked
|
||||
# list, and the inherited dictionary is updated with the new key/value pair.
|
||||
if key not in self:
|
||||
root = self.__root
|
||||
last = root[0]
|
||||
last[1] = root[0] = self.__map[key] = [last, root, key]
|
||||
dict_setitem(self, key, value)
|
||||
|
||||
def __delitem__(self, key, dict_delitem=dict.__delitem__):
|
||||
'od.__delitem__(y) <==> del od[y]'
|
||||
# Deleting an existing item uses self.__map to find the link which is
|
||||
# then removed by updating the links in the predecessor and successor nodes.
|
||||
dict_delitem(self, key)
|
||||
link_prev, link_next, key = self.__map.pop(key)
|
||||
link_prev[1] = link_next
|
||||
link_next[0] = link_prev
|
||||
|
||||
def __iter__(self):
|
||||
'od.__iter__() <==> iter(od)'
|
||||
root = self.__root
|
||||
curr = root[1]
|
||||
while curr is not root:
|
||||
yield curr[2]
|
||||
curr = curr[1]
|
||||
|
||||
def __reversed__(self):
|
||||
'od.__reversed__() <==> reversed(od)'
|
||||
root = self.__root
|
||||
curr = root[0]
|
||||
while curr is not root:
|
||||
yield curr[2]
|
||||
curr = curr[0]
|
||||
|
||||
def clear(self):
|
||||
'od.clear() -> None. Remove all items from od.'
|
||||
try:
|
||||
for node in self.__map.itervalues():
|
||||
del node[:]
|
||||
root = self.__root
|
||||
root[:] = [root, root, None]
|
||||
self.__map.clear()
|
||||
except AttributeError:
|
||||
pass
|
||||
dict.clear(self)
|
||||
|
||||
def popitem(self, last=True):
|
||||
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
|
||||
Pairs are returned in LIFO order if last is true or FIFO order if false.
|
||||
|
||||
'''
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
root = self.__root
|
||||
if last:
|
||||
link = root[0]
|
||||
link_prev = link[0]
|
||||
link_prev[1] = root
|
||||
root[0] = link_prev
|
||||
else:
|
||||
link = root[1]
|
||||
link_next = link[1]
|
||||
root[1] = link_next
|
||||
link_next[0] = root
|
||||
key = link[2]
|
||||
del self.__map[key]
|
||||
value = dict.pop(self, key)
|
||||
return key, value
|
||||
|
||||
# -- the following methods do not depend on the internal structure --
|
||||
|
||||
def keys(self):
|
||||
'od.keys() -> list of keys in od'
|
||||
return list(self)
|
||||
|
||||
def values(self):
|
||||
'od.values() -> list of values in od'
|
||||
return [self[key] for key in self]
|
||||
|
||||
def items(self):
|
||||
'od.items() -> list of (key, value) pairs in od'
|
||||
return [(key, self[key]) for key in self]
|
||||
|
||||
def iterkeys(self):
|
||||
'od.iterkeys() -> an iterator over the keys in od'
|
||||
return iter(self)
|
||||
|
||||
def itervalues(self):
|
||||
'od.itervalues -> an iterator over the values in od'
|
||||
for k in self:
|
||||
yield self[k]
|
||||
|
||||
def iteritems(self):
|
||||
'od.iteritems -> an iterator over the (key, value) items in od'
|
||||
for k in self:
|
||||
yield (k, self[k])
|
||||
|
||||
def update(*args, **kwds):
|
||||
'''od.update(E, **F) -> None. Update od from dict/iterable E and F.
|
||||
|
||||
If E is a dict instance, does: for k in E: od[k] = E[k]
|
||||
If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
|
||||
Or if E is an iterable of items, does: for k, v in E: od[k] = v
|
||||
In either case, this is followed by: for k, v in F.items(): od[k] = v
|
||||
|
||||
'''
|
||||
if len(args) > 2:
|
||||
raise TypeError('update() takes at most 2 positional '
|
||||
'arguments (%d given)' % (len(args),))
|
||||
elif not args:
|
||||
raise TypeError('update() takes at least 1 argument (0 given)')
|
||||
self = args[0]
|
||||
# Make progressively weaker assumptions about "other"
|
||||
other = ()
|
||||
if len(args) == 2:
|
||||
other = args[1]
|
||||
if isinstance(other, dict):
|
||||
for key in other:
|
||||
self[key] = other[key]
|
||||
elif hasattr(other, 'keys'):
|
||||
for key in other.keys():
|
||||
self[key] = other[key]
|
||||
else:
|
||||
for key, value in other:
|
||||
self[key] = value
|
||||
for key, value in kwds.items():
|
||||
self[key] = value
|
||||
|
||||
__update = update # let subclasses override update without breaking __init__
|
||||
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
||||
If key is not found, d is returned if given, otherwise KeyError is raised.
|
||||
|
||||
'''
|
||||
if key in self:
|
||||
result = self[key]
|
||||
del self[key]
|
||||
return result
|
||||
if default is self.__marker:
|
||||
raise KeyError(key)
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
|
||||
if key in self:
|
||||
return self[key]
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def __repr__(self, _repr_running={}):
|
||||
'od.__repr__() <==> repr(od)'
|
||||
call_key = id(self), _get_ident()
|
||||
if call_key in _repr_running:
|
||||
return '...'
|
||||
_repr_running[call_key] = 1
|
||||
try:
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
finally:
|
||||
del _repr_running[call_key]
|
||||
|
||||
def __reduce__(self):
|
||||
'Return state information for pickling'
|
||||
items = [[k, self[k]] for k in self]
|
||||
inst_dict = vars(self).copy()
|
||||
for k in vars(OrderedDict()):
|
||||
inst_dict.pop(k, None)
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def copy(self):
|
||||
'od.copy() -> a shallow copy of od'
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
|
||||
and values equal to v (which defaults to None).
|
||||
|
||||
'''
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
|
||||
while comparison to a regular mapping is order-insensitive.
|
||||
|
||||
'''
|
||||
if isinstance(other, OrderedDict):
|
||||
return len(self)==len(other) and self.items() == other.items()
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
# -- the following methods are only used in Python 2.7 --
|
||||
|
||||
def viewkeys(self):
|
||||
"od.viewkeys() -> a set-like object providing a view on od's keys"
|
||||
return KeysView(self)
|
||||
|
||||
def viewvalues(self):
|
||||
"od.viewvalues() -> an object providing a view on od's values"
|
||||
return ValuesView(self)
|
||||
|
||||
def viewitems(self):
|
||||
"od.viewitems() -> a set-like object providing a view on od's items"
|
||||
return ItemsView(self)
|
116
rest-api-templates/res_stasis_http_resource.c.mustache
Normal file
116
rest-api-templates/res_stasis_http_resource.c.mustache
Normal file
@@ -0,0 +1,116 @@
|
||||
{{#api_declaration}}
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* {{{copyright}}}
|
||||
*
|
||||
* {{{author}}}
|
||||
{{! Template Copyright
|
||||
* Copyright (C) 2013, Digium, Inc.
|
||||
*
|
||||
* David M. Lee, II <dlee@digium.com>
|
||||
}}
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
{{! Template for rendering the res_ module for an HTTP resource. }}
|
||||
/*
|
||||
{{> do-not-edit}}
|
||||
* This file is generated by a mustache template. Please see the original
|
||||
* template in rest-api-templates/res_stasis_http_resource.c.mustache
|
||||
*/
|
||||
|
||||
/*! \file
|
||||
*
|
||||
* \brief {{{description}}}
|
||||
*
|
||||
* \author {{{author}}}
|
||||
*/
|
||||
|
||||
/*** MODULEINFO
|
||||
<depend type="module">res_stasis_http</depend>
|
||||
<support_level>core</support_level>
|
||||
***/
|
||||
|
||||
#include "asterisk.h"
|
||||
|
||||
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
|
||||
|
||||
#include "asterisk/module.h"
|
||||
#include "stasis_http/resource_{{name}}.h"
|
||||
|
||||
{{#apis}}
|
||||
{{#operations}}
|
||||
/*!
|
||||
* \brief Parameter parsing callback for {{path}}.
|
||||
* \param get_params GET parameters in the HTTP request.
|
||||
* \param path_vars Path variables extracted from the request.
|
||||
* \param headers HTTP headers.
|
||||
* \param[out] response Response to the HTTP request.
|
||||
*/
|
||||
static void stasis_http_{{c_nickname}}_cb(
|
||||
struct ast_variable *get_params, struct ast_variable *path_vars,
|
||||
struct ast_variable *headers, struct stasis_http_response *response)
|
||||
{
|
||||
struct ast_{{c_nickname}}_args args = {};
|
||||
{{#has_parameters}}
|
||||
struct ast_variable *i;
|
||||
|
||||
{{#has_query_parameters}}
|
||||
for (i = get_params; i; i = i->next) {
|
||||
{{#query_parameters}}
|
||||
if (strcmp(i->name, "{{name}}") == 0) {
|
||||
args.{{c_name}} = {{c_convert}}(i->value);
|
||||
} else
|
||||
{{/query_parameters}}
|
||||
{}
|
||||
}
|
||||
{{/has_query_parameters}}
|
||||
{{#has_path_parameters}}
|
||||
for (i = path_vars; i; i = i->next) {
|
||||
{{#path_parameters}}
|
||||
if (strcmp(i->name, "{{name}}") == 0) {
|
||||
args.{{c_name}} = {{c_convert}}(i->value);
|
||||
} else
|
||||
{{/path_parameters}}
|
||||
{}
|
||||
}
|
||||
{{/has_path_parameters}}
|
||||
{{/has_parameters}}
|
||||
stasis_http_{{c_nickname}}(headers, &args, response);
|
||||
}
|
||||
{{/operations}}
|
||||
{{/apis}}
|
||||
|
||||
{{! The rest_handler partial expands to the tree of stasis_rest_handlers }}
|
||||
{{#root_path}}
|
||||
{{> rest_handler}}
|
||||
{{/root_path}}
|
||||
|
||||
static int load_module(void)
|
||||
{
|
||||
return stasis_http_add_handler(&{{root_full_name}});
|
||||
}
|
||||
|
||||
static int unload_module(void)
|
||||
{
|
||||
stasis_http_remove_handler(&{{root_full_name}});
|
||||
return 0;
|
||||
}
|
||||
|
||||
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT,
|
||||
"RESTful API module - {{{description}}}",
|
||||
.load = load_module,
|
||||
.unload = unload_module,
|
||||
.nonoptreq = "res_stasis_http",
|
||||
);
|
||||
{{/api_declaration}}
|
38
rest-api-templates/rest_handler.mustache
Normal file
38
rest-api-templates/rest_handler.mustache
Normal file
@@ -0,0 +1,38 @@
|
||||
{{! -*- C -*-
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* Copyright (C) 2013, Digium, Inc.
|
||||
*
|
||||
* David M. Lee, II <dlee@digium.com>
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
}}
|
||||
{{!
|
||||
* Recursive partial template to render a rest_handler. Used in
|
||||
* res_stasis_http_resource.c.mustache.
|
||||
}}
|
||||
{{#children}}
|
||||
{{> rest_handler}}
|
||||
{{/children}}
|
||||
/*! \brief REST handler for {{path}} */
|
||||
static struct stasis_rest_handlers {{full_name}} = {
|
||||
.path_segment = "{{name}}",
|
||||
{{#is_wildcard}}
|
||||
.is_wildcard = 1,
|
||||
{{/is_wildcard}}
|
||||
.callbacks = {
|
||||
{{#operations}}
|
||||
[{{c_http_method}}] = stasis_http_{{c_nickname}}_cb,
|
||||
{{/operations}}
|
||||
},
|
||||
.num_children = {{num_children}},
|
||||
.children = { {{#children}}&{{full_name}},{{/children}} }
|
||||
};
|
26
rest-api-templates/stasis_http.make.mustache
Normal file
26
rest-api-templates/stasis_http.make.mustache
Normal file
@@ -0,0 +1,26 @@
|
||||
{{! -*- Makefile -*- }}
|
||||
#
|
||||
# Asterisk -- A telephony toolkit for Linux.
|
||||
#
|
||||
# Generated Makefile for res_stasis_http dependencies.
|
||||
#
|
||||
# Copyright (C) 2013, Digium, Inc.
|
||||
#
|
||||
# This program is free software, distributed under the terms of
|
||||
# the GNU General Public License
|
||||
#
|
||||
|
||||
#
|
||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
# !!!!! DO NOT EDIT !!!!!
|
||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
# This file is generated by a template. Please see the original template at
|
||||
# rest-api-templates/stasis_http.make.mustache
|
||||
#
|
||||
|
||||
{{#apis}}
|
||||
res_stasis_http_{{name}}.so: stasis_http/resource_{{name}}.o
|
||||
|
||||
stasis_http/resource_{{name}}.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_stasis_http_{{name}})
|
||||
|
||||
{{/apis}}
|
41
rest-api-templates/stasis_http_resource.c.mustache
Normal file
41
rest-api-templates/stasis_http_resource.c.mustache
Normal file
@@ -0,0 +1,41 @@
|
||||
{{#api_declaration}}
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* {{{copyright}}}
|
||||
*
|
||||
* {{{author}}}
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
/*! \file
|
||||
*
|
||||
* \brief {{{resource_path}}} implementation- {{{description}}}
|
||||
*
|
||||
* \author {{{author}}}
|
||||
*/
|
||||
|
||||
#include "asterisk.h"
|
||||
|
||||
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
|
||||
|
||||
#include "resource_{{name}}.h"
|
||||
|
||||
{{#apis}}
|
||||
{{#operations}}
|
||||
void stasis_http_{{c_nickname}}(struct ast_variable *headers, struct ast_{{c_nickname}}_args *args, struct stasis_http_response *response)
|
||||
{
|
||||
ast_log(LOG_ERROR, "TODO: stasis_http_{{c_nickname}}\n");
|
||||
}
|
||||
{{/operations}}
|
||||
{{/apis}}
|
||||
{{/api_declaration}}
|
68
rest-api-templates/stasis_http_resource.h.mustache
Normal file
68
rest-api-templates/stasis_http_resource.h.mustache
Normal file
@@ -0,0 +1,68 @@
|
||||
{{#api_declaration}}
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* {{{copyright}}}
|
||||
*
|
||||
* {{{author}}}
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
/*! \file
|
||||
*
|
||||
* \brief Generated file - declares stubs to be implemented in
|
||||
* res/stasis_http/resource_{{name}}.c
|
||||
*
|
||||
* {{{description}}}
|
||||
*
|
||||
* \author {{{author}}}
|
||||
*/
|
||||
|
||||
/*
|
||||
{{> do-not-edit}}
|
||||
* This file is generated by a mustache template. Please see the original
|
||||
* template in rest-api-templates/stasis_http_resource.h.mustache
|
||||
*/
|
||||
|
||||
#ifndef _ASTERISK_RESOURCE_{{name_caps}}_H
|
||||
#define _ASTERISK_RESOURCE_{{name_caps}}_H
|
||||
|
||||
#include "asterisk/stasis_http.h"
|
||||
|
||||
{{#apis}}
|
||||
{{#operations}}
|
||||
/*! \brief Argument struct for stasis_http_{{c_nickname}}() */
|
||||
struct ast_{{c_nickname}}_args {
|
||||
{{#parameters}}
|
||||
{{#description}}
|
||||
/*! \brief {{{description}}} */
|
||||
{{/description}}
|
||||
{{c_data_type}}{{c_space}}{{c_name}};
|
||||
{{/parameters}}
|
||||
};
|
||||
/*!
|
||||
* \brief {{summary}}
|
||||
{{#notes}}
|
||||
*
|
||||
* {{{notes}}}
|
||||
{{/notes}}
|
||||
*
|
||||
* \param headers HTTP headers
|
||||
* \param args Swagger parameters
|
||||
* \param[out] response HTTP response
|
||||
*/
|
||||
void stasis_http_{{c_nickname}}(struct ast_variable *headers, struct ast_{{c_nickname}}_args *args, struct stasis_http_response *response);
|
||||
{{/operations}}
|
||||
{{/apis}}
|
||||
|
||||
#endif /* _ASTERISK_RESOURCE_{{name_caps}}_H */
|
||||
{{/api_declaration}}
|
482
rest-api-templates/swagger_model.py
Normal file
482
rest-api-templates/swagger_model.py
Normal file
@@ -0,0 +1,482 @@
|
||||
|
||||
# Asterisk -- An open source telephony toolkit.
|
||||
#
|
||||
# Copyright (C) 2013, Digium, Inc.
|
||||
#
|
||||
# David M. Lee, II <dlee@digium.com>
|
||||
#
|
||||
# See http://www.asterisk.org for more information about
|
||||
# the Asterisk project. Please do not directly contact
|
||||
# any of the maintainers of this project for assistance;
|
||||
# the project provides a web site, mailing lists and IRC
|
||||
# channels for your use.
|
||||
#
|
||||
# This program is free software, distributed under the terms of
|
||||
# the GNU General Public License Version 2. See the LICENSE file
|
||||
# at the top of the source tree.
|
||||
#
|
||||
|
||||
"""Swagger data model objects.
|
||||
|
||||
These objects should map directly to the Swagger api-docs, without a lot of
|
||||
additional fields. In the process of translation, it should also validate the
|
||||
model for consistency against the Swagger spec (i.e., fail if fields are
|
||||
missing, or have incorrect values).
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/API-Declaration for the spec.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os.path
|
||||
import pprint
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from odict import OrderedDict
|
||||
|
||||
|
||||
SWAGGER_VERSION = "1.1"
|
||||
|
||||
|
||||
class SwaggerError(Exception):
|
||||
"""Raised when an error is encountered mapping the JSON objects into the
|
||||
model.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, context, cause=None):
|
||||
"""Ctor.
|
||||
|
||||
@param msg: String message for the error.
|
||||
@param context: Array of strings for current context in the API.
|
||||
@param cause: Optional exception that caused this one.
|
||||
"""
|
||||
super(Exception, self).__init__(msg, context, cause)
|
||||
|
||||
|
||||
class SwaggerPostProcessor(object):
|
||||
"""Post processing interface for model objects. This processor can add
|
||||
fields to model objects for additional information to use in the
|
||||
templates.
|
||||
"""
|
||||
def process_api(self, resource_api, context):
|
||||
"""Post process a ResourceApi object.
|
||||
|
||||
@param resource_api: ResourceApi object.
|
||||
@param contect: Current context in the API.
|
||||
"""
|
||||
pass
|
||||
|
||||
def process_operation(self, operation, context):
|
||||
"""Post process a Operation object.
|
||||
|
||||
@param operation: Operation object.
|
||||
@param contect: Current context in the API.
|
||||
"""
|
||||
pass
|
||||
|
||||
def process_parameter(self, parameter, context):
|
||||
"""Post process a Parameter object.
|
||||
|
||||
@param parameter: Parameter object.
|
||||
@param contect: Current context in the API.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Stringify(object):
|
||||
"""Simple mix-in to make the repr of the model classes more meaningful.
|
||||
"""
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__, pprint.saferepr(self.__dict__))
|
||||
|
||||
|
||||
class AllowableRange(Stringify):
|
||||
"""Model of a allowableValues of type RANGE
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/datatypes#complex-types
|
||||
"""
|
||||
def __init__(self, min_value, max_value):
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
|
||||
|
||||
class AllowableList(Stringify):
|
||||
"""Model of a allowableValues of type LIST
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/datatypes#complex-types
|
||||
"""
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
|
||||
|
||||
def load_allowable_values(json, context):
|
||||
"""Parse a JSON allowableValues object.
|
||||
|
||||
This returns None, AllowableList or AllowableRange, depending on the
|
||||
valueType in the JSON. If the valueType is not recognized, a SwaggerError
|
||||
is raised.
|
||||
"""
|
||||
if not json:
|
||||
return None
|
||||
|
||||
if not 'valueType' in json:
|
||||
raise SwaggerError("Missing valueType field", context)
|
||||
|
||||
value_type = json['valueType']
|
||||
|
||||
if value_type == 'RANGE':
|
||||
if not 'min' in json:
|
||||
raise SwaggerError("Missing field min", context)
|
||||
if not 'max' in json:
|
||||
raise SwaggerError("Missing field max", context)
|
||||
return AllowableRange(json['min'], json['max'])
|
||||
if value_type == 'LIST':
|
||||
if not 'values' in json:
|
||||
raise SwaggerError("Missing field values", context)
|
||||
return AllowableList(json['values'])
|
||||
raise SwaggerError("Unkown valueType %s" % value_type, context)
|
||||
|
||||
|
||||
class Parameter(Stringify):
|
||||
"""Model of an operation's parameter.
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/parameters
|
||||
"""
|
||||
|
||||
required_fields = ['name', 'paramType', 'dataType']
|
||||
|
||||
def __init__(self):
|
||||
self.param_type = None
|
||||
self.name = None
|
||||
self.description = None
|
||||
self.data_type = None
|
||||
self.required = None
|
||||
self.allowable_values = None
|
||||
self.allow_multiple = None
|
||||
|
||||
def load(self, parameter_json, processor, context):
|
||||
context = add_context(context, parameter_json, 'name')
|
||||
validate_required_fields(parameter_json, self.required_fields, context)
|
||||
self.name = parameter_json.get('name')
|
||||
self.param_type = parameter_json.get('paramType')
|
||||
self.description = parameter_json.get('description') or ''
|
||||
self.data_type = parameter_json.get('dataType')
|
||||
self.required = parameter_json.get('required') or False
|
||||
self.allowable_values = load_allowable_values(
|
||||
parameter_json.get('allowableValues'), context)
|
||||
self.allow_multiple = parameter_json.get('allowMultiple') or False
|
||||
processor.process_parameter(self, context)
|
||||
return self
|
||||
|
||||
def is_type(self, other_type):
|
||||
return self.param_type == other_type
|
||||
|
||||
|
||||
class ErrorResponse(Stringify):
|
||||
"""Model of an error response.
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/errors
|
||||
"""
|
||||
|
||||
required_fields = ['code', 'reason']
|
||||
|
||||
def __init__(self):
|
||||
self.code = None
|
||||
self.reason = None
|
||||
|
||||
def load(self, err_json, processor, context):
|
||||
context = add_context(context, err_json, 'code')
|
||||
validate_required_fields(err_json, self.required_fields, context)
|
||||
self.code = err_json.get('code')
|
||||
self.reason = err_json.get('reason')
|
||||
return self
|
||||
|
||||
|
||||
class Operation(Stringify):
|
||||
"""Model of an operation on an API
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/API-Declaration#apis
|
||||
"""
|
||||
|
||||
required_fields = ['httpMethod', 'nickname', 'responseClass', 'summary']
|
||||
|
||||
def __init__(self):
|
||||
self.http_method = None
|
||||
self.nickname = None
|
||||
self.response_class = None
|
||||
self.parameters = []
|
||||
self.summary = None
|
||||
self.notes = None
|
||||
self.error_responses = []
|
||||
|
||||
def load(self, op_json, processor, context):
|
||||
context = add_context(context, op_json, 'nickname')
|
||||
validate_required_fields(op_json, self.required_fields, context)
|
||||
self.http_method = op_json.get('httpMethod')
|
||||
self.nickname = op_json.get('nickname')
|
||||
self.response_class = op_json.get('responseClass')
|
||||
params_json = op_json.get('parameters') or []
|
||||
self.parameters = [
|
||||
Parameter().load(j, processor, context) for j in params_json]
|
||||
self.query_parameters = [
|
||||
p for p in self.parameters if p.is_type('query')]
|
||||
self.has_query_parameters = self.query_parameters and True
|
||||
self.path_parameters = [
|
||||
p for p in self.parameters if p.is_type('path')]
|
||||
self.has_path_parameters = self.path_parameters and True
|
||||
self.header_parameters = [
|
||||
p for p in self.parameters if p.is_type('header')]
|
||||
self.has_header_parameters = self.header_parameters and True
|
||||
self.has_parameters = self.has_query_parameters or \
|
||||
self.has_path_parameters or self.has_header_parameters
|
||||
self.summary = op_json.get('summary')
|
||||
self.notes = op_json.get('notes')
|
||||
err_json = op_json.get('errorResponses') or []
|
||||
self.error_responses = [
|
||||
ErrorResponse().load(j, processor, context) for j in err_json]
|
||||
processor.process_operation(self, context)
|
||||
return self
|
||||
|
||||
|
||||
class Api(Stringify):
|
||||
"""Model of a single API in an API declaration.
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/API-Declaration
|
||||
"""
|
||||
|
||||
required_fields = ['path', 'operations']
|
||||
|
||||
def __init__(self,):
|
||||
self.path = None
|
||||
self.description = None
|
||||
self.operations = []
|
||||
|
||||
def load(self, api_json, processor, context):
|
||||
context = add_context(context, api_json, 'path')
|
||||
validate_required_fields(api_json, self.required_fields, context)
|
||||
self.path = api_json.get('path')
|
||||
self.description = api_json.get('description')
|
||||
op_json = api_json.get('operations')
|
||||
self.operations = [
|
||||
Operation().load(j, processor, context) for j in op_json]
|
||||
return self
|
||||
|
||||
|
||||
class Property(Stringify):
|
||||
"""Model of a Swagger property.
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/datatypes
|
||||
"""
|
||||
|
||||
required_fields = ['type']
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.type = None
|
||||
self.description = None
|
||||
self.required = None
|
||||
|
||||
def load(self, property_json, processor, context):
|
||||
validate_required_fields(property_json, self.required_fields, context)
|
||||
self.type = property_json.get('type')
|
||||
self.description = property_json.get('description') or ''
|
||||
self.required = property_json.get('required') or False
|
||||
return self
|
||||
|
||||
|
||||
class Model(Stringify):
|
||||
"""Model of a Swagger model.
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/datatypes
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.id = None
|
||||
self.properties = None
|
||||
|
||||
def load(self, model_json, processor, context):
|
||||
context = add_context(context, model_json, 'id')
|
||||
self.id = model_json.get('id')
|
||||
props = model_json.get('properties').items() or []
|
||||
self.properties = [
|
||||
Property(k).load(j, processor, context) for (k, j) in props]
|
||||
return self
|
||||
|
||||
|
||||
class ApiDeclaration(Stringify):
|
||||
"""Model class for an API Declaration.
|
||||
|
||||
See https://github.com/wordnik/swagger-core/wiki/API-Declaration
|
||||
"""
|
||||
|
||||
required_fields = [
|
||||
'swaggerVersion', '_author', '_copyright', 'apiVersion', 'basePath',
|
||||
'resourcePath', 'apis', 'models'
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.swagger_version = None
|
||||
self.author = None
|
||||
self.copyright = None
|
||||
self.api_version = None
|
||||
self.base_path = None
|
||||
self.resource_path = None
|
||||
self.apis = []
|
||||
self.models = []
|
||||
|
||||
def load_file(self, api_declaration_file, processor, context=[]):
|
||||
context = context + [api_declaration_file]
|
||||
try:
|
||||
return self.__load_file(api_declaration_file, processor, context)
|
||||
except SwaggerError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print >> sys.stderr, "Error: ", traceback.format_exc()
|
||||
raise SwaggerError(
|
||||
"Error loading %s" % api_declaration_file, context, e)
|
||||
|
||||
def __load_file(self, api_declaration_file, processor, context):
|
||||
with open(api_declaration_file) as fp:
|
||||
self.load(json.load(fp), processor, context)
|
||||
|
||||
expected_resource_path = '/api-docs/' + \
|
||||
os.path.basename(api_declaration_file) \
|
||||
.replace(".json", ".{format}")
|
||||
|
||||
if self.resource_path != expected_resource_path:
|
||||
print "%s != %s" % (self.resource_path, expected_resource_path)
|
||||
raise SwaggerError("resourcePath has incorrect value", context)
|
||||
|
||||
return self
|
||||
|
||||
def load(self, api_decl_json, processor, context):
|
||||
"""Loads a resource from a single Swagger resource.json file.
|
||||
"""
|
||||
# If the version doesn't match, all bets are off.
|
||||
self.swagger_version = api_decl_json.get('swaggerVersion')
|
||||
if self.swagger_version != SWAGGER_VERSION:
|
||||
raise SwaggerError(
|
||||
"Unsupported Swagger version %s" % swagger_version, context)
|
||||
|
||||
validate_required_fields(api_decl_json, self.required_fields, context)
|
||||
|
||||
self.author = api_decl_json.get('_author')
|
||||
self.copyright = api_decl_json.get('_copyright')
|
||||
self.api_version = api_decl_json.get('apiVersion')
|
||||
self.base_path = api_decl_json.get('basePath')
|
||||
self.resource_path = api_decl_json.get('resourcePath')
|
||||
api_json = api_decl_json.get('apis') or []
|
||||
self.apis = [
|
||||
Api().load(j, processor, context) for j in api_json]
|
||||
models = api_decl_json.get('models').items() or []
|
||||
self.models = OrderedDict(
|
||||
(k, Model().load(j, processor, context)) for (k, j) in models)
|
||||
|
||||
for (name, model) in self.models.items():
|
||||
c = list(context).append('model = %s' % name)
|
||||
if name != model.id:
|
||||
raise SwaggerError("Model id doesn't match name", c)
|
||||
return self
|
||||
|
||||
|
||||
class ResourceApi(Stringify):
|
||||
"""Model of an API listing in the resources.json file.
|
||||
"""
|
||||
|
||||
required_fields = ['path', 'description']
|
||||
|
||||
def __init__(self):
|
||||
self.path = None
|
||||
self.description = None
|
||||
self.api_declaration = None
|
||||
|
||||
def load(self, api_json, processor, context):
|
||||
context = add_context(context, api_json, 'path')
|
||||
validate_required_fields(api_json, self.required_fields, context)
|
||||
self.path = api_json['path']
|
||||
self.description = api_json['description']
|
||||
|
||||
if not self.path or self.path[0] != '/':
|
||||
raise SwaggerError("Path must start with /", context)
|
||||
processor.process_api(self, context)
|
||||
return self
|
||||
|
||||
def load_api_declaration(self, base_dir, processor):
|
||||
self.file = (base_dir + self.path).replace('{format}', 'json')
|
||||
self.api_declaration = ApiDeclaration().load_file(self.file, processor)
|
||||
processor.process_api(self, [self.file])
|
||||
|
||||
|
||||
class ResourceListing(Stringify):
|
||||
"""Model of Swagger's resources.json file.
|
||||
"""
|
||||
|
||||
required_fields = ['apiVersion', 'basePath', 'apis']
|
||||
|
||||
def __init__(self):
|
||||
self.swagger_version = None
|
||||
self.api_version = None
|
||||
self.base_path = None
|
||||
self.apis = None
|
||||
|
||||
def load_file(self, resource_file, processor):
|
||||
context = [resource_file]
|
||||
try:
|
||||
return self.__load_file(resource_file, processor, context)
|
||||
except SwaggerError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print >> sys.stderr, "Error: ", traceback.format_exc()
|
||||
raise SwaggerError(
|
||||
"Error loading %s" % resource_file, context, e)
|
||||
|
||||
def __load_file(self, resource_file, processor, context):
|
||||
with open(resource_file) as fp:
|
||||
return self.load(json.load(fp), processor, context)
|
||||
|
||||
def load(self, resources_json, processor, context):
|
||||
# If the version doesn't match, all bets are off.
|
||||
self.swagger_version = resources_json.get('swaggerVersion')
|
||||
if self.swagger_version != SWAGGER_VERSION:
|
||||
raise SwaggerError(
|
||||
"Unsupported Swagger version %s" % swagger_version, context)
|
||||
|
||||
validate_required_fields(resources_json, self.required_fields, context)
|
||||
self.api_version = resources_json['apiVersion']
|
||||
self.base_path = resources_json['basePath']
|
||||
apis_json = resources_json['apis']
|
||||
self.apis = [
|
||||
ResourceApi().load(j, processor, context) for j in apis_json]
|
||||
return self
|
||||
|
||||
|
||||
def validate_required_fields(json, required_fields, context):
|
||||
"""Checks a JSON object for a set of required fields.
|
||||
|
||||
If any required field is missing, a SwaggerError is raised.
|
||||
|
||||
@param json: JSON object to check.
|
||||
@param required_fields: List of required fields.
|
||||
@param context: Current context in the API.
|
||||
"""
|
||||
missing_fields = [f for f in required_fields if not f in json]
|
||||
|
||||
if missing_fields:
|
||||
raise SwaggerError(
|
||||
"Missing fields: %s" % ', '.join(missing_fields), context)
|
||||
|
||||
|
||||
def add_context(context, json, id_field):
|
||||
"""Returns a new context with a new item added to it.
|
||||
|
||||
@param context: Old context.
|
||||
@param json: Current JSON object.
|
||||
@param id_field: Field identifying this object.
|
||||
@return New context with additional item.
|
||||
"""
|
||||
if not id_field in json:
|
||||
raise SwaggerError("Missing id_field: %s" % id_field, context)
|
||||
return context + ['%s=%s' % (id_field, str(json[id_field]))]
|
53
rest-api-templates/transform.py
Normal file
53
rest-api-templates/transform.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#
|
||||
# Asterisk -- An open source telephony toolkit.
|
||||
#
|
||||
# Copyright (C) 2013, Digium, Inc.
|
||||
#
|
||||
# David M. Lee, II <dlee@digium.com>
|
||||
#
|
||||
# See http://www.asterisk.org for more information about
|
||||
# the Asterisk project. Please do not directly contact
|
||||
# any of the maintainers of this project for assistance;
|
||||
# the project provides a web site, mailing lists and IRC
|
||||
# channels for your use.
|
||||
#
|
||||
# This program is free software, distributed under the terms of
|
||||
# the GNU General Public License Version 2. See the LICENSE file
|
||||
# at the top of the source tree.
|
||||
#
|
||||
|
||||
import os.path
|
||||
import pystache
|
||||
|
||||
|
||||
class Transform(object):
|
||||
"""Transformation for template to code.
|
||||
"""
|
||||
def __init__(self, template_file, dest_file_template_str, overwrite=True):
|
||||
"""Ctor.
|
||||
|
||||
@param template_file: Filename of the mustache template.
|
||||
@param dest_file_template_str: Destination file name. This is a
|
||||
mustache template, so each resource can write to a unique file.
|
||||
@param overwrite: If True, destination file is ovewritten if it exists.
|
||||
"""
|
||||
template_str = unicode(open(template_file, "r").read())
|
||||
self.template = pystache.parse(template_str)
|
||||
dest_file_template_str = unicode(dest_file_template_str)
|
||||
self.dest_file_template = pystache.parse(dest_file_template_str)
|
||||
self.overwrite = overwrite
|
||||
|
||||
def render(self, renderer, model, dest_dir):
|
||||
"""Render a model according to this transformation.
|
||||
|
||||
@param render: Pystache renderer.
|
||||
@param model: Model object to render.
|
||||
@param dest_dir: Destination directory to write generated code.
|
||||
"""
|
||||
dest_file = pystache.render(self.dest_file_template, model)
|
||||
dest_file = os.path.join(dest_dir, dest_file)
|
||||
if os.path.exists(dest_file) and not self.overwrite:
|
||||
return
|
||||
print "Rendering %s" % dest_file
|
||||
with open(dest_file, "w") as out:
|
||||
out.write(renderer.render(self.template, model))
|
Reference in New Issue
Block a user