nitrestful: fallback on other restful services if one fails type check
[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 # Condition to select this method from a request
228 var conds = new Array[String]
229
230 # Name of the resource from the method or as renamed
231 var resource_conds = new Array[String]
232 for resource in method.restful_resources do resource_conds.add "resource == \"{resource}\""
233 conds.add "(" + resource_conds.join(" or ") + ")"
234
235 # HTTP methods/verbs
236 if method.restful_verbs.not_empty then
237 var method_conds = new Array[String]
238 for meth in method.restful_verbs do method_conds.add "request.method == \"{meth}\""
239 conds.add "(" + method_conds.join(" or ") + ")"
240 end
241
242 t.add """
243 if {{{conds.join(" and ")}}} then
244 """
245
246 # Extract the arguments from the request for the method call
247 var args = new Array[String]
248 var isas = new Array[String]
249 for param in msig.mparameters do
250
251 t.add """
252 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
253 """
254
255 var mtype = param.mtype
256 mtype.gen_arg_convert(t, param.name)
257
258 var arg = "out_{param.name}"
259 args.add arg
260
261 if mtype.needs_type_check then
262 isas.add "{arg} isa {mtype.name}"
263 end
264
265 t.add "\n"
266 end
267
268 if isas.not_empty then t.add """
269 if {{{isas.join(" and ")}}} then
270 """
271
272 var sig = ""
273 if args.not_empty then sig = "({args.join(", ")})"
274
275 t.add """
276 return {{{method.name}}}{{{sig}}}
277 """
278
279 if isas.not_empty then t.add """
280 end
281 """
282 t.add """
283 end
284 """
285 end
286
287 t.add """
288 return super
289 end
290 end"""
291 end
292
293 # Write support module
294 if module_path != null then
295 # To file
296 nit_module.write_to_file module_path
297 else
298 # To stdout
299 nit_module.write_to stdout
300 end