Merge: lib/core: add `Path::/`
authorJean Privat <jean@pryen.org>
Thu, 11 Aug 2016 19:43:22 +0000 (15:43 -0400)
committerJean Privat <jean@pryen.org>
Thu, 11 Aug 2016 19:43:22 +0000 (15:43 -0400)
Pull-Request: #2253
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

15 files changed:
lib/json/serialization.nit
lib/nitcorn/nitcorn.nit
lib/nitcorn/reactor.nit
lib/nitcorn/server_config.nit
lib/serialization/serialization.nit
src/nitweb.nit
src/web/api_catalog.nit
src/web/api_docdown.nit
src/web/api_feedback.nit
src/web/api_graph.nit
src/web/api_metrics.nit
src/web/api_model.nit
src/web/web_base.nit
tests/sav/nitweb.res
wallet [new file with mode: 0755]

index 6a66407..e2c3adf 100644 (file)
@@ -242,6 +242,9 @@ class JsonDeserializer
        # Depth-first path in the serialized object tree.
        private var path = new Array[Map[String, nullable Object]]
 
+       # Names of the attributes from the root to the object currently being deserialized
+       var attributes_path = new Array[String]
+
        # Last encountered object reference id.
        #
        # See `id_to_object`.
@@ -265,7 +268,10 @@ class JsonDeserializer
 
                var value = current[name]
 
-               return convert_object(value)
+               attributes_path.add name
+               var res = convert_object(value)
+               attributes_path.pop
+               return res
        end
 
        # This may be called multiple times by the same object from constructors
@@ -441,11 +447,16 @@ class JsonDeserializer
                return convert_object(root)
        end
 
-       # User customizable heuristic to get the name of the Nit class to deserialize `json_object`
+       # User customizable heuristic to infer the name of the Nit class to deserialize `json_object`
        #
        # This method is called only when deserializing an object without the metadata `__class`.
-       # Return the class name as a `String` when it can be inferred.
-       # Return `null` when the class name cannot be found.
+       # Use the content of `json_object` to identify what Nit class it should be deserialized into.
+       # Or use `self.attributes_path` indicating where the deserialized object will be stored,
+       # is is less reliable as some objects don't have an associated attribute:
+       # the root/first deserialized object and collection elements.
+       #
+       # Return the class name as a `String` when it can be inferred,
+       # or `null` when the class name cannot be found.
        #
        # If a valid class name is returned, `json_object` will then be deserialized normally.
        # So it must contain the attributes of the corresponding class, as usual.
@@ -461,6 +472,7 @@ class JsonDeserializer
        #     serialize
        #
        #     var error: String
+       #     var related_data: MyData
        # end
        #
        # class MyJsonDeserializer
@@ -468,18 +480,26 @@ class JsonDeserializer
        #
        #     redef fun class_name_heuristic(json_object)
        #     do
+       #         # Infer the Nit class from the content of the JSON object.
        #         if json_object.keys.has("error") then return "MyError"
        #         if json_object.keys.has("data") then return "MyData"
+       #
+       #         # Infer the Nit class from the attribute where it will be stored.
+       #         # This line duplicates a previous line, and would only apply when
+       #         # `MyData` is within a `MyError`.
+       #         if attributes_path.not_empty and attributes_path.last == "related_data" then return "MyData"
+       #
        #         return null
        #     end
        # end
        #
-       # var json = """{"data": "some other data"}"""
+       # var json = """{"data": "some data"}"""
        # var deserializer = new MyJsonDeserializer(json)
        # var deserialized = deserializer.deserialize
        # assert deserialized isa MyData
        #
-       # json = """{"error": "some error message"}"""
+       # json = """{"error": "some error message",
+       #            "related_data": {"data": "some other data"}"""
        # deserializer = new MyJsonDeserializer(json)
        # deserialized = deserializer.deserialize
        # assert deserialized isa MyError
index 2a5b43a..40d02ef 100644 (file)
 # Basic usage example:
 # ~~~~
 # class MyAction
-#      super Action
-#
-#      redef fun answer(http_request, turi)
-#      do
-#              var response = new HttpResponse(200)
-#              response.body = """
-#              <!DOCTYPE html>
-#              <head>
-#                      <meta charset="utf-8">
-#                      <title>Hello World</title>
-#              </head>
-#              <body>
-#                      <p>Hello World</p>
-#              </body>
-#              </html>"""
-#              return response
-#      end
+#     super Action
+#
+#     redef fun answer(http_request, turi)
+#     do
+#         var response = new HttpResponse(200)
+#         response.body = """
+# <!DOCTYPE html>
+# <head>
+#     <meta charset="utf-8">
+#     <title>Hello World</title>
+# </head>
+# <body>
+#     <p>Hello World</p>
+# </body>
+# </html>"""
+#         return response
+#     end
 # end
 #
-# var vh = new VirtualHost("localhost:80")
+# # Listen to port 8080 on all interfaces
+# var vh = new VirtualHost("0.0.0.0:8080")
 #
 # # Serve index.html with our custom handler
 # vh.routes.add new Route("/index.html", new MyAction)
index de9ddc0..66334c3 100644 (file)
@@ -201,8 +201,15 @@ redef class Interfaces
        redef fun add(e)
        do
                super
-               var config = vh.server_config
-               if config != null then sys.listen_on(e, config.factory)
+               var config = virtual_host.server_config
+               if config != null then register_and_listen(e, config)
+       end
+
+       # Indirection to `listen_on` and check if this targets all addresses
+       private fun register_and_listen(e: Interface, config: ServerConfig)
+       do
+               listen_on(e, config.factory)
+               if e.name == "0.0.0.0" or e.name == "::0" then config.default_virtual_host = virtual_host
        end
 
        # TODO remove
@@ -212,7 +219,7 @@ redef class VirtualHosts
        redef fun add(e)
        do
                super
-               for i in e.interfaces do sys.listen_on(i, config.factory)
+               for i in e.interfaces do e.interfaces.register_and_listen(i, config)
        end
 
        # TODO remove
index e61caad..3cf51ff 100644 (file)
@@ -25,7 +25,7 @@ class ServerConfig
        var virtual_hosts = new VirtualHosts(self)
 
        # Default `VirtualHost` to respond to requests not handled by any of the `virtual_hosts`
-       var default_virtual_host: nullable VirtualHost = null
+       var default_virtual_host: nullable VirtualHost = null is writable
 end
 
 # A `VirtualHost` configuration
@@ -82,7 +82,7 @@ class Interfaces
        super Array[Interface]
 
        # Back reference to the associtated `VirtualHost`
-       var vh: VirtualHost
+       var virtual_host: VirtualHost
 
        # Add an `Interface` described by `text` formatted as `interface.name.com:port`
        fun add_from_string(text: String)
index 0ad3019..435f8df 100644 (file)
@@ -88,21 +88,27 @@ end
 
 # Abstract deserialization service
 #
-# After initialization of one of its sub-classes, call `deserialize`
+# The main service is `deserialize`.
 abstract class Deserializer
-       # Main method of this class, returns a Nit object
+       # Deserialize and return an object, storing errors in the attribute `errors`
+       #
+       # This method behavior varies according to the implementation engines.
        fun deserialize: nullable Object is abstract
 
-       # Internal method to be implemented by sub-classes
+       # Deserialize the attribute with `name` from the object open for deserialization
+       #
+       # Internal method to be implemented by the engines.
        fun deserialize_attribute(name: String): nullable Object is abstract
 
-       # Internal method called by objects in creation,
-       # to be implemented by sub-classes
+       # Register a newly allocated object (even if not completely built)
+       #
+       # Internal method called by objects in creation, to be implemented by the engines.
        fun notify_of_creation(new_object: Object) is abstract
 
        # Deserialize the next available object as an instance of `class_name`
        #
-       # Returns the deserialized object on success, aborts on error.
+       # Return the deserialized object on success and
+       # record in `errors` if `class_name` is unknown.
        #
        # This method should be redefined for each custom subclass of `Serializable`.
        # All refinement should look for a precise `class_name` and call super
index 7c6cdc1..9376172 100644 (file)
 # Runs a webserver based on nitcorn that render things from model.
 module nitweb
 
+import popcorn::pop_config
 import frontend
 import web
 
 redef class ToolContext
 
-       # Host name to bind on.
+       # Path to app config file.
+       var opt_config = new OptionString("Path to app config file", "--config")
+
+       # Host name to bind on (will overwrite the config one).
        var opt_host = new OptionString("Host to bind the server on", "--host")
 
-       # Port number to bind on.
-       var opt_port = new OptionInt("Port number to use", 3000, "--port")
+       # Port number to bind on (will overwrite the config one).
+       var opt_port = new OptionInt("Port number to use", -1, "--port")
 
        # Web rendering phase.
        var webphase: Phase = new NitwebPhase(self, null)
 
        init do
                super
-               option_context.add_option(opt_host, opt_port)
+               option_context.add_option(opt_config, opt_host, opt_port)
        end
 end
 
 # Phase that builds the model and wait for http request to serve pages.
 private class NitwebPhase
        super Phase
-       redef fun process_mainmodule(mainmodule, mmodules)
-       do
-               var model = mainmodule.model
-               var modelbuilder = toolcontext.modelbuilder
 
-               # Build catalog
+       # Build the nitweb config from `toolcontext` options.
+       fun build_config(toolcontext: ToolContext, mainmodule: MModule): NitwebConfig do
+               var config_file = toolcontext.opt_config.value
+               if config_file == null then config_file = "nitweb.ini"
+               var config = new NitwebConfig(
+                       config_file,
+                       toolcontext.modelbuilder.model,
+                       mainmodule,
+                       toolcontext.modelbuilder)
+               var opt_host = toolcontext.opt_host.value
+               if opt_host != null then config["app.host"] = opt_host
+               var opt_port = toolcontext.opt_port.value
+               if opt_port >= 0 then config["app.port"] = opt_port.to_s
+               return config
+       end
+
+       # Build the nit catalog used in homepage.
+       fun build_catalog(model: Model, modelbuilder: ModelBuilder): Catalog do
                var catalog = new Catalog(modelbuilder)
                for mpackage in model.mpackages do
                        catalog.deps.add_node(mpackage)
@@ -59,61 +76,49 @@ private class NitwebPhase
                        catalog.git_info(mpackage)
                        catalog.package_page(mpackage)
                end
+               return catalog
+       end
 
-               # Prepare mongo connection
-               var mongo = new MongoClient("mongodb://localhost:27017/")
-               var db = mongo.database("nitweb")
-               var collection = db.collection("stars")
-
-               # Run the server
-               var host = toolcontext.opt_host.value or else "localhost"
-               var port = toolcontext.opt_port.value
+       redef fun process_mainmodule(mainmodule, mmodules)
+       do
+               var model = mainmodule.model
+               var modelbuilder = toolcontext.modelbuilder
+               var config = build_config(toolcontext, mainmodule)
+               var catalog = build_catalog(model, modelbuilder)
 
                var app = new App
 
                app.use_before("/*", new RequestClock)
-               app.use("/api", new APIRouter(model, modelbuilder, mainmodule, catalog, collection))
+               app.use("/api", new NitwebAPIRouter(config, catalog))
                app.use("/*", new StaticHandler(toolcontext.share_dir / "nitweb", "index.html"))
                app.use_after("/*", new ConsoleLog)
 
-               app.listen(host, port.to_i)
+               app.listen(config.app_host, config.app_port)
        end
 end
 
 # Group all api handlers in one router.
-class APIRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # ModelBuilder to pass to handlers.
-       var modelbuilder: ModelBuilder
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+class NitwebAPIRouter
+       super APIRouter
 
        # Catalog to pass to handlers.
        var catalog: Catalog
 
-       # Mongo collection used to store ratings.
-       var collection: MongoCollection
-
        init do
-               use("/catalog", new APICatalogRouter(model, mainmodule, catalog))
-               use("/list", new APIList(model, mainmodule))
-               use("/search", new APISearch(model, mainmodule))
-               use("/random", new APIRandom(model, mainmodule))
-               use("/entity/:id", new APIEntity(model, mainmodule))
-               use("/code/:id", new APIEntityCode(model, mainmodule, modelbuilder))
-               use("/uml/:id", new APIEntityUML(model, mainmodule))
-               use("/linearization/:id", new APIEntityLinearization(model, mainmodule))
-               use("/defs/:id", new APIEntityDefs(model, mainmodule))
-               use("/feedback/", new APIFeedbackRouter(model, mainmodule, collection))
-               use("/inheritance/:id", new APIEntityInheritance(model, mainmodule))
-               use("/graph/", new APIGraphRouter(model, mainmodule))
-               use("/docdown/", new APIDocdown(model, mainmodule, modelbuilder))
-               use("/metrics/", new APIMetricsRouter(model, mainmodule))
+               use("/catalog", new APICatalogRouter(config, catalog))
+               use("/list", new APIList(config))
+               use("/search", new APISearch(config))
+               use("/random", new APIRandom(config))
+               use("/entity/:id", new APIEntity(config))
+               use("/code/:id", new APIEntityCode(config))
+               use("/uml/:id", new APIEntityUML(config))
+               use("/linearization/:id", new APIEntityLinearization(config))
+               use("/defs/:id", new APIEntityDefs(config))
+               use("/feedback/", new APIFeedbackRouter(config))
+               use("/inheritance/:id", new APIEntityInheritance(config))
+               use("/graph/", new APIGraphRouter(config))
+               use("/docdown/", new APIDocdown(config))
+               use("/metrics/", new APIMetricsRouter(config))
        end
 end
 
@@ -121,7 +126,7 @@ end
 var toolcontext = new ToolContext
 var tpl = new Template
 tpl.add "Usage: nitweb [OPTION]... <file.nit>...\n"
-tpl.add "Run a webserver based on nitcorn that serve pages about model."
+tpl.add "Run a webserver based on nitcorn that serves pages about model."
 toolcontext.tooldescription = tpl.write_to_string
 
 # process options
index 72d207f..f0c70c6 100644 (file)
@@ -19,23 +19,17 @@ import catalog
 
 # Group all api handlers in one router.
 class APICatalogRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        # Catalog to pass to handlers.
        var catalog: Catalog
 
        init do
-               use("/highlighted", new APICatalogHighLighted(model, mainmodule, catalog))
-               use("/required", new APICatalogMostRequired(model, mainmodule, catalog))
-               use("/bytags", new APICatalogByTags(model, mainmodule, catalog))
-               use("/contributors", new APICatalogContributors(model, mainmodule, catalog))
-               use("/stats", new APICatalogStats(model, mainmodule, catalog))
+               use("/highlighted", new APICatalogHighLighted(config, catalog))
+               use("/required", new APICatalogMostRequired(config, catalog))
+               use("/bytags", new APICatalogByTags(config, catalog))
+               use("/contributors", new APICatalogContributors(config, catalog))
+               use("/stats", new APICatalogStats(config, catalog))
        end
 end
 
@@ -74,7 +68,7 @@ class APICatalogStats
 
        redef fun get(req, res) do
                var obj = new JsonObject
-               obj["packages"] = model.mpackages.length
+               obj["packages"] = config.model.mpackages.length
                obj["maintainers"] = catalog.maint2proj.length
                obj["contributors"] = catalog.contrib2proj.length
                obj["modules"] = catalog.mmodules.sum
@@ -97,7 +91,7 @@ class APICatalogMostRequired
        redef fun get(req, res) do
                if catalog.deps.not_empty then
                        var reqs = new Counter[MPackage]
-                       for p in model.mpackages do
+                       for p in config.model.mpackages do
                                reqs[p] = catalog.deps[p].smallers.length - 1
                        end
                        res.json list_best(reqs)
index 3510b64..bf4c80f 100644 (file)
@@ -24,13 +24,10 @@ import doc_commands
 class APIDocdown
        super APIHandler
 
-       # Modelbuilder used by the commands
-       var modelbuilder: ModelBuilder
-
        # Specific Markdown processor to use within Nitweb
        var md_processor: MarkdownProcessor is lazy do
                var proc = new MarkdownProcessor
-               proc.emitter.decorator = new NitwebDecorator(view, modelbuilder)
+               proc.emitter.decorator = new NitwebDecorator(view, config.modelbuilder)
                return proc
        end
 
index 61ba756..76e9d67 100644 (file)
@@ -18,21 +18,38 @@ module api_feedback
 import web_base
 import mongodb
 
-# Group all api handlers in one router
-class APIFeedbackRouter
-       super Router
+redef class NitwebConfig
+
+       # MongoDB uri used for data persistence.
+       #
+       # * key: `mongo.uri`
+       # * default: `mongodb://localhost:27017/`
+       var mongo_uri: String is lazy do
+               return value_or_default("mongo.uri", "mongodb://localhost:27017/")
+       end
 
-       # Model to pass to handlers
-       var model: Model
+       # MongoDB DB used for data persistence.
+       #
+       # * key: `mongo.db`
+       # * default: `nitweb`
+       var mongo_db: String is lazy do return value_or_default("mongo.db", "nitweb")
 
-       # Mainmodule to pass to handlers
-       var mainmodule: MModule
+       # Mongo instance
+       var mongo: MongoClient is lazy do return new MongoClient(mongo_uri)
 
-       # Mongo collection used to store ratings
-       var collection: MongoCollection
+       # Database instance
+       var db: MongoDb is lazy do return mongo.database(mongo_db)
+
+       # MongoDB collection used to store stars.
+       var stars: MongoCollection is lazy do return db.collection("stars")
+end
+
+# Group all api handlers in one router
+class APIFeedbackRouter
+       super APIRouter
 
        init do
-               use("/stars/:id", new APIStars(model, mainmodule, collection))
+               use("/stars/:id", new APIStars(config))
        end
 end
 
@@ -40,9 +57,6 @@ end
 class APIStars
        super APIHandler
 
-       # Collection used to store ratings
-       var collection: MongoCollection
-
        redef fun get(req, res) do
                var mentity = mentity_from_uri(req, res)
                if mentity == null then
@@ -71,7 +85,7 @@ class APIStars
                end
 
                var val = new MEntityRating(mentity.full_name, rating, get_time)
-               collection.insert(val.json)
+               config.stars.insert(val.json)
 
                res.json mentity_ratings(mentity)
        end
@@ -82,7 +96,7 @@ class APIStars
 
                var req = new JsonObject
                req["mentity"] = mentity.full_name
-               var rs = collection.find_all(req)
+               var rs = config.stars.find_all(req)
                for r in rs do ratings.ratings.add new MEntityRating.from_json(r)
                return ratings
        end
index 67a32e2..d58031d 100644 (file)
@@ -21,16 +21,10 @@ import uml
 
 # Group all api handlers in one router.
 class APIGraphRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        init do
-               use("/inheritance/:id", new APIInheritanceGraph(model, mainmodule))
+               use("/inheritance/:id", new APIInheritanceGraph(config))
        end
 end
 
index 30a53f1..8d64c04 100644 (file)
@@ -19,16 +19,10 @@ import metrics
 
 # Group all api handlers in one router.
 class APIMetricsRouter
-       super Router
-
-       # Model to pass to handlers.
-       var model: Model
-
-       # Mainmodule to pass to handlers.
-       var mainmodule: MModule
+       super APIRouter
 
        init do
-               use("/structural/:id", new APIStructuralMetrics(model, mainmodule))
+               use("/structural/:id", new APIStructuralMetrics(config))
        end
 end
 
@@ -36,6 +30,7 @@ class APIStructuralMetrics
        super APIHandler
 
        private fun mclasses_metrics: MetricSet do
+               var mainmodule = config.mainmodule
                var metrics = new MetricSet
                metrics.register(new CNOA(mainmodule, view))
                metrics.register(new CNOP(mainmodule, view))
@@ -58,6 +53,7 @@ class APIStructuralMetrics
        end
 
        private fun mmodules_metrics: MetricSet do
+               var mainmodule = config.mainmodule
                var metrics = new MetricSet
                metrics.register(new MNOA(mainmodule, view))
                metrics.register(new MNOP(mainmodule, view))
index f8c6eeb..63fd636 100644 (file)
@@ -148,7 +148,7 @@ class APIEntityLinearization
                        res.error 404
                        return
                end
-               var lin = mentity.collect_linearization(mainmodule)
+               var lin = mentity.collect_linearization(config.mainmodule)
                if lin == null then
                        res.error 404
                        return
@@ -206,7 +206,7 @@ class APIEntityUML
                var dot
                if mentity isa MClassDef then mentity = mentity.mclass
                if mentity isa MClass then
-                       var uml = new UMLModel(view, mainmodule)
+                       var uml = new UMLModel(view, config.mainmodule)
                        dot = uml.generate_class_uml.write_to_string
                else if mentity isa MModule then
                        var uml = new UMLModel(view, mentity)
@@ -225,9 +225,6 @@ end
 class APIEntityCode
        super APIHandler
 
-       # Modelbuilder used to access sources.
-       var modelbuilder: ModelBuilder
-
        redef fun get(req, res) do
                var mentity = mentity_from_uri(req, res)
                if mentity == null then return
@@ -241,7 +238,7 @@ class APIEntityCode
 
        # Highlight `mentity` source code.
        private fun render_source(mentity: MEntity): nullable HTMLTag do
-               var node = modelbuilder.mentity2node(mentity)
+               var node = config.modelbuilder.mentity2node(mentity)
                if node == null then return null
                var hl = new HighlightVisitor
                hl.enter_visit node
index 20d0b05..c535936 100644 (file)
@@ -19,10 +19,11 @@ import model::model_views
 import model::model_json
 import doc_down
 import popcorn
+import popcorn::pop_config
 
-# Specific nitcorn Action that uses a Model
-class ModelHandler
-       super Handler
+# Nitweb config file.
+class NitwebConfig
+       super AppConfig
 
        # Model to use.
        var model: Model
@@ -30,6 +31,17 @@ class ModelHandler
        # MModule used to flatten model.
        var mainmodule: MModule
 
+       # Modelbuilder used to access sources.
+       var modelbuilder: ModelBuilder
+end
+
+# Specific nitcorn Action that uses a Model
+class ModelHandler
+       super Handler
+
+       # App config.
+       var config: NitwebConfig
+
        # Find the MEntity ` with `full_name`.
        fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do
                if full_name == null then return null
@@ -38,7 +50,7 @@ class ModelHandler
 
        # Init the model view from the `req` uri parameters.
        fun init_model_view(req: HttpRequest): ModelView do
-               var view = new ModelView(model)
+               var view = new ModelView(config.model)
                var show_private = req.bool_arg("private") or else false
                if not show_private then view.min_visibility = protected_visibility
 
@@ -59,7 +71,7 @@ abstract class APIHandler
        #
        # So we can cache the model view.
        var view: ModelView is lazy do
-               var view = new ModelView(model)
+               var view = new ModelView(config.model)
                view.min_visibility = private_visibility
                view.include_fictive = true
                view.include_empty_doc = true
@@ -87,6 +99,14 @@ abstract class APIHandler
        end
 end
 
+# A Rooter dedicated to APIHandlers.
+class APIRouter
+       super Router
+
+       # App config.
+       var config: NitwebConfig
+end
+
 redef class MEntity
 
        # URL to `self` within the web interface.
index 7c0cf6d..a6f1e98 100644 (file)
@@ -1,3 +1,3 @@
 Usage: nitweb [OPTION]... <file.nit>...
-Run a webserver based on nitcorn that serve pages about model.
+Run a webserver based on nitcorn that serves pages about model.
 Use --help for help
diff --git a/wallet b/wallet
new file mode 100755 (executable)
index 0000000..567234d
Binary files /dev/null and b/wallet differ