nitc: use is_generated in various tools and generated files
[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 """generated"""
194 nit_module.annotations.add """no_warning("parentheses")"""
195 nit_module.header = """
196 # This file is generated by nitrestful
197 # Do not modify, instead refine the generated services.
198 """
199
200 for mmod in mmodules do
201 nit_module.imports.add mmod.name
202 end
203
204 var phase = toolcontext.restful_phase
205 assert phase isa RestfulPhase
206
207 for mclass in phase.restful_classes do
208
209 var t = new Template
210 nit_module.content.add t
211
212 t.add """
213 redef class {{{mclass}}}
214 redef fun answer(request, truncated_uri)
215 do
216 var resources = truncated_uri.split("/")
217 if resources.not_empty and resources.first.is_empty then resources.shift
218
219 if resources.length != 1 then return super
220 var resource = resources.first
221
222 """
223 var methods = mclass.restful_methods
224 for i in methods.length.times, method in methods do
225 var msig = method.intro.msignature
226 if msig == null then continue
227
228 t.add " "
229 if i != 0 then t.add "else "
230
231 # Condition to select this method from a request
232 var conds = new Array[String]
233
234 # Name of the resource from the method or as renamed
235 var resource_conds = new Array[String]
236 for resource in method.restful_resources do resource_conds.add "resource == \"{resource}\""
237 conds.add "(" + resource_conds.join(" or ") + ")"
238
239 # HTTP methods/verbs
240 if method.restful_verbs.not_empty then
241 var method_conds = new Array[String]
242 for meth in method.restful_verbs do method_conds.add "request.method == \"{meth}\""
243 conds.add "(" + method_conds.join(" or ") + ")"
244 end
245
246 t.add """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 not {{{isas.join(" or not ")}}} then
273 return super
274 end
275 """
276
277 var sig = ""
278 if args.not_empty then sig = "({args.join(", ")})"
279
280 t.add """
281 return {{{method.name}}}{{{sig}}}
282 """
283 end
284
285 t.add """
286 end
287 return super
288 end
289 end"""
290 end
291
292 # Write support module
293 if module_path != null then
294 # To file
295 nit_module.write_to_file module_path
296 else
297 # To stdout
298 nit_module.write_to stdout
299 end