nitrestful: support custom resource name and HTTP methods/verbs
[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 answer(request, truncated_uri)
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 return super
219 var resource = resources.first
220
221 """
222 var methods = mclass.restful_methods
223 for i in methods.length.times, method in methods do
224 var msig = method.intro.msignature
225 if msig == null then continue
226
227 t.add " "
228 if i != 0 then t.add "else "
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 """if {{{conds.join(" and ")}}} then
246 """
247
248 # Extract the arguments from the request for the method call
249 var args = new Array[String]
250 var isas = new Array[String]
251 for param in msig.mparameters do
252
253 t.add """
254 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
255 """
256
257 var mtype = param.mtype
258 mtype.gen_arg_convert(t, param.name)
259
260 var arg = "out_{param.name}"
261 args.add arg
262
263 if mtype.needs_type_check then
264 isas.add "{arg} isa {mtype.name}"
265 end
266
267 t.add "\n"
268 end
269
270 if isas.not_empty then t.add """
271 if not {{{isas.join(" or not ")}}} then
272 return super
273 end
274 """
275
276 var sig = ""
277 if args.not_empty then sig = "({args.join(", ")})"
278
279 t.add """
280 return {{{method.name}}}{{{sig}}}
281 """
282 end
283
284 t.add """
285 end
286 return super
287 end
288 end"""
289 end
290
291 # Write support module
292 if module_path != null then
293 # To file
294 nit_module.write_to_file module_path
295 else
296 # To stdout
297 nit_module.write_to stdout
298 end