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[MClassDef]
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 mpropdef
= node
.mpropdef
41 if mpropdef
== null then return
42 var mproperty
= mpropdef
.mproperty
43 var mclassdef
= mpropdef
.mclassdef
44 var mmodule
= mclassdef
.mmodule
46 var http_resources
= new Array[String]
47 var http_methods
= new Array[String]
48 for arg
in nat
.n_args
do
49 var str
= arg
.as_string
50 var id
= arg
.collect_text
52 # String -> rename resource
53 http_resources
.add str
54 else if arg
isa ATypeExpr and not id
.chars
.has
("[") then
55 # Class id -> HTTP method
57 else if id
== "async" then
58 mproperty
.restful_async
= true
60 toolcontext
.error
(nat
.location
,
61 "Syntax Error: `restful` expects String literals or ids as arguments.")
66 # Test subclass of `RestfulAction`
67 var sup_class_name
= "RestfulAction"
68 var sup_class
= toolcontext
.modelbuilder
.try_get_mclass_by_name
(
69 nat
, mmodule
, sup_class_name
)
70 var in_hierarchy
= mclassdef
.in_hierarchy
71 if in_hierarchy
== null or sup_class
== null then return
72 var sup_classes
= in_hierarchy
.greaters
73 if not sup_classes
.has
(sup_class
.intro
) then
74 toolcontext
.error
(nat
.location
,
75 "Syntax Error: `restful` is only valid within subclasses of `{sup_class_name}`")
79 # Register the property
80 mclassdef
.restful_methods
.add mproperty
81 restful_classes
.add mclassdef
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
101 # Is this a `restful` method to be executed asynchronously
102 private var restful_async
= false
105 redef class ToolContext
106 # Generate serialization and deserialization methods on `auto_serializable` annotated classes.
107 var restful_phase
: Phase = new RestfulPhase(self, [modelize_class_phase
])
109 # Where do we put a single result?
110 var opt_output
: OptionString = new OptionString("Output file (can also be 'stdout')", "-o", "--output")
112 # Where do we put the result?
113 var opt_dir
: OptionString = new OptionString("Output directory", "--dir")
117 option_context
.add_option
(opt_output
, opt_dir
)
123 # Write code in `template` to parse the argument `arg_name` to this parameter type
124 private fun gen_arg_convert
(template
: Template, arg_name
: String)
126 if self.name
== "String" or self.name
== "nullable String" then
127 # String are used as is
129 var out_{{{arg_name}}} = in_{{{arg_name}}}
132 # Deserialize everything else
134 var out_{{{arg_name}}} = deserialize_arg(in_{{{arg_name}}}, "{{{self.name}}}")
139 # Does this parameter type needs to be checked before calling the method?
141 # Some nullable types do not need to be check as `null` values are acceptable.
142 private fun needs_type_check
: Bool do return true
145 redef class MNullableType
146 redef fun needs_type_check
do return name
!= "nullable String" and name
!= "nullable Object"
149 var toolcontext
= new ToolContext
150 toolcontext
.tooldescription
= """
151 Usage: nitrestful [OPTION] module.nit [other_module.nit [...]]
152 Generates the boilerplate code to link RESTful request to static Nit methods."""
154 toolcontext
.process_options args
155 var arguments
= toolcontext
.option_context
.rest
158 if toolcontext
.opt_output
.value
!= null and toolcontext
.opt_dir
.value
!= null then
159 print
"Error: cannot use both --dir and --output"
162 if arguments
.length
> 1 and toolcontext
.opt_output
.value
!= null then
163 print
"Error: --output needs a single source file. Do you prefer --dir?"
167 var model
= new Model
168 var modelbuilder
= new ModelBuilder(model
, toolcontext
)
170 var mmodules
= modelbuilder
.parse
(arguments
)
171 modelbuilder
.run_phases
172 var first_mmodule
= mmodules
.first
174 # Name of the support module
177 # Path to the support module
178 var module_path
= toolcontext
.opt_output
.value
179 if module_path
== null then
180 module_name
= "{first_mmodule.name}_rest"
181 module_path
= "{module_name}.nit"
183 var dir
= toolcontext
.opt_dir
.value
184 if dir
!= null then module_path
= dir
.join_path
(module_path
)
185 else if module_path
== "stdout" then
186 module_name
= "{first_mmodule.name}_rest"
188 else if module_path
.has_suffix
(".nit") then
189 module_name
= module_path
.basename
(".nit")
191 module_name
= module_path
.basename
192 module_path
+= ".nit"
195 var nit_module
= new NitModule(module_name
)
196 nit_module
.annotations
.add
"""no_warning("parentheses")"""
197 nit_module
.header
= """
198 # This file is generated by nitrestful
199 # Do not modify, instead refine the generated services.
202 for mmod
in mmodules
do
203 nit_module
.imports
.add mmod
.name
206 var phase
= toolcontext
.restful_phase
207 assert phase
isa RestfulPhase
209 for mclassdef
in phase
.restful_classes
do
210 var mclass
= mclassdef
.mclass
213 nit_module
.content
.add t
215 var classes
= new Template
216 nit_module
.content
.add classes
219 redef class {{{mclass}}}
220 redef fun prepare_respond_and_close(request, truncated_uri, http_server)
222 var resources = truncated_uri.split("/")
223 if resources.not_empty and resources.first.is_empty then resources.shift
225 if resources.length != 1 then
229 var resource = resources.first
232 var methods
= mclassdef
.restful_methods
233 for i
in methods
.length
.times
, method
in methods
do
234 var msig
= method
.intro
.msignature
235 if msig
== null then continue
237 # Condition to select this method from a request
238 var conds
= new Array[String]
240 # Name of the resource from the method or as renamed
241 var resource_conds
= new Array[String]
242 for resource
in method
.restful_resources
do resource_conds
.add
"resource == \"{resource}\
""
243 conds
.add
"(" + resource_conds
.join
(" or ") + ")"
246 if method
.restful_verbs
.not_empty
then
247 var method_conds
= new Array[String]
248 for meth
in method
.restful_verbs
do method_conds
.add
"request.method == \"{meth}\
""
249 conds
.add
"(" + method_conds
.join
(" or ") + ")"
253 if {{{conds.join(" and ")}}} then
256 # Extract the arguments from the request for the method call
257 var args
= new Array[String]
258 var isas
= new Array[String]
259 for param
in msig
.mparameters
do
262 var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
265 var mtype
= param
.mtype
266 var bound_mtype
= mclassdef
.bound_mtype
267 var resolved_mtype
= mtype
.resolve_for
(bound_mtype
, bound_mtype
, mclassdef
.mmodule
, true)
268 var resolved_type_name
= resolved_mtype
.name
270 resolved_mtype
.gen_arg_convert
(t
, param
.name
)
272 var arg
= "out_{param.name}"
275 if mtype
.needs_type_check
then
276 isas
.add
"{arg} isa {mtype.name}"
282 if isas
.not_empty
then t
.add
"""
283 if {{{isas.join(" and ")}}} then
287 if args
.not_empty
then sig
= "({args.join(", ")})"
289 if not method
.restful_async
then
292 var response = {{{method.name}}}{{{sig}}}
293 http_server.respond response
298 # Asynchronous method
299 var task_name
= "Task_{mclass}_{method.name}"
300 args
.unshift
"http_server"
301 args
.unshift
"request"
305 var task = new {{{task_name}}}({{{args.join(", ")}}})
306 self.thread_pool.execute task
310 var thread_attribs
= new Array[String]
311 for param
in msig
.mparameters
do
312 thread_attribs
.add
"""
313 private var out_{{{param.name}}}: {{{param.mtype}}}"""
318 # Generated task to execute {{{mclass}}}::{{{method.name}}}
319 class {{{task_name}}}
322 redef type A: {{{mclass}}}
324 {{{thread_attribs.join("\n")}}}
326 redef fun indirect_restful_method
328 return action.{{{method.name}}}{{{sig}}}
334 if isas
.not_empty
then t
.add
"""
348 # Write support module
349 if module_path
!= null then
351 nit_module
.write_to_file module_path
354 nit_module
.write_to stdout