nitrestful: generate code for async restful methods
[nit.git] / src / nitrestful.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Tool generating boilerplate code linking RESTful actions to Nit methods
16 module nitrestful
17
18 import gen_nit
19
20 import frontend
21
22 private class RestfulPhase
23 super Phase
24
25 # Classes with methods marked with the `restful` annotation
26 var restful_classes = new HashSet[MClass]
27
28 redef fun process_annotated_node(node, nat)
29 do
30 # Skip if we are not interested
31 var text = nat.n_atid.n_id.text
32 if text != "restful" then return
33
34 if not node isa AMethPropdef then
35 toolcontext.error(nat.location,
36 "Syntax Error: `restful` can only be applied on method definitions")
37 return
38 end
39
40 var mpropdef = node.mpropdef
41 if mpropdef == null then return
42 var mproperty = mpropdef.mproperty
43 var mclassdef = mpropdef.mclassdef
44 var mmodule = mclassdef.mmodule
45
46 var http_resources = new Array[String]
47 var http_methods = new Array[String]
48 for arg in nat.n_args do
49 var str = arg.as_string
50 var id = arg.collect_text
51 if str != null then
52 # String -> rename resource
53 http_resources.add str
54 else if arg isa ATypeExpr and not id.chars.has("[") then
55 # Class id -> HTTP method
56 http_methods.add id
57 else if id == "async" then
58 mproperty.restful_async = true
59 else
60 toolcontext.error(nat.location,
61 "Syntax Error: `restful` expects String literals or ids as arguments.")
62 return
63 end
64 end
65
66 # Test subclass of `RestfulAction`
67 var sup_class_name = "RestfulAction"
68 var sup_class = toolcontext.modelbuilder.try_get_mclass_by_name(
69 nat, mmodule, sup_class_name)
70 var in_hierarchy = mclassdef.in_hierarchy
71 if in_hierarchy == null or sup_class == null then return
72 var sup_classes = in_hierarchy.greaters
73 if not sup_classes.has(sup_class.intro) then
74 toolcontext.error(nat.location,
75 "Syntax Error: `restful` is only valid within subclasses of `{sup_class_name}`")
76 return
77 end
78
79 # Register the property
80 var mclass = mclassdef.mclass
81 mclass.restful_methods.add mproperty
82 restful_classes.add mclass
83
84 if http_resources.not_empty then mproperty.restful_resources = http_resources
85 mproperty.restful_verbs = http_methods
86 end
87 end
88
89 redef class MClass
90
91 # Methods with the `restful` annotation in this class
92 private var restful_methods = new Array[MMethod]
93 end
94
95 redef class MMethod
96 # HTTP access methods, e.g. `GET, POST, PUT or DELETE`
97 private var restful_verbs = new Array[String] is lazy
98
99 # Associated resources within an action, e.g. `foo` in `http://localhost/foo?arg=bar`
100 private var restful_resources: Array[String] = [name] is lazy
101
102 # Is this a `restful` method to be executed asynchronously
103 private var restful_async = false
104 end
105
106 redef class ToolContext
107 # Generate serialization and deserialization methods on `auto_serializable` annotated classes.
108 var restful_phase: Phase = new RestfulPhase(self, [modelize_class_phase])
109
110 # Where do we put a single result?
111 var opt_output: OptionString = new OptionString("Output file (can also be 'stdout')", "-o", "--output")
112
113 # Where do we put the result?
114 var opt_dir: OptionString = new OptionString("Output directory", "--dir")
115
116 redef init
117 do
118 option_context.add_option(opt_output, opt_dir)
119 super
120 end
121 end
122
123 redef class MType
124 # Write code in `template` to parse the argument `arg_name` to this parameter type
125 private fun gen_arg_convert(template: Template, arg_name: String)
126 do
127 if self.name == "String" or self.name == "nullable String" then
128 # String are used as is
129 template.add """
130 var out_{{{arg_name}}} = in_{{{arg_name}}}
131 """
132 else
133 # Deserialize everything else
134 template.add """
135 var out_{{{arg_name}}} = deserialize_arg(in_{{{arg_name}}})
136 """
137 end
138 end
139
140 # Does this parameter type needs to be checked before calling the method?
141 #
142 # Some nullable types do not need to be check as `null` values are acceptable.
143 private fun needs_type_check: Bool do return true
144 end
145
146 redef class MNullableType
147 redef fun needs_type_check do return name != "nullable String" and name != "nullable Object"
148 end
149
150 var toolcontext = new ToolContext
151 toolcontext.tooldescription = """
152 Usage: nitrestful [OPTION] module.nit [other_module.nit [...]]
153 Generates the boilerplate code to link RESTful request to static Nit methods."""
154
155 toolcontext.process_options args
156 var arguments = toolcontext.option_context.rest
157
158 # Check options
159 if toolcontext.opt_output.value != null and toolcontext.opt_dir.value != null then
160 print "Error: cannot use both --dir and --output"
161 exit 1
162 end
163 if arguments.length > 1 and toolcontext.opt_output.value != null then
164 print "Error: --output needs a single source file. Do you prefer --dir?"
165 exit 1
166 end
167
168 var model = new Model
169 var modelbuilder = new ModelBuilder(model, toolcontext)
170
171 var mmodules = modelbuilder.parse(arguments)
172 modelbuilder.run_phases
173 var first_mmodule = mmodules.first
174
175 # Name of the support module
176 var module_name
177
178 # Path to the support module
179 var module_path = toolcontext.opt_output.value
180 if module_path == null then
181 module_name = "{first_mmodule.name}_rest"
182 module_path = "{module_name}.nit"
183
184 var dir = toolcontext.opt_dir.value
185 if dir != null then module_path = dir.join_path(module_path)
186 else if module_path == "stdout" then
187 module_name = "{first_mmodule.name}_rest"
188 module_path = null
189 else if module_path.has_suffix(".nit") then
190 module_name = module_path.basename(".nit")
191 else
192 module_name = module_path.basename
193 module_path += ".nit"
194 end
195
196 var nit_module = new NitModule(module_name)
197 nit_module.annotations.add """no_warning("parentheses")"""
198 nit_module.header = """
199 # This file is generated by nitrestful
200 # Do not modify, instead refine the generated services.
201 """
202
203 for mmod in mmodules do
204 nit_module.imports.add mmod.name
205 end
206
207 var phase = toolcontext.restful_phase
208 assert phase isa RestfulPhase
209
210 for mclass in phase.restful_classes do
211
212 var t = new Template
213 nit_module.content.add t
214
215 var classes = new Template
216 nit_module.content.add classes
217
218 t.add """
219 redef class {{{mclass}}}
220 redef fun prepare_respond_and_close(request, truncated_uri, http_server)
221 do
222 var resources = truncated_uri.split("/")
223 if resources.not_empty and resources.first.is_empty then resources.shift
224
225 if resources.length != 1 then
226 super
227 return
228 end
229 var resource = resources.first
230
231 """
232 var methods = mclass.restful_methods
233 for i in methods.length.times, method in methods do
234 var msig = method.intro.msignature
235 if msig == null then continue
236
237 # Condition to select this method from a request
238 var conds = new Array[String]
239
240 # Name of the resource from the method or as renamed
241 var resource_conds = new Array[String]
242 for resource in method.restful_resources do resource_conds.add "resource == \"{resource}\""
243 conds.add "(" + resource_conds.join(" or ") + ")"
244
245 # HTTP methods/verbs
246 if method.restful_verbs.not_empty then
247 var method_conds = new Array[String]
248 for meth in method.restful_verbs do method_conds.add "request.method == \"{meth}\""
249 conds.add "(" + method_conds.join(" or ") + ")"
250 end
251
252 t.add """
253 if {{{conds.join(" and ")}}} then
254 """
255
256 # Extract the arguments from the request for the method call
257 var args = new Array[String]
258 var isas = new Array[String]
259 for param in msig.mparameters do
260
261 t.add """
262 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
263 """
264
265 var mtype = param.mtype
266 mtype.gen_arg_convert(t, param.name)
267
268 var arg = "out_{param.name}"
269 args.add arg
270
271 if mtype.needs_type_check then
272 isas.add "{arg} isa {mtype.name}"
273 end
274
275 t.add "\n"
276 end
277
278 if isas.not_empty then t.add """
279 if {{{isas.join(" and ")}}} then
280 """
281
282 var sig = ""
283 if args.not_empty then sig = "({args.join(", ")})"
284
285 if not method.restful_async then
286 # Synchronous method
287 t.add """
288 var response = {{{method.name}}}{{{sig}}}
289 http_server.respond response
290 http_server.close
291 return
292 """
293 else
294 # Asynchronous method
295 var task_name = "Task_{mclass}_{method.name}"
296 args.unshift "http_server"
297 args.unshift "request"
298 args.unshift "self"
299
300 t.add """
301 var task = new {{{task_name}}}({{{args.join(", ")}}})
302 self.thread_pool.execute task
303 return
304 """
305
306 var thread_attribs = new Array[String]
307 for param in msig.mparameters do
308 thread_attribs.add """
309 private var out_{{{param.name}}}: {{{param.mtype}}}"""
310 end
311
312 classes.add """
313
314 # Generated task to execute {{{mclass}}}::{{{method.name}}}
315 class {{{task_name}}}
316 super RestfulTask
317
318 redef type A: {{{mclass}}}
319
320 {{{thread_attribs.join("\n")}}}
321
322 redef fun indirect_restful_method
323 do
324 return action.{{{method.name}}}{{{sig}}}
325 end
326 end
327 """
328 end
329
330 if isas.not_empty then t.add """
331 end
332 """
333 t.add """
334 end
335 """
336 end
337
338 t.add """
339 super
340 end
341 end"""
342 end
343
344 # Write support module
345 if module_path != null then
346 # To file
347 nit_module.write_to_file module_path
348 else
349 # To stdout
350 nit_module.write_to stdout
351 end