nitrestful: detect the async keyword
[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 t.add """
216 redef class {{{mclass}}}
217 redef fun prepare_respond_and_close(request, truncated_uri, http_server)
218 do
219 var resources = truncated_uri.split("/")
220 if resources.not_empty and resources.first.is_empty then resources.shift
221
222 if resources.length != 1 then
223 super
224 return
225 end
226 var resource = resources.first
227
228 """
229 var methods = mclass.restful_methods
230 for i in methods.length.times, method in methods do
231 var msig = method.intro.msignature
232 if msig == null then continue
233
234 # Condition to select this method from a request
235 var conds = new Array[String]
236
237 # Name of the resource from the method or as renamed
238 var resource_conds = new Array[String]
239 for resource in method.restful_resources do resource_conds.add "resource == \"{resource}\""
240 conds.add "(" + resource_conds.join(" or ") + ")"
241
242 # HTTP methods/verbs
243 if method.restful_verbs.not_empty then
244 var method_conds = new Array[String]
245 for meth in method.restful_verbs do method_conds.add "request.method == \"{meth}\""
246 conds.add "(" + method_conds.join(" or ") + ")"
247 end
248
249 t.add """
250 if {{{conds.join(" and ")}}} then
251 """
252
253 # Extract the arguments from the request for the method call
254 var args = new Array[String]
255 var isas = new Array[String]
256 for param in msig.mparameters do
257
258 t.add """
259 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
260 """
261
262 var mtype = param.mtype
263 mtype.gen_arg_convert(t, param.name)
264
265 var arg = "out_{param.name}"
266 args.add arg
267
268 if mtype.needs_type_check then
269 isas.add "{arg} isa {mtype.name}"
270 end
271
272 t.add "\n"
273 end
274
275 if isas.not_empty then t.add """
276 if {{{isas.join(" and ")}}} then
277 """
278
279 var sig = ""
280 if args.not_empty then sig = "({args.join(", ")})"
281
282 t.add """
283 var response = {{{method.name}}}{{{sig}}}
284 http_server.respond response
285 http_server.close
286 return
287 """
288
289 if isas.not_empty then t.add """
290 end
291 """
292 t.add """
293 end
294 """
295 end
296
297 t.add """
298 super
299 end
300 end"""
301 end
302
303 # Write support module
304 if module_path != null then
305 # To file
306 nit_module.write_to_file module_path
307 else
308 # To stdout
309 nit_module.write_to stdout
310 end