nitrestful: fix naming of "resources" as recommended
[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
43 var mproperty = mpropdef.mproperty
44 var mclassdef = mpropdef.mclassdef
45 var mmodule = mclassdef.mmodule
46
47 # Test subclass of `RestfulAction`
48 var sup_class_name = "RestfulAction"
49 var sup_class = toolcontext.modelbuilder.try_get_mclass_by_name(
50 nat, mmodule, sup_class_name)
51 var in_hierarchy = mclassdef.in_hierarchy
52 if in_hierarchy == null or sup_class == null then return
53 var sup_classes = in_hierarchy.greaters
54 if not sup_classes.has(sup_class.intro) then
55 toolcontext.error(nat.location,
56 "Syntax Error: `restful` is only valid within subclasses of `{sup_class_name}`")
57 return
58 end
59
60 # Register the property
61 var mclass = mclassdef.mclass
62 mclass.restful_methods.add mproperty
63 restful_classes.add mclass
64 end
65 end
66
67 redef class MClass
68
69 # Methods with the `restful` annotation in this class
70 private var restful_methods = new Array[MMethod]
71 end
72
73 redef class ToolContext
74 # Generate serialization and deserialization methods on `auto_serializable` annotated classes.
75 var restful_phase: Phase = new RestfulPhase(self, [modelize_class_phase])
76
77 # Where do we put a single result?
78 var opt_output: OptionString = new OptionString("Output file (can also be 'stdout')", "-o", "--output")
79
80 # Where do we put the result?
81 var opt_dir: OptionString = new OptionString("Output directory", "--dir")
82
83 redef init
84 do
85 option_context.add_option(opt_output, opt_dir)
86 super
87 end
88 end
89
90 redef class MType
91 # Write code in `template` to parse the argument `arg_name` to this parameter type
92 private fun gen_arg_convert(template: Template, arg_name: String)
93 do
94 if self.name == "String" or self.name == "nullable String" then
95 # String are used as is
96 template.add """
97 var out_{{{arg_name}}} = in_{{{arg_name}}}
98 """
99 else
100 # Deserialize everything else
101 template.add """
102 var out_{{{arg_name}}} = deserialize_arg(in_{{{arg_name}}})
103 """
104 end
105 end
106
107 # Does this parameter type needs to be checked before calling the method?
108 #
109 # Some nullable types do not need to be check as `null` values are acceptable.
110 private fun needs_type_check: Bool do return true
111 end
112
113 redef class MNullableType
114 redef fun needs_type_check do return name != "nullable String" and name != "nullable Object"
115 end
116
117 var toolcontext = new ToolContext
118 toolcontext.tooldescription = """
119 Usage: nitrestful [OPTION] module.nit [other_module.nit [...]]
120 Generates the boilerplate code to link RESTful request to static Nit methods."""
121
122 toolcontext.process_options args
123 var arguments = toolcontext.option_context.rest
124
125 # Check options
126 if toolcontext.opt_output.value != null and toolcontext.opt_dir.value != null then
127 print "Error: cannot use both --dir and --output"
128 exit 1
129 end
130 if arguments.length > 1 and toolcontext.opt_output.value != null then
131 print "Error: --output needs a single source file. Do you prefer --dir?"
132 exit 1
133 end
134
135 var model = new Model
136 var modelbuilder = new ModelBuilder(model, toolcontext)
137
138 var mmodules = modelbuilder.parse(arguments)
139 modelbuilder.run_phases
140 var first_mmodule = mmodules.first
141
142 # Name of the support module
143 var module_name
144
145 # Path to the support module
146 var module_path = toolcontext.opt_output.value
147 if module_path == null then
148 module_name = "{first_mmodule.name}_rest"
149 module_path = "{module_name}.nit"
150
151 var dir = toolcontext.opt_dir.value
152 if dir != null then module_path = dir.join_path(module_path)
153 else if module_path == "stdout" then
154 module_name = "{first_mmodule.name}_rest"
155 module_path = null
156 else if module_path.has_suffix(".nit") then
157 module_name = module_path.basename(".nit")
158 else
159 module_name = module_path.basename
160 module_path += ".nit"
161 end
162
163 var nit_module = new NitModule(module_name)
164 nit_module.header = """
165 # This file is generated by nitrestful
166 # Do not modify, instead refine the generated services.
167 """
168
169 for mmod in mmodules do
170 nit_module.imports.add mmod.name
171 end
172
173 var phase = toolcontext.restful_phase
174 assert phase isa RestfulPhase
175
176 for mclass in phase.restful_classes do
177
178 var t = new Template
179 nit_module.content.add t
180
181 t.add """
182 redef class {{{mclass}}}
183 redef fun answer(request, truncated_uri)
184 do
185 var resources = truncated_uri.split("/")
186 if resources.not_empty and resources.first.is_empty then resources.shift
187
188 if resources.length != 1 then return super
189 var resource = resources.first
190
191 """
192 var methods = mclass.restful_methods
193 for i in methods.length.times, method in methods do
194 var msig = method.intro.msignature
195 if msig == null then continue
196
197 t.add " "
198 if i != 0 then t.add "else "
199
200 t.add """if resource == "{{{method.name}}}" then
201 """
202
203 var args = new Array[String]
204 var isas = new Array[String]
205 for param in msig.mparameters do
206
207 t.add """
208 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
209 """
210
211 var mtype = param.mtype
212 mtype.gen_arg_convert(t, param.name)
213
214 var arg = "out_{param.name}"
215 args.add arg
216
217 if mtype.needs_type_check then
218 isas.add "{arg} isa {mtype.name}"
219 end
220
221 t.add "\n"
222 end
223
224 if isas.not_empty then t.add """
225 if not {{{isas.join(" or not ")}}} then
226 return super
227 end
228 """
229
230 var sig = ""
231 if args.not_empty then sig = "({args.join(", ")})"
232
233 t.add """
234 return {{{method.name}}}{{{sig}}}
235 """
236 end
237
238 t.add """
239 end
240 return super
241 end
242 end"""
243 end
244
245 # Write support module
246 if module_path != null then
247 # To file
248 nit_module.write_to_file module_path
249 else
250 # To stdout
251 nit_module.write_to stdout
252 end