b1fac013a7198e8035811f1fd43ac627e0c7943a
[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
28 def simple_name(name):
29     """Removes the {markers} from a path segement.
30
31     @param name: Swagger path segement, with {pathVar} markers.
32     """
33     if name.startswith('{') and name.endswith('}'):
34         return name[1:-1]
35     return name
36
37
38 def snakify(name):
39     """Helper to take a camelCase or dash-seperated name and make it
40     snake_case.
41     """
42     r = ''
43     prior_lower = False
44     for c in name:
45         if c.isupper() and prior_lower:
46             r += "_"
47         if c is '-':
48             c = '_'
49         prior_lower = c.islower()
50         r += c.lower()
51     return r
52
53
54 class PathSegment(Stringify):
55     """Tree representation of a Swagger API declaration.
56     """
57     def __init__(self, name, parent):
58         """Ctor.
59
60         @param name: Name of this path segment. May have {pathVar} markers.
61         @param parent: Parent PathSegment.
62         """
63         #: Segment name, with {pathVar} markers removed
64         self.name = simple_name(name)
65         #: True if segment is a {pathVar}, else None.
66         self.is_wildcard = None
67         #: Underscore seperated name all ancestor segments
68         self.full_name = None
69         #: Dictionary of child PathSegements
70         self.__children = OrderedDict()
71         #: List of operations on this segement
72         self.operations = []
73
74         if self.name != name:
75             self.is_wildcard = True
76
77         if not self.name:
78             assert(not parent)
79             self.full_name = ''
80         if not parent or not parent.name:
81             self.full_name = name
82         else:
83             self.full_name = "%s_%s" % (parent.full_name, self.name)
84
85     def get_child(self, path):
86         """Walks decendents to get path, creating it if necessary.
87
88         @param path: List of path names.
89         @return: PageSegment corresponding to path.
90         """
91         assert simple_name(path[0]) == self.name
92         if (len(path) == 1):
93             return self
94         child = self.__children.get(path[1])
95         if not child:
96             child = PathSegment(path[1], self)
97             self.__children[path[1]] = child
98         return child.get_child(path[1:])
99
100     def children(self):
101         """Gets list of children.
102         """
103         return self.__children.values()
104
105     def num_children(self):
106         """Gets count of children.
107         """
108         return len(self.__children)
109
110 class AsteriskProcessor(SwaggerPostProcessor):
111     """A SwaggerPostProcessor which adds fields needed to generate Asterisk
112     RESTful HTTP binding code.
113     """
114
115     #: How Swagger types map to C.
116     type_mapping = {
117         'string': 'const char *',
118         'boolean': 'int',
119         'number': 'int',
120         'int': 'int',
121         'long': 'long',
122         'double': 'double',
123         'float': 'float',
124     }
125
126     #: String conversion functions for string to C type.
127     convert_mapping = {
128         'const char *': '',
129         'int': 'atoi',
130         'long': 'atol',
131         'double': 'atof',
132     }
133
134     def process_api(self, resource_api, context):
135         # Derive a resource name from the API declaration's filename
136         resource_api.name = re.sub('\..*', '',
137                                    os.path.basename(resource_api.path))
138         # Now in all caps, from include guard
139         resource_api.name_caps = resource_api.name.upper()
140         # Construct the PathSegement tree for the API.
141         if resource_api.api_declaration:
142             resource_api.root_path = PathSegment('', None)
143             for api in resource_api.api_declaration.apis:
144                 segment = resource_api.root_path.get_child(api.path.split('/'))
145                 for operation in api.operations:
146                     segment.operations.append(operation)
147             resource_api.api_declaration.has_events = False
148             for model in resource_api.api_declaration.models:
149                 if model.id == "Event":
150                     resource_api.api_declaration.has_events = True
151                     break
152             if resource_api.api_declaration.has_events:
153                 resource_api.api_declaration.events = \
154                     [self.process_model(model, context) for model in \
155                         resource_api.api_declaration.models if model.id != "Event"]
156             else:
157                 resource_api.api_declaration.events = []
158
159             # Since every API path should start with /[resource], root should
160             # have exactly one child.
161             if resource_api.root_path.num_children() != 1:
162                 raise SwaggerError(
163                     "Should not mix resources in one API declaration", context)
164             # root_path isn't needed any more
165             resource_api.root_path = resource_api.root_path.children()[0]
166             if resource_api.name != resource_api.root_path.name:
167                 raise SwaggerError(
168                     "API declaration name should match", context)
169             resource_api.root_full_name = resource_api.root_path.full_name
170
171     def process_operation(self, operation, context):
172         # Nicknames are camelcase, Asterisk coding is snake case
173         operation.c_nickname = snakify(operation.nickname)
174         operation.c_http_method = 'AST_HTTP_' + operation.http_method
175         if not operation.summary.endswith("."):
176             raise SwaggerError("Summary should end with .", context)
177
178     def process_parameter(self, parameter, context):
179         if not parameter.data_type in self.type_mapping:
180             raise SwaggerError(
181                 "Invalid parameter type %s" % paramter.data_type, context)
182         # Parameter names are camelcase, Asterisk convention is snake case
183         parameter.c_name = snakify(parameter.name)
184         parameter.c_data_type = self.type_mapping[parameter.data_type]
185         parameter.c_convert = self.convert_mapping[parameter.c_data_type]
186         # You shouldn't put a space between 'char *' and the variable
187         if parameter.c_data_type.endswith('*'):
188             parameter.c_space = ''
189         else:
190             parameter.c_space = ' '
191
192     def process_model(self, model, context):
193         model.c_id = snakify(model.id)
194         model.channel = False
195         model.channel_desc = ""
196         model.bridge = False
197         model.bridge_desc = ""
198         model.properties = [self.process_property(model, prop, context) for prop in model.properties]
199         model.properties = [prop for prop in model.properties if prop]
200         model.has_properties = (len(model.properties) != 0)
201         return model
202
203     def process_property(self, model, prop, context):
204         # process channel separately since it will be pulled out
205         if prop.name == 'channel' and prop.type == 'Channel':
206             model.channel = True
207             model.channel_desc = prop.description or ""
208             return None
209
210         # process bridge separately since it will be pulled out
211         if prop.name == 'bridge' and prop.type == 'Bridge':
212             model.bridge = True
213             model.bridge_desc = prop.description or ""
214             return None
215
216         prop.c_name = snakify(prop.name)
217         if prop.type in self.type_mapping:
218             prop.c_type = self.type_mapping[prop.type]
219             prop.c_convert = self.convert_mapping[prop.c_type]
220         else:
221             prop.c_type = "Property type %s not mappable to a C type" % (prop.type)
222             prop.c_convert = "Property type %s not mappable to a C conversion" % (prop.type)
223             #raise SwaggerError(
224             #    "Invalid property type %s" % prop.type, context)
225         # You shouldn't put a space between 'char *' and the variable
226         if prop.c_type.endswith('*'):
227             prop.c_space = ''
228         else:
229             prop.c_space = ' '
230         return prop