rest-api-templates: Wikify error code response reasons
[asterisk/asterisk.git] / rest-api-templates / asterisk_processor.py
1 #
2 # Asterisk -- An open source telephony toolkit.
3 #
4 # Copyright (C) 2013, Digium, Inc.
5 #
6 # David M. Lee, II <dlee@digium.com>
7 #
8 # See http://www.asterisk.org for more information about
9 # the Asterisk project. Please do not directly contact
10 # any of the maintainers of this project for assistance;
11 # the project provides a web site, mailing lists and IRC
12 # channels for your use.
13 #
14 # This program is free software, distributed under the terms of
15 # the GNU General Public License Version 2. See the LICENSE file
16 # at the top of the source tree.
17 #
18
19 """Implementation of SwaggerPostProcessor which adds fields needed to generate
20 Asterisk RESTful HTTP binding code.
21 """
22
23 import re
24
25 from swagger_model import *
26
27 try:
28     from collections import OrderedDict
29 except ImportError:
30     from odict import OrderedDict
31
32
33 def simple_name(name):
34     """Removes the {markers} from a path segement.
35
36     @param name: Swagger path segement, with {pathVar} markers.
37     """
38     if name.startswith('{') and name.endswith('}'):
39         return name[1:-1]
40     return name
41
42
43 def wikify(str):
44     """Escapes a string for the wiki.
45
46     @param str: String to escape
47     """
48     return re.sub(r'([{}\[\]])', r'\\\1', str)
49
50
51 def snakify(name):
52     """Helper to take a camelCase or dash-seperated name and make it
53     snake_case.
54     """
55     r = ''
56     prior_lower = False
57     for c in name:
58         if c.isupper() and prior_lower:
59             r += "_"
60         if c is '-':
61             c = '_'
62         prior_lower = c.islower()
63         r += c.lower()
64     return r
65
66
67 class PathSegment(Stringify):
68     """Tree representation of a Swagger API declaration.
69     """
70     def __init__(self, name, parent):
71         """Ctor.
72
73         @param name: Name of this path segment. May have {pathVar} markers.
74         @param parent: Parent PathSegment.
75         """
76         #: Segment name, with {pathVar} markers removed
77         self.name = simple_name(name)
78         #: True if segment is a {pathVar}, else None.
79         self.is_wildcard = None
80         #: Underscore seperated name all ancestor segments
81         self.full_name = None
82         #: Dictionary of child PathSegements
83         self.__children = OrderedDict()
84         #: List of operations on this segement
85         self.operations = []
86
87         if self.name != name:
88             self.is_wildcard = True
89
90         if not self.name:
91             assert(not parent)
92             self.full_name = ''
93         if not parent or not parent.name:
94             self.full_name = name
95         else:
96             self.full_name = "%s_%s" % (parent.full_name, self.name)
97
98     def get_child(self, path):
99         """Walks decendents to get path, creating it if necessary.
100
101         @param path: List of path names.
102         @return: PageSegment corresponding to path.
103         """
104         assert simple_name(path[0]) == self.name
105         if (len(path) == 1):
106             return self
107         child = self.__children.get(path[1])
108         if not child:
109             child = PathSegment(path[1], self)
110             self.__children[path[1]] = child
111         return child.get_child(path[1:])
112
113     def children(self):
114         """Gets list of children.
115         """
116         return self.__children.values()
117
118     def num_children(self):
119         """Gets count of children.
120         """
121         return len(self.__children)
122
123
124 class AsteriskProcessor(SwaggerPostProcessor):
125     """A SwaggerPostProcessor which adds fields needed to generate Asterisk
126     RESTful HTTP binding code.
127     """
128
129     #: How Swagger types map to C.
130     type_mapping = {
131         'string': 'const char *',
132         'boolean': 'int',
133         'number': 'int',
134         'int': 'int',
135         'long': 'long',
136         'double': 'double',
137         'float': 'float',
138     }
139
140     #: String conversion functions for string to C type.
141     convert_mapping = {
142         'string': '',
143         'int': 'atoi',
144         'long': 'atol',
145         'double': 'atof',
146         'boolean': 'ast_true',
147     }
148
149     #: JSON conversion functions
150     json_convert_mapping = {
151         'string': 'ast_json_string_get',
152         'int': 'ast_json_integer_get',
153         'long': 'ast_json_integer_get',
154         'double': 'ast_json_real_get',
155         'boolean': 'ast_json_is_true',
156     }
157
158     def __init__(self, wiki_prefix):
159         self.wiki_prefix = wiki_prefix
160
161     def process_resource_api(self, resource_api, context):
162         resource_api.wiki_prefix = self.wiki_prefix
163         # Derive a resource name from the API declaration's filename
164         resource_api.name = re.sub('\..*', '',
165                                    os.path.basename(resource_api.path))
166         # Now in all caps, for include guard
167         resource_api.name_caps = resource_api.name.upper()
168         resource_api.name_title = resource_api.name.capitalize()
169         resource_api.c_name = snakify(resource_api.name)
170         # Construct the PathSegement tree for the API.
171         if resource_api.api_declaration:
172             resource_api.root_path = PathSegment('', None)
173             for api in resource_api.api_declaration.apis:
174                 segment = resource_api.root_path.get_child(api.path.split('/'))
175                 for operation in api.operations:
176                     segment.operations.append(operation)
177                 api.full_name = segment.full_name
178
179             # Since every API path should start with /[resource], root should
180             # have exactly one child.
181             if resource_api.root_path.num_children() != 1:
182                 raise SwaggerError(
183                     "Should not mix resources in one API declaration", context)
184             # root_path isn't needed any more
185             resource_api.root_path = resource_api.root_path.children()[0]
186             if resource_api.name != resource_api.root_path.name:
187                 raise SwaggerError(
188                     "API declaration name should match", context)
189             resource_api.root_full_name = resource_api.root_path.full_name
190
191     def process_api(self, api, context):
192         api.wiki_path = wikify(api.path)
193
194     def process_operation(self, operation, context):
195         # Nicknames are camelCase, Asterisk coding is snake case
196         operation.c_nickname = snakify(operation.nickname)
197         operation.c_http_method = 'AST_HTTP_' + operation.http_method
198         if not operation.summary.endswith("."):
199             raise SwaggerError("Summary should end with .", context)
200         operation.wiki_summary = wikify(operation.summary or "")
201         operation.wiki_notes = wikify(operation.notes or "")
202         for error_response in operation.error_responses:
203             error_response.wiki_reason = wikify(error_response.reason or "")
204         operation.parse_body = (operation.body_parameter or operation.has_query_parameters) and True
205
206     def process_parameter(self, parameter, context):
207         if parameter.param_type == 'body':
208             parameter.is_body_parameter = True;
209             parameter.c_data_type = 'struct ast_json *'
210         else:
211             parameter.is_body_parameter = False;
212             if not parameter.data_type in self.type_mapping:
213                 raise SwaggerError(
214                     "Invalid parameter type %s" % parameter.data_type, context)
215             # Type conversions
216             parameter.c_data_type = self.type_mapping[parameter.data_type]
217             parameter.c_convert = self.convert_mapping[parameter.data_type]
218             parameter.json_convert = self.json_convert_mapping[parameter.data_type]
219
220         # Parameter names are camelcase, Asterisk convention is snake case
221         parameter.c_name = snakify(parameter.name)
222         # You shouldn't put a space between 'char *' and the variable
223         if parameter.c_data_type.endswith('*'):
224             parameter.c_space = ''
225         else:
226             parameter.c_space = ' '
227         parameter.wiki_description = wikify(parameter.description)
228         if parameter.allowable_values:
229             parameter.wiki_allowable_values = parameter.allowable_values.to_wiki()
230         else:
231             parameter.wiki_allowable_values = None
232
233     def process_model(self, model, context):
234         model.description_dox = model.description.replace('\n', '\n * ')
235         model.description_dox = re.sub(' *\n', '\n', model.description_dox)
236         model.wiki_description = wikify(model.description)
237         model.c_id = snakify(model.id)
238         return model
239
240     def process_property(self, prop, context):
241         if "-" in prop.name:
242             raise SwaggerError("Property names cannot have dashes", context)
243         if prop.name != prop.name.lower():
244             raise SwaggerError("Property name should be all lowercase",
245                                context)
246         prop.wiki_description = wikify(prop.description)
247
248     def process_type(self, swagger_type, context):
249         swagger_type.c_name = snakify(swagger_type.name)
250         swagger_type.c_singular_name = snakify(swagger_type.singular_name)
251         swagger_type.wiki_name = wikify(swagger_type.name)