diff options
Diffstat (limited to 'rest-api-templates')
-rw-r--r-- | rest-api-templates/README.txt | 15 | ||||
-rw-r--r-- | rest-api-templates/asterisk_processor.py | 179 | ||||
-rw-r--r-- | rest-api-templates/do-not-edit.mustache | 4 | ||||
-rwxr-xr-x | rest-api-templates/make_stasis_http_stubs.py | 84 | ||||
-rw-r--r-- | rest-api-templates/odict.py | 261 | ||||
-rw-r--r-- | rest-api-templates/res_stasis_http_resource.c.mustache | 116 | ||||
-rw-r--r-- | rest-api-templates/rest_handler.mustache | 38 | ||||
-rw-r--r-- | rest-api-templates/stasis_http.make.mustache | 26 | ||||
-rw-r--r-- | rest-api-templates/stasis_http_resource.c.mustache | 41 | ||||
-rw-r--r-- | rest-api-templates/stasis_http_resource.h.mustache | 68 | ||||
-rw-r--r-- | rest-api-templates/swagger_model.py | 482 | ||||
-rw-r--r-- | rest-api-templates/transform.py | 53 |
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)) |