Merge: doc: fixed some typos and other misc. corrections
[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 """generated"""
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 mclassdef in phase.restful_classes do
211 var mclass = mclassdef.mclass
212
213 var t = new Template
214 nit_module.content.add t
215
216 var classes = new Template
217 nit_module.content.add classes
218
219 t.add """
220 redef class {{{mclass}}}
221 redef fun prepare_respond_and_close(request, truncated_uri, http_server)
222 do
223 var resources = truncated_uri.split("/")
224 if resources.not_empty and resources.first.is_empty then resources.shift
225
226 if resources.length != 1 then
227 super
228 return
229 end
230 var resource = resources.first
231
232 """
233 var methods = mclassdef.restful_methods
234 for i in methods.length.times, method in methods do
235 var msig = method.intro.msignature
236 if msig == null then continue
237
238 # Condition to select this method from a request
239 var conds = new Array[String]
240
241 # Name of the resource from the method or as renamed
242 var resource_conds = new Array[String]
243 for resource in method.restful_resources do resource_conds.add "resource == \"{resource}\""
244 conds.add "(" + resource_conds.join(" or ") + ")"
245
246 # HTTP methods/verbs
247 if method.restful_verbs.not_empty then
248 var method_conds = new Array[String]
249 for meth in method.restful_verbs do method_conds.add "request.method == \"{meth}\""
250 conds.add "(" + method_conds.join(" or ") + ")"
251 end
252
253 t.add """
254 if {{{conds.join(" and ")}}} then
255 """
256
257 # Extract the arguments from the request for the method call
258 var args = new Array[String]
259 var isas = new Array[String]
260 for param in msig.mparameters do
261
262 t.add """
263 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
264 """
265
266 var mtype = param.mtype
267 var bound_mtype = mclassdef.bound_mtype
268 var resolved_mtype = mtype.resolve_for(bound_mtype, bound_mtype, mclassdef.mmodule, true)
269 var resolved_type_name = resolved_mtype.name
270
271 resolved_mtype.gen_arg_convert(t, param.name)
272
273 var arg = "out_{param.name}"
274 args.add arg
275
276 if mtype.needs_type_check then
277 isas.add "{arg} isa {mtype.name}"
278 end
279
280 t.add "\n"
281 end
282
283 if isas.not_empty then t.add """
284 if {{{isas.join(" and ")}}} then
285 """
286
287 var sig = ""
288 if args.not_empty then sig = "({args.join(", ")})"
289
290 if not method.restful_async then
291 # Synchronous method
292 t.add """
293 var response = {{{method.name}}}{{{sig}}}
294 http_server.respond response
295 http_server.close
296 return
297 """
298 else
299 # Asynchronous method
300 var task_name = "Task_{mclass}_{method.name}"
301 args.unshift "http_server"
302 args.unshift "request"
303 args.unshift "self"
304
305 t.add """
306 var task = new {{{task_name}}}({{{args.join(", ")}}})
307 self.thread_pool.execute task
308 return
309 """
310
311 var thread_attribs = new Array[String]
312 for param in msig.mparameters do
313 thread_attribs.add """
314 private var out_{{{param.name}}}: {{{param.mtype}}}"""
315 end
316
317 classes.add """
318
319 # Generated task to execute {{{mclass}}}::{{{method.name}}}
320 class {{{task_name}}}
321 super RestfulTask
322
323 redef type A: {{{mclass}}}
324
325 {{{thread_attribs.join("\n")}}}
326
327 redef fun indirect_restful_method
328 do
329 return action.{{{method.name}}}{{{sig}}}
330 end
331 end
332 """
333 end
334
335 if isas.not_empty then t.add """
336 end
337 """
338 t.add """
339 end
340 """
341 end
342
343 t.add """
344 super
345 end
346 end"""
347 end
348
349 # Write support module
350 if module_path != null then
351 # To file
352 nit_module.write_to_file module_path
353 else
354 # To stdout
355 nit_module.write_to stdout
356 end