nitrestful: intercept at `prepare_respond_and_close` instead
[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 http_resources = new Array[String]
41 var http_methods = new Array[String]
42 for arg in nat.n_args do
43 var str = arg.as_string
44 var id = arg.collect_text
45 if str != null then
46 # String -> rename resource
47 http_resources.add str
48 else if arg isa ATypeExpr and not id.chars.has("[") then
49 # Class id -> HTTP method
50 http_methods.add id
51 else
52 toolcontext.error(nat.location,
53 "Syntax Error: `restful` expects String literals or ids as arguments.")
54 return
55 end
56 end
57
58 var mpropdef = node.mpropdef
59 if mpropdef == null then return
60
61 var mproperty = mpropdef.mproperty
62 var mclassdef = mpropdef.mclassdef
63 var mmodule = mclassdef.mmodule
64
65 # Test subclass of `RestfulAction`
66 var sup_class_name = "RestfulAction"
67 var sup_class = toolcontext.modelbuilder.try_get_mclass_by_name(
68 nat, mmodule, sup_class_name)
69 var in_hierarchy = mclassdef.in_hierarchy
70 if in_hierarchy == null or sup_class == null then return
71 var sup_classes = in_hierarchy.greaters
72 if not sup_classes.has(sup_class.intro) then
73 toolcontext.error(nat.location,
74 "Syntax Error: `restful` is only valid within subclasses of `{sup_class_name}`")
75 return
76 end
77
78 # Register the property
79 var mclass = mclassdef.mclass
80 mclass.restful_methods.add mproperty
81 restful_classes.add mclass
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 MClass
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 end
101
102 redef class ToolContext
103 # Generate serialization and deserialization methods on `auto_serializable` annotated classes.
104 var restful_phase: Phase = new RestfulPhase(self, [modelize_class_phase])
105
106 # Where do we put a single result?
107 var opt_output: OptionString = new OptionString("Output file (can also be 'stdout')", "-o", "--output")
108
109 # Where do we put the result?
110 var opt_dir: OptionString = new OptionString("Output directory", "--dir")
111
112 redef init
113 do
114 option_context.add_option(opt_output, opt_dir)
115 super
116 end
117 end
118
119 redef class MType
120 # Write code in `template` to parse the argument `arg_name` to this parameter type
121 private fun gen_arg_convert(template: Template, arg_name: String)
122 do
123 if self.name == "String" or self.name == "nullable String" then
124 # String are used as is
125 template.add """
126 var out_{{{arg_name}}} = in_{{{arg_name}}}
127 """
128 else
129 # Deserialize everything else
130 template.add """
131 var out_{{{arg_name}}} = deserialize_arg(in_{{{arg_name}}})
132 """
133 end
134 end
135
136 # Does this parameter type needs to be checked before calling the method?
137 #
138 # Some nullable types do not need to be check as `null` values are acceptable.
139 private fun needs_type_check: Bool do return true
140 end
141
142 redef class MNullableType
143 redef fun needs_type_check do return name != "nullable String" and name != "nullable Object"
144 end
145
146 var toolcontext = new ToolContext
147 toolcontext.tooldescription = """
148 Usage: nitrestful [OPTION] module.nit [other_module.nit [...]]
149 Generates the boilerplate code to link RESTful request to static Nit methods."""
150
151 toolcontext.process_options args
152 var arguments = toolcontext.option_context.rest
153
154 # Check options
155 if toolcontext.opt_output.value != null and toolcontext.opt_dir.value != null then
156 print "Error: cannot use both --dir and --output"
157 exit 1
158 end
159 if arguments.length > 1 and toolcontext.opt_output.value != null then
160 print "Error: --output needs a single source file. Do you prefer --dir?"
161 exit 1
162 end
163
164 var model = new Model
165 var modelbuilder = new ModelBuilder(model, toolcontext)
166
167 var mmodules = modelbuilder.parse(arguments)
168 modelbuilder.run_phases
169 var first_mmodule = mmodules.first
170
171 # Name of the support module
172 var module_name
173
174 # Path to the support module
175 var module_path = toolcontext.opt_output.value
176 if module_path == null then
177 module_name = "{first_mmodule.name}_rest"
178 module_path = "{module_name}.nit"
179
180 var dir = toolcontext.opt_dir.value
181 if dir != null then module_path = dir.join_path(module_path)
182 else if module_path == "stdout" then
183 module_name = "{first_mmodule.name}_rest"
184 module_path = null
185 else if module_path.has_suffix(".nit") then
186 module_name = module_path.basename(".nit")
187 else
188 module_name = module_path.basename
189 module_path += ".nit"
190 end
191
192 var nit_module = new NitModule(module_name)
193 nit_module.annotations.add """no_warning("parentheses")"""
194 nit_module.header = """
195 # This file is generated by nitrestful
196 # Do not modify, instead refine the generated services.
197 """
198
199 for mmod in mmodules do
200 nit_module.imports.add mmod.name
201 end
202
203 var phase = toolcontext.restful_phase
204 assert phase isa RestfulPhase
205
206 for mclass in phase.restful_classes do
207
208 var t = new Template
209 nit_module.content.add t
210
211 t.add """
212 redef class {{{mclass}}}
213 redef fun prepare_respond_and_close(request, truncated_uri, http_server)
214 do
215 var resources = truncated_uri.split("/")
216 if resources.not_empty and resources.first.is_empty then resources.shift
217
218 if resources.length != 1 then
219 super
220 return
221 end
222 var resource = resources.first
223
224 """
225 var methods = mclass.restful_methods
226 for i in methods.length.times, method in methods do
227 var msig = method.intro.msignature
228 if msig == null then continue
229
230 # Condition to select this method from a request
231 var conds = new Array[String]
232
233 # Name of the resource from the method or as renamed
234 var resource_conds = new Array[String]
235 for resource in method.restful_resources do resource_conds.add "resource == \"{resource}\""
236 conds.add "(" + resource_conds.join(" or ") + ")"
237
238 # HTTP methods/verbs
239 if method.restful_verbs.not_empty then
240 var method_conds = new Array[String]
241 for meth in method.restful_verbs do method_conds.add "request.method == \"{meth}\""
242 conds.add "(" + method_conds.join(" or ") + ")"
243 end
244
245 t.add """
246 if {{{conds.join(" and ")}}} then
247 """
248
249 # Extract the arguments from the request for the method call
250 var args = new Array[String]
251 var isas = new Array[String]
252 for param in msig.mparameters do
253
254 t.add """
255 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
256 """
257
258 var mtype = param.mtype
259 mtype.gen_arg_convert(t, param.name)
260
261 var arg = "out_{param.name}"
262 args.add arg
263
264 if mtype.needs_type_check then
265 isas.add "{arg} isa {mtype.name}"
266 end
267
268 t.add "\n"
269 end
270
271 if isas.not_empty then t.add """
272 if {{{isas.join(" and ")}}} then
273 """
274
275 var sig = ""
276 if args.not_empty then sig = "({args.join(", ")})"
277
278 t.add """
279 var response = {{{method.name}}}{{{sig}}}
280 http_server.respond response
281 http_server.close
282 return
283 """
284
285 if isas.not_empty then t.add """
286 end
287 """
288 t.add """
289 end
290 """
291 end
292
293 t.add """
294 super
295 end
296 end"""
297 end
298
299 # Write support module
300 if module_path != null then
301 # To file
302 nit_module.write_to_file module_path
303 else
304 # To stdout
305 nit_module.write_to stdout
306 end