From 1c21b8575bfd70b98b1102fd3dd09fc0bc335e14 Mon Sep 17 00:00:00 2001 From: "David M. Lee" Date: Mon, 22 Apr 2013 14:58:53 +0000 Subject: 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 --- rest-api-templates/asterisk_processor.py | 179 +++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 rest-api-templates/asterisk_processor.py (limited to 'rest-api-templates/asterisk_processor.py') 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 +# +# 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 = ' ' -- cgit v1.2.3