1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Tool generating boilerplate code linking RESTful actions to Nit methods
22 private class RestfulPhase
25 # Classes with methods marked with the `restful` annotation
26 var restful_classes
= new HashSet[MClass]
28 redef fun process_annotated_node
(node
, nat
)
30 # Skip if we are not interested
31 var text
= nat
.n_atid
.n_id
.text
32 if text
!= "restful" then return
34 if not node
isa AMethPropdef then
35 toolcontext
.error
(nat
.location
,
36 "Syntax Error: `restful` can only be applied on method definitions")
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
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
52 toolcontext
.error
(nat
.location
,
53 "Syntax Error: `restful` expects String literals or ids as arguments.")
58 var mpropdef
= node
.mpropdef
59 if mpropdef
== null then return
61 var mproperty
= mpropdef
.mproperty
62 var mclassdef
= mpropdef
.mclassdef
63 var mmodule
= mclassdef
.mmodule
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}`")
78 # Register the property
79 var mclass
= mclassdef
.mclass
80 mclass
.restful_methods
.add mproperty
81 restful_classes
.add mclass
83 if http_resources
.not_empty
then mproperty
.restful_resources
= http_resources
84 mproperty
.restful_verbs
= http_methods
90 # Methods with the `restful` annotation in this class
91 private var restful_methods
= new Array[MMethod]
95 # HTTP access methods, e.g. `GET, POST, PUT or DELETE`
96 private var restful_verbs
= new Array[String] is lazy
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
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
])
106 # Where do we put a single result?
107 var opt_output
: OptionString = new OptionString("Output file (can also be 'stdout')", "-o", "--output")
109 # Where do we put the result?
110 var opt_dir
: OptionString = new OptionString("Output directory", "--dir")
114 option_context
.add_option
(opt_output
, opt_dir
)
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)
123 if self.name
== "String" or self.name
== "nullable String" then
124 # String are used as is
126 var out_{{{arg_name}}} = in_{{{arg_name}}}
129 # Deserialize everything else
131 var out_{{{arg_name}}} = deserialize_arg(in_{{{arg_name}}})
136 # Does this parameter type needs to be checked before calling the method?
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
142 redef class MNullableType
143 redef fun needs_type_check
do return name
!= "nullable String" and name
!= "nullable Object"
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."""
151 toolcontext
.process_options args
152 var arguments
= toolcontext
.option_context
.rest
155 if toolcontext
.opt_output
.value
!= null and toolcontext
.opt_dir
.value
!= null then
156 print
"Error: cannot use both --dir and --output"
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?"
164 var model
= new Model
165 var modelbuilder
= new ModelBuilder(model
, toolcontext
)
167 var mmodules
= modelbuilder
.parse
(arguments
)
168 modelbuilder
.run_phases
169 var first_mmodule
= mmodules
.first
171 # Name of the support module
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"
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"
185 else if module_path
.has_suffix
(".nit") then
186 module_name
= module_path
.basename
(".nit")
188 module_name
= module_path
.basename
189 module_path
+= ".nit"
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.
199 for mmod
in mmodules
do
200 nit_module
.imports
.add mmod
.name
203 var phase
= toolcontext
.restful_phase
204 assert phase
isa RestfulPhase
206 for mclass
in phase
.restful_classes
do
209 nit_module
.content
.add t
212 redef class {{{mclass}}}
213 redef fun answer(request, truncated_uri)
215 var resources = truncated_uri.split("/")
216 if resources.not_empty and resources.first.is_empty then resources.shift
218 if resources.length != 1 then return super
219 var resource = resources.first
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
228 if i
!= 0 then t
.add
"else "
230 # Condition to select this method from a request
231 var conds
= new Array[String]
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 ") + ")"
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 ") + ")"
245 t
.add
"""if {{{conds.join(" and ")}}} then
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
254 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
257 var mtype
= param
.mtype
258 mtype
.gen_arg_convert
(t
, param
.name
)
260 var arg
= "out_{param.name}"
263 if mtype
.needs_type_check
then
264 isas
.add
"{arg} isa {mtype.name}"
270 if isas
.not_empty
then t
.add
"""
271 if not {{{isas.join(" or not ")}}} then
277 if args
.not_empty
then sig
= "({args.join(", ")})"
280 return {{{method.name}}}{{{sig}}}
291 # Write support module
292 if module_path
!= null then
294 nit_module
.write_to_file module_path
297 nit_module
.write_to stdout