summaryrefslogtreecommitdiff
path: root/rest-api-templates/asterisk_processor.py
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/asterisk_processor.py
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/asterisk_processor.py')
-rw-r--r--rest-api-templates/asterisk_processor.py179
1 files changed, 179 insertions, 0 deletions
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 = ' '