src: intro the nitrestful tool
authorAlexis Laferrière <alexis.laf@xymus.net>
Sun, 29 Nov 2015 01:09:17 +0000 (20:09 -0500)
committerAlexis Laferrière <alexis.laf@xymus.net>
Sun, 29 Nov 2015 05:15:52 +0000 (00:15 -0500)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

src/Makefile
src/nitrestful.nit [new file with mode: 0644]

index 6b9866a..0a5b909 100644 (file)
@@ -16,7 +16,7 @@
 
 NITCOPT=--semi-global
 OLDNITCOPT=--semi-global
-OBJS=nitc nitpick nit nitdoc nitls nitunit nitpretty nitmetrics nitx nitlight nitdbg_client nitserial
+OBJS=nitc nitpick nit nitdoc nitls nitunit nitpretty nitmetrics nitx nitlight nitdbg_client nitserial nitrestful
 SRCS=$(patsubst %,%.nit,$(OBJS))
 BINS=$(patsubst %,../bin/%,$(OBJS))
 
diff --git a/src/nitrestful.nit b/src/nitrestful.nit
new file mode 100644 (file)
index 0000000..22ffd1d
--- /dev/null
@@ -0,0 +1,252 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Tool generating boilerplate code linking RESTful actions to Nit methods
+module nitrestful
+
+import gen_nit
+
+import frontend
+
+private class RestfulPhase
+       super Phase
+
+       # Classes with methods marked with the `restful` annotation
+       var restful_classes = new HashSet[MClass]
+
+       redef fun process_annotated_node(node, nat)
+       do
+               # Skip if we are not interested
+               var text = nat.n_atid.n_id.text
+               if text != "restful" then return
+
+               if not node isa AMethPropdef then
+                       toolcontext.error(nat.location,
+                               "Syntax Error: `restful` can only be applied on method definitions")
+                       return
+               end
+
+               var mpropdef = node.mpropdef
+               if mpropdef == null then return
+
+               var mproperty = mpropdef.mproperty
+               var mclassdef = mpropdef.mclassdef
+               var mmodule = mclassdef.mmodule
+
+               # Test subclass of `RestfulAction`
+               var sup_class_name = "RestfulAction"
+               var sup_class = toolcontext.modelbuilder.try_get_mclass_by_name(
+                       nat, mmodule, sup_class_name)
+               var in_hierarchy = mclassdef.in_hierarchy
+               if in_hierarchy == null or sup_class == null then return
+               var sup_classes = in_hierarchy.greaters
+               if not sup_classes.has(sup_class.intro) then
+                       toolcontext.error(nat.location,
+                               "Syntax Error: `restful` is only valid within subclasses of `{sup_class_name}`")
+                       return
+               end
+
+               # Register the property
+               var mclass = mclassdef.mclass
+               mclass.restful_methods.add mproperty
+               restful_classes.add mclass
+       end
+end
+
+redef class MClass
+
+       # Methods with the `restful` annotation in this class
+       private var restful_methods = new Array[MMethod]
+end
+
+redef class ToolContext
+       # Generate serialization and deserialization methods on `auto_serializable` annotated classes.
+       var restful_phase: Phase = new RestfulPhase(self, [modelize_class_phase])
+
+       # Where do we put a single result?
+       var opt_output: OptionString = new OptionString("Output file (can also be 'stdout')", "-o", "--output")
+
+       # Where do we put the result?
+       var opt_dir: OptionString = new OptionString("Output directory", "--dir")
+
+       redef init
+       do
+               option_context.add_option(opt_output, opt_dir)
+               super
+       end
+end
+
+redef class MType
+       # Write code in `template` to parse the argument `arg_name` to this parameter type
+       private fun gen_arg_convert(template: Template, arg_name: String)
+       do
+               if self.name == "String" or self.name == "nullable String" then
+                       # String are used as is
+                       template.add """
+                       var out_{{{arg_name}}} = in_{{{arg_name}}}
+"""
+               else
+                       # Deserialize everything else
+                       template.add """
+                       var out_{{{arg_name}}} = deserialize_arg(in_{{{arg_name}}})
+"""
+               end
+       end
+
+       # Does this parameter type needs to be checked before calling the method?
+       #
+       # Some nullable types do not need to be check as `null` values are acceptable.
+       private fun needs_type_check: Bool do return true
+end
+
+redef class MNullableType
+       redef fun needs_type_check do return name != "nullable String" and name != "nullable Object"
+end
+
+var toolcontext = new ToolContext
+toolcontext.tooldescription = """
+Usage: nitrestful [OPTION] module.nit [other_module.nit [...]]
+Generates the boilerplate code to link RESTful request to static Nit methods."""
+
+toolcontext.process_options args
+var arguments = toolcontext.option_context.rest
+
+# Check options
+if toolcontext.opt_output.value != null and toolcontext.opt_dir.value != null then
+       print "Error: cannot use both --dir and --output"
+       exit 1
+end
+if arguments.length > 1 and toolcontext.opt_output.value != null then
+       print "Error: --output needs a single source file. Do you prefer --dir?"
+       exit 1
+end
+
+var model = new Model
+var modelbuilder = new ModelBuilder(model, toolcontext)
+
+var mmodules = modelbuilder.parse(arguments)
+modelbuilder.run_phases
+var first_mmodule = mmodules.first
+
+# Name of the support module
+var module_name
+
+# Path to the support module
+var module_path = toolcontext.opt_output.value
+if module_path == null then
+       module_name = "{first_mmodule.name}_rest"
+       module_path = "{module_name}.nit"
+
+       var dir = toolcontext.opt_dir.value
+       if dir != null then module_path = dir.join_path(module_path)
+else if module_path == "stdout" then
+       module_name = "{first_mmodule.name}_rest"
+       module_path = null
+else if module_path.has_suffix(".nit") then
+       module_name = module_path.basename(".nit")
+else
+       module_name = module_path.basename
+       module_path += ".nit"
+end
+
+var nit_module = new NitModule(module_name)
+nit_module.header = """
+# This file is generated by nitrestful
+# Do not modify, instead refine the generated services.
+"""
+
+for mmod in mmodules do
+       nit_module.imports.add mmod.name
+end
+
+var phase = toolcontext.restful_phase
+assert phase isa RestfulPhase
+
+for mclass in phase.restful_classes do
+
+       var t = new Template
+       nit_module.content.add t
+
+       t.add """
+redef class {{{mclass}}}
+       redef fun answer(request, truncated_uri)
+       do
+               var verbs = truncated_uri.split("/")
+               if verbs.not_empty and verbs.first.is_empty then verbs.shift
+
+               if verbs.length != 1 then return super
+               var verb = verbs.first
+
+"""
+       var methods = mclass.restful_methods
+       for i in methods.length.times, method in methods do
+               var msig = method.intro.msignature
+               if msig == null then continue
+
+               t.add "         "
+               if i != 0 then t.add "else "
+
+               t.add """if verb == "{{{method.name}}}" then
+"""
+
+               var args = new Array[String]
+               var isas = new Array[String]
+               for param in msig.mparameters do
+
+                       t.add """
+                       var in_{{{param.name}}} = request.string_arg("{{{param.name}}}")
+"""
+
+                       var mtype = param.mtype
+                       mtype.gen_arg_convert(t, param.name)
+
+                       var arg = "out_{param.name}"
+                       args.add arg
+
+                       if mtype.needs_type_check then
+                               isas.add "{arg} isa {mtype.name}"
+                       end
+
+                       t.add "\n"
+               end
+
+               if isas.not_empty then t.add """
+                       if not {{{isas.join(" or not ")}}} then
+                               return super
+                       end
+"""
+
+               var sig = ""
+               if args.not_empty then sig = "({args.join(", ")})"
+
+               t.add """
+                       return {{{method.name}}}{{{sig}}}
+"""
+       end
+
+       t.add """
+               end
+               return super
+       end
+end"""
+end
+
+# Write support module
+if module_path != null then
+       # To file
+       nit_module.write_to_file module_path
+else
+       # To stdout
+       nit_module.write_to stdout
+end