nitrestful & lib: use static type to limit and as heuristic at deserialization
[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[MClassDef]
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 mclassdef.restful_methods.add mproperty
81 restful_classes.add mclassdef
82
83 if http_resources.not_empty then mproperty.restful_resources = http_resources
84 mproperty.restful_verbs = http_methods
85 end
86 end
87
88 redef class MClassDef
89
90 # Methods with the `restful` annotation in this class
91 private var restful_methods = new Array[MMethod]
92 end
93
94 redef class MMethod
95 # HTTP access methods, e.g. `GET, POST, PUT or DELETE`
96 private var restful_verbs = new Array[String] is lazy
97
98 # Associated resources within an action, e.g. `foo` in `http://localhost/foo?arg=bar`
99 private var restful_resources: Array[String] = [name] is lazy
100
101 # Is this a `restful` method to be executed asynchronously
102 private var restful_async = false
103 end
104
105 redef class ToolContext
106 # Generate serialization and deserialization methods on `auto_serializable` annotated classes.
107 var restful_phase: Phase = new RestfulPhase(self, [modelize_class_phase])
108
109 # Where do we put a single result?
110 var opt_output: OptionString = new OptionString("Output file (can also be 'stdout')", "-o", "--output")
111
112 # Where do we put the result?
113 var opt_dir: OptionString = new OptionString("Output directory", "--dir")
114
115 redef init
116 do
117 option_context.add_option(opt_output, opt_dir)
118 super
119 end
120 end
121
122 redef class MType
123 # Write code in `template` to parse the argument `arg_name` to this parameter type
124 private fun gen_arg_convert(template: Template, arg_name: String)
125 do
126 if self.name == "String" or self.name == "nullable String" then
127 # String are used as is
128 template.add """
129 var out_{{{arg_name}}} = in_{{{arg_name}}}
130 """
131 else
132 # Deserialize everything else
133 template.add """
134 var out_{{{arg_name}}} = deserialize_arg(in_{{{arg_name}}}, "{{{self.name}}}")
135 """
136 end
137 end
138
139 # Does this parameter type needs to be checked before calling the method?
140 #
141 # Some nullable types do not need to be check as `null` values are acceptable.
142 private fun needs_type_check: Bool do return true
143 end
144
145 redef class MNullableType
146 redef fun needs_type_check do return name != "nullable String" and name != "nullable Object"
147 end
148
149 var toolcontext = new ToolContext
150 toolcontext.tooldescription = """
151 Usage: nitrestful [OPTION] module.nit [other_module.nit [...]]
152 Generates the boilerplate code to link RESTful request to static Nit methods."""
153
154 toolcontext.process_options args
155 var arguments = toolcontext.option_context.rest
156
157 # Check options
158 if toolcontext.opt_output.value != null and toolcontext.opt_dir.value != null then
159 print "Error: cannot use both --dir and --output"
160 exit 1
161 end
162 if arguments.length > 1 and toolcontext.opt_output.value != null then
163 print "Error: --output needs a single source file. Do you prefer --dir?"
164 exit 1
165 end
166
167 var model = new Model
168 var modelbuilder = new ModelBuilder(model, toolcontext)
169
170 var mmodules = modelbuilder.parse(arguments)
171 modelbuilder.run_phases
172 var first_mmodule = mmodules.first
173
174 # Name of the support module
175 var module_name
176
177 # Path to the support module
178 var module_path = toolcontext.opt_output.value
179 if module_path == null then
180 module_name = "{first_mmodule.name}_rest"
181 module_path = "{module_name}.nit"
182
183 var dir = toolcontext.opt_dir.value
184 if dir != null then module_path = dir.join_path(module_path)
185 else if module_path == "stdout" then
186 module_name = "{first_mmodule.name}_rest"
187 module_path = null
188 else if module_path.has_suffix(".nit") then
189 module_name = module_path.basename(".nit")
190 else
191 module_name = module_path.basename
192 module_path += ".nit"
193 end
194
195 var nit_module = new NitModule(module_name)
196 nit_module.annotations.add """no_warning("parentheses")"""
197 nit_module.header = """
198 # This file is generated by nitrestful
199 # Do not modify, instead refine the generated services.
200 """
201
202 for mmod in mmodules do
203 nit_module.imports.add mmod.name
204 end
205
206 var phase = toolcontext.restful_phase
207 assert phase isa RestfulPhase
208
209 for mclassdef in phase.restful_classes do
210 var mclass = mclassdef.mclass
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 = mclassdef.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 var bound_mtype = mclassdef.bound_mtype
267 var resolved_mtype = mtype.resolve_for(bound_mtype, bound_mtype, mclassdef.mmodule, true)
268 var resolved_type_name = resolved_mtype.name
269
270 resolved_mtype.gen_arg_convert(t, param.name)
271
272 var arg = "out_{param.name}"
273 args.add arg
274
275 if mtype.needs_type_check then
276 isas.add "{arg} isa {mtype.name}"
277 end
278
279 t.add "\n"
280 end
281
282 if isas.not_empty then t.add """
283 if {{{isas.join(" and ")}}} then
284 """
285
286 var sig = ""
287 if args.not_empty then sig = "({args.join(", ")})"
288
289 if not method.restful_async then
290 # Synchronous method
291 t.add """
292 var response = {{{method.name}}}{{{sig}}}
293 http_server.respond response
294 http_server.close
295 return
296 """
297 else
298 # Asynchronous method
299 var task_name = "Task_{mclass}_{method.name}"
300 args.unshift "http_server"
301 args.unshift "request"
302 args.unshift "self"
303
304 t.add """
305 var task = new {{{task_name}}}({{{args.join(", ")}}})
306 self.thread_pool.execute task
307 return
308 """
309
310 var thread_attribs = new Array[String]
311 for param in msig.mparameters do
312 thread_attribs.add """
313 private var out_{{{param.name}}}: {{{param.mtype}}}"""
314 end
315
316 classes.add """
317
318 # Generated task to execute {{{mclass}}}::{{{method.name}}}
319 class {{{task_name}}}
320 super RestfulTask
321
322 redef type A: {{{mclass}}}
323
324 {{{thread_attribs.join("\n")}}}
325
326 redef fun indirect_restful_method
327 do
328 return action.{{{method.name}}}{{{sig}}}
329 end
330 end
331 """
332 end
333
334 if isas.not_empty then t.add """
335 end
336 """
337 t.add """
338 end
339 """
340 end
341
342 t.add """
343 super
344 end
345 end"""
346 end
347
348 # Write support module
349 if module_path != null then
350 # To file
351 nit_module.write_to_file module_path
352 else
353 # To stdout
354 nit_module.write_to stdout
355 end