summaryrefslogtreecommitdiff
path: root/rest-api-templates
diff options
context:
space:
mode:
authorDavid M. Lee <dlee@digium.com>2013-04-22 14:58:53 +0000
committerDavid M. Lee <dlee@digium.com>2013-04-22 14:58:53 +0000
commit1c21b8575bfd70b98b1102fd3dd09fc0bc335e14 (patch)
tree9a6ef6074e545ad2768bc1994e1a233fc1443729 /rest-api-templates
parent1871017cc6bd2e2ce7c638eeb6813e982377a521 (diff)
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
Diffstat (limited to 'rest-api-templates')
-rw-r--r--rest-api-templates/README.txt15
-rw-r--r--rest-api-templates/asterisk_processor.py179
-rw-r--r--rest-api-templates/do-not-edit.mustache4
-rwxr-xr-xrest-api-templates/make_stasis_http_stubs.py84
-rw-r--r--rest-api-templates/odict.py261
-rw-r--r--rest-api-templates/res_stasis_http_resource.c.mustache116
-rw-r--r--rest-api-templates/rest_handler.mustache38
-rw-r--r--rest-api-templates/stasis_http.make.mustache26
-rw-r--r--rest-api-templates/stasis_http_resource.c.mustache41
-rw-r--r--rest-api-templates/stasis_http_resource.h.mustache68
-rw-r--r--rest-api-templates/swagger_model.py482
-rw-r--r--rest-api-templates/transform.py53
12 files changed, 1367 insertions, 0 deletions
diff --git a/rest-api-templates/README.txt b/rest-api-templates/README.txt
new file mode 100644
index 000000000..e927ad768
--- /dev/null
+++ b/rest-api-templates/README.txt
@@ -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/
diff --git a/rest-api-templates/asterisk_processor.py b/rest-api-templates/asterisk_processor.py
new file mode 100644
index 000000000..81aefbb39
--- /dev/null
+++ b/rest-api-templates/asterisk_processor.py
@@ -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 = ' '
diff --git a/rest-api-templates/do-not-edit.mustache b/rest-api-templates/do-not-edit.mustache
new file mode 100644
index 000000000..05ba14276
--- /dev/null
+++ b/rest-api-templates/do-not-edit.mustache
@@ -0,0 +1,4 @@
+{{! A partial for the big warning, so it's not in the template itself }}
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * !!!!! DO NOT EDIT !!!!!
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/rest-api-templates/make_stasis_http_stubs.py b/rest-api-templates/make_stasis_http_stubs.py
new file mode 100755
index 000000000..1ec7c5c91
--- /dev/null
+++ b/rest-api-templates/make_stasis_http_stubs.py
@@ -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)
diff --git a/rest-api-templates/odict.py b/rest-api-templates/odict.py
new file mode 100644
index 000000000..8f536a2b8
--- /dev/null
+++ b/rest-api-templates/odict.py
@@ -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)
diff --git a/rest-api-templates/res_stasis_http_resource.c.mustache b/rest-api-templates/res_stasis_http_resource.c.mustache
new file mode 100644
index 000000000..b02ab62bd
--- /dev/null
+++ b/rest-api-templates/res_stasis_http_resource.c.mustache
@@ -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}}
diff --git a/rest-api-templates/rest_handler.mustache b/rest-api-templates/rest_handler.mustache
new file mode 100644
index 000000000..a7dfc60e8
--- /dev/null
+++ b/rest-api-templates/rest_handler.mustache
@@ -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}} }
+};
diff --git a/rest-api-templates/stasis_http.make.mustache b/rest-api-templates/stasis_http.make.mustache
new file mode 100644
index 000000000..103eb2bff
--- /dev/null
+++ b/rest-api-templates/stasis_http.make.mustache
@@ -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}}
diff --git a/rest-api-templates/stasis_http_resource.c.mustache b/rest-api-templates/stasis_http_resource.c.mustache
new file mode 100644
index 000000000..7a5535511
--- /dev/null
+++ b/rest-api-templates/stasis_http_resource.c.mustache
@@ -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}}
diff --git a/rest-api-templates/stasis_http_resource.h.mustache b/rest-api-templates/stasis_http_resource.h.mustache
new file mode 100644
index 000000000..6e7af1648
--- /dev/null
+++ b/rest-api-templates/stasis_http_resource.h.mustache
@@ -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}}
diff --git a/rest-api-templates/swagger_model.py b/rest-api-templates/swagger_model.py
new file mode 100644
index 000000000..c42bb7086
--- /dev/null
+++ b/rest-api-templates/swagger_model.py
@@ -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]))]
diff --git a/rest-api-templates/transform.py b/rest-api-templates/transform.py
new file mode 100644
index 000000000..d0ef3c4a1
--- /dev/null
+++ b/rest-api-templates/transform.py
@@ -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))