Merge: Intro of nitrestful a RESTful API generator
authorJean Privat <jean@pryen.org>
Fri, 4 Dec 2015 00:01:47 +0000 (19:01 -0500)
committerJean Privat <jean@pryen.org>
Fri, 4 Dec 2015 00:01:47 +0000 (19:01 -0500)
This tool generate a Nit module which implements `Action::answer` to redirect request to a static Nit method. It checks the presence of args, their types, deserialize objects as needed and calls the target method.

Missing arguments or arguments with errors (wrong type, failed deserialization, etc.) are replaced by `null` when the corresponding parameter is `nullable`. If the parameter is non-nullable, or if there is any other error, the generated code calls `super` from `answer` for the user code to handle exceptions and errors.

With the `restful` annotation we can write a normal method with static types such as: (from the example)
~~~
# User code
class MyAction
super RestfulAction

# Method answering requests like `foo?s=some_string&i=42&b=true`
fun foo(s: String, i: Int, b: Bool): HttpResponse is restful do
var resp = new HttpResponse(200)
resp.body = "foo {s} {i} {b}"
return resp
end
...
~~~

And nitrestful will generate for use the wrapper extracting the args from the request and call the method `foo`:
~~~
# Generated code by `nitrestful`
redef class MyAction
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

if verb == "foo" then
var in_s = request.string_arg("s")
var out_s = in_s

var in_i = request.string_arg("i")
var out_i = deserialize_arg(in_i)

var in_b = request.string_arg("b")
var out_b = deserialize_arg(in_b)

if not out_s isa String or not out_i isa Int or not out_b isa Bool then
return super
end
return foo(out_s, out_i, out_b)
end
return super
end
end
~~~

This is an early version of this tool. More work is needed to test different types, different usages, error management, and improve the docs and examples.

close #1852

Pull-Request: #1863
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Romain Chanoir <romain.chanoir@viacesi.fr>
Reviewed-by: Jean-Philippe Caissy <jpcaissy@piji.ca>

17 files changed:
contrib/jwrapper/Makefile
contrib/jwrapper/src/model.nit
lib/gen_nit.nit
lib/nitcorn/examples/.gitignore [new file with mode: 0644]
lib/nitcorn/examples/Makefile
lib/nitcorn/examples/src/restful_annot.nit [new file with mode: 0644]
lib/nitcorn/restful.nit [new file with mode: 0644]
share/man/nitrestful.md [new file with mode: 0644]
src/Makefile
src/frontend/serialization_phase.nit
src/nitrestful.nit [new file with mode: 0644]
src/nitserial.nit
tests/niti.skip
tests/nitrestful.args [new file with mode: 0644]
tests/nitvm.skip
tests/sav/nitrestful.res [new file with mode: 0644]
tests/sav/nitrestful_args1.res [new file with mode: 0644]

index 3173333..c1901bf 100644 (file)
@@ -12,7 +12,7 @@ src/javap_test_parser.nit: ../nitcc/src/nitcc grammar/javap.sablecc
        mv javap* gen/
 
 src/serial.nit: $(shell ../../bin/nitls -M src/jwrapper.nit)
-       ../../bin/nitserial -o src/serial.nit -d package src/jwrapper.nit
+       ../../bin/nitserial -o src/serial.nit src/jwrapper.nit
 
 bin/jwrapper: src/javap_test_parser.nit src/serial.nit $(shell ../../bin/nitls -M src/jwrapper.nit) ../../bin/nitc
        mkdir -p bin
index 9013bff..e642da4 100644 (file)
@@ -157,7 +157,7 @@ class NitType
        var identifier: String
 
        # If this NitType was found in `lib/android`, contains the module name to import
-       var mod: nullable NitModule
+       var mod: nullable NitModuleRef
 
        # Is this type known, wrapped and available in Nit?
        var is_known: Bool = true
@@ -183,7 +183,7 @@ class JavaClass
        var constructors = new Array[JavaConstructor]
 
        # Importations from this class
-       var imports = new HashSet[NitModule]
+       var imports = new HashSet[NitModuleRef]
 
        # Interfaces implemented by this class
        var implements = new HashSet[JavaType]
@@ -482,7 +482,7 @@ class JavaConstructor
 end
 
 # A Nit module, use to import the referenced extern classes
-class NitModule
+class NitModuleRef
        # Relative path to the module
        var path: String
 
@@ -490,7 +490,7 @@ class NitModule
        var name: String is lazy do return path.basename(".nit")
 
        redef fun to_s do return self.name
-       redef fun ==(other) do return other isa NitModule and self.path == other.path
+       redef fun ==(other) do return other isa NitModuleRef and self.path == other.path
        redef fun hash do return self.path.hash
 end
 
@@ -501,7 +501,7 @@ redef class Sys
        # * The value is the corresponding `NitType`.
        var find_extern_class: DefaultMap[String, nullable NitType] is lazy do
                var map = new DefaultMap[String, nullable NitType](null)
-               var modules = new HashMap[String, NitModule]
+               var modules = new HashMap[String, NitModuleRef]
 
                var lib_paths = opt_libs.value
                if lib_paths == null then lib_paths = new Array[String]
@@ -543,7 +543,7 @@ redef class Sys
 
                                var mod = modules.get_or_null(path)
                                if mod == null then
-                                       mod = new NitModule(path)
+                                       mod = new NitModuleRef(path)
                                        modules[path] = mod
                                end
 
index ea410a2..a9dc0ed 100644 (file)
@@ -15,6 +15,8 @@
 # Support to generate and otherwise manipulate Nit code
 module gen_nit
 
+import template
+
 redef class Sys
        # Reserved keywords in the Nit language
        var keywords: Set[String] is lazy do return new HashSet[String].from([
@@ -39,3 +41,37 @@ redef class Sys
        var methods_in_pointer: Array[String] is lazy do return methods_in_object + [
                "free"]
 end
+
+# Template of a Nit module to generate Nit code
+class NitModule
+       super Template
+
+       # Header on top of the module, usually the documentation
+       var header: nullable Writable = null is writable
+
+       # The module's name
+       var name: Writable is writable
+
+       # Imports from this module
+       var imports = new Array[Writable]
+
+       # Main content of this module
+       var content = new Array[Writable]
+
+       redef fun rendering
+       do
+               var header = header
+               if header != null then add header
+
+               var name = name
+               add "module {name}\n\n"
+
+               for i in imports do add "import {i}\n"
+               add "\n"
+
+               for l in content do
+                       add l
+                       add "\n"
+               end
+       end
+end
diff --git a/lib/nitcorn/examples/.gitignore b/lib/nitcorn/examples/.gitignore
new file mode 100644 (file)
index 0000000..c704121
--- /dev/null
@@ -0,0 +1 @@
+src/restful_annot_gen.nit
index 9597941..d1bdeae 100644 (file)
@@ -1,7 +1,15 @@
-all:
+all: bin/restful_annot
        mkdir -p bin/
        ../../../bin/nitc --dir bin src/nitcorn_hello_world.nit src/simple_file_server.nit
 
 xymus.net:
        mkdir -p bin/
        ../../../bin/nitc --dir bin/ src/xymus_net.nit
+
+pre-build: src/restful_annot_gen.nit
+src/restful_annot_gen.nit:
+       ../../../bin/nitrestful -o $@ src/restful_annot.nit
+
+bin/restful_annot: src/restful_annot_gen.nit
+       mkdir -p bin/
+       ../../../bin/nitc -o $@ src/restful_annot_gen.nit
diff --git a/lib/nitcorn/examples/src/restful_annot.nit b/lib/nitcorn/examples/src/restful_annot.nit
new file mode 100644 (file)
index 0000000..3657031
--- /dev/null
@@ -0,0 +1,47 @@
+# 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.
+
+import nitcorn::restful
+
+class MyAction
+       super RestfulAction
+
+       # Method answering requests like `foo?s=some_string&i=42&b=true`
+       fun foo(s: String, i: Int, b: Bool): HttpResponse
+       is restful do
+               var resp = new HttpResponse(200)
+               resp.body = "foo {s} {i} {b}"
+               return resp
+       end
+
+       # Method answering requests like `bar?s=these_arguments_are_optional`
+       fun bar(s: nullable String, i: nullable Int, b: nullable Bool): HttpResponse
+       is restful do
+               var resp = new HttpResponse(200)
+               resp.body = "bar {s or else "null"} {i or else "null"} {b or else "null"}"
+               return resp
+       end
+end
+
+var vh = new VirtualHost("localhost:8080")
+
+# Serve everything with our restful action
+vh.routes.add new Route(null, new MyAction)
+
+# Avoid executing when running tests
+if "NIT_TESTING".environ == "true" then exit 0
+
+var factory = new HttpFactory.and_libevent
+factory.config.virtual_hosts.add vh
+factory.run
diff --git a/lib/nitcorn/restful.nit b/lib/nitcorn/restful.nit
new file mode 100644 (file)
index 0000000..bccfd2c
--- /dev/null
@@ -0,0 +1,45 @@
+# 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.
+
+# Support module for the `nitrestful` tool and the `restful` annotation
+module restful is new_annotation(restful)
+
+import nitcorn
+import json::serialization
+
+# Action with `restful` methods
+class RestfulAction
+       super Action
+
+       redef fun answer(request, truncated_uri) do return new HttpResponse(400)
+
+       # Service to deserialize arguments from JSON
+       #
+       # Accepts `nullable String` for convenience, but returns `null` when `val == null`.
+       #
+       # This method is called by the code generated by `nitrestful`.
+       # It can be specialized to customize its behavior.
+       protected fun deserialize_arg(val: nullable String): nullable Object
+       do
+               if val == null then return null
+
+               var deserializer = new JsonDeserializer(val)
+               if deserializer.errors.not_empty then
+                       print_error deserializer.errors.join("\n")
+                       return null
+               end
+
+               return deserializer.deserialize
+       end
+end
diff --git a/share/man/nitrestful.md b/share/man/nitrestful.md
new file mode 100644 (file)
index 0000000..0968830
--- /dev/null
@@ -0,0 +1,19 @@
+# NAME
+
+nitrestful - generates boilerplate code to relay RESTful request to static Nit methods
+
+# SYNOPSIS
+
+nitrestful [*options*]... FILE
+
+# OPTIONS
+
+### `-o`, `--output`
+Output file (can also be 'stdout').
+
+### `--dir`
+Output directory.
+
+# SEE ALSO
+
+The Nit language documentation and the source code of its tools and libraries may be downloaded from <http://nitlanguage.org>
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))
 
index 3a70e66..358f211 100644 (file)
@@ -56,7 +56,6 @@ redef class ADefinition
        end
 end
 
-# TODO add annotations on attributes (volatile, sensitive or do_not_serialize?)
 private class SerializationPhasePreModel
        super Phase
 
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
index 1c24d69..9c8a07b 100644 (file)
 # generate and include its own serialization support module.
 module nitserial
 
-import frontend
-import rapid_type_analysis
 import template
+import gen_nit
 
-# A Nit module
-#
-# TODO add more features and move to lib
-class NitModule
-       super Template
-
-       var header: nullable Writable = null
-
-       # The module's name
-       var name: Writable
-
-       # Imports from this module
-       var imports = new Array[Writable]
-
-       # Main content of this module
-       var content = new Array[Writable]
-
-       redef fun rendering
-       do
-               var header = header
-               if header != null then add header
-
-               var name = name
-               add "module {name}\n\n"
-
-               for i in imports do add "import {i}\n"
-               add "\n"
-
-               for l in content do add "{l}\n"
-       end
-end
+import frontend
+import rapid_type_analysis
 
 redef class ToolContext
        # Where do we put a single result?
index 2fc5a83..8c7c00e 100644 (file)
@@ -17,6 +17,7 @@ test_docdown_args
 pep8analysis
 emscripten
 nitserial_args
+nitrestful_args
 nitunit_args
 nitpretty_args
 hamming_number
diff --git a/tests/nitrestful.args b/tests/nitrestful.args
new file mode 100644 (file)
index 0000000..054e93a
--- /dev/null
@@ -0,0 +1 @@
+-o stdout ../lib/nitcorn/examples/src/restful_annot.nit
index 2fc5a83..8c7c00e 100644 (file)
@@ -17,6 +17,7 @@ test_docdown_args
 pep8analysis
 emscripten
 nitserial_args
+nitrestful_args
 nitunit_args
 nitpretty_args
 hamming_number
diff --git a/tests/sav/nitrestful.res b/tests/sav/nitrestful.res
new file mode 100644 (file)
index 0000000..c1bc443
--- /dev/null
@@ -0,0 +1,3 @@
+Usage: nitrestful [OPTION] module.nit [other_module.nit [...]]
+Generates the boilerplate code to link RESTful request to static Nit methods.
+Use --help for help
diff --git a/tests/sav/nitrestful_args1.res b/tests/sav/nitrestful_args1.res
new file mode 100644 (file)
index 0000000..e2947d2
--- /dev/null
@@ -0,0 +1,47 @@
+# This file is generated by nitrestful
+# Do not modify, instead refine the generated services.
+module restful_annot_rest
+
+import restful_annot
+
+redef class MyAction
+       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
+
+               if verb == "foo" then
+                       var in_s = request.string_arg("s")
+                       var out_s = in_s
+
+                       var in_i = request.string_arg("i")
+                       var out_i = deserialize_arg(in_i)
+
+                       var in_b = request.string_arg("b")
+                       var out_b = deserialize_arg(in_b)
+
+                       if not out_s isa String or not out_i isa Int or not out_b isa Bool then
+                               return super
+                       end
+                       return foo(out_s, out_i, out_b)
+               else if verb == "bar" then
+                       var in_s = request.string_arg("s")
+                       var out_s = in_s
+
+                       var in_i = request.string_arg("i")
+                       var out_i = deserialize_arg(in_i)
+
+                       var in_b = request.string_arg("b")
+                       var out_b = deserialize_arg(in_b)
+
+                       if not out_i isa nullable Int or not out_b isa nullable Bool then
+                               return super
+                       end
+                       return bar(out_s, out_i, out_b)
+               end
+               return super
+       end
+end