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:
David M. Lee
2013-04-22 14:58:53 +00:00
parent 1871017cc6
commit 1c21b8575b
63 changed files with 8605 additions and 59 deletions

View 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/

View 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 = ' '

View File

@@ -0,0 +1,4 @@
{{! A partial for the big warning, so it's not in the template itself }}
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* !!!!! DO NOT EDIT !!!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

View 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
View 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)

View 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}}

View 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}} }
};

View 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}}

View 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}}

View 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}}

View 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]))]

View 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))