nitweb: use model filters
[nit.git] / src / web / web_base.nit
index 145f80c..4aad814 100644 (file)
@@ -19,10 +19,15 @@ import model::model_views
 import model::model_json
 import doc_down
 import popcorn
+import popcorn::pop_config
+import popcorn::pop_repos
+import popcorn::pop_json
 
-# Specific nitcorn Action that uses a Model
-class ModelHandler
-       super Handler
+# Nitweb config file.
+class NitwebConfig
+       super AppConfig
+
+       redef fun default_db_name do return "nitweb"
 
        # Model to use.
        var model: Model
@@ -30,42 +35,27 @@ class ModelHandler
        # MModule used to flatten model.
        var mainmodule: MModule
 
-       # 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
-               return model.mentity_by_full_name(full_name.from_percent_encoding)
-       end
-
-       # Init the model view from the `req` uri parameters.
-       fun init_model_view(req: HttpRequest): ModelView do
-               var view = new ModelView(model)
-               var show_private = req.bool_arg("private") or else false
-               if not show_private then view.min_visibility = protected_visibility
+       # Modelbuilder used to access sources.
+       var modelbuilder: ModelBuilder
 
-               view.include_fictive = req.bool_arg("fictive") or else false
-               view.include_empty_doc = req.bool_arg("empty-doc") or else true
-               view.include_test_suite = req.bool_arg("test-suite") or else false
-               view.include_attribute = req.bool_arg("attributes") or else true
-
-               return view
-       end
+       # ModelView used to access model.
+       var view: ModelView
 end
 
-# Specific handler for nitweb API.
+# Specific handler for the nitweb API.
 abstract class APIHandler
-       super ModelHandler
+       super Handler
 
-       # The JSON API does not filter anything by default.
-       #
-       # So we can cache the model view.
-       var view: ModelView is lazy do
-               var view = new ModelView(model)
-               view.min_visibility = private_visibility
-               view.include_fictive = true
-               view.include_empty_doc = true
-               view.include_attribute = true
-               view.include_test_suite = true
-               return view
+       # 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
+               var mentity = model.mentity_by_full_name(full_name.from_percent_encoding)
+               if mentity == null then return null
+               if config.view.accept_mentity(mentity) then return mentity
+               return null
        end
 
        # Try to load the mentity from uri with `/:id`.
@@ -76,153 +66,294 @@ abstract class APIHandler
        fun mentity_from_uri(req: HttpRequest, res: HttpResponse): nullable MEntity do
                var id = req.param("id")
                if id == null then
-                       res.error 400
+                       res.api_error(400, "Expected mentity full name")
                        return null
                end
-               var mentity = find_mentity(view, id)
+               var mentity = find_mentity(config.view, id)
                if mentity == null then
-                       res.error 404
+                       res.api_error(404, "MEntity `{id}` not found")
                end
                return mentity
        end
+
+       # Paginate a json array
+       #
+       # Returns only a subset of `results` depending on the current `page` and the
+       # number of elements to return set by `limit`.
+       #
+       # Transforms the json array into an object:
+       # ~~~json
+       # {
+       #       "page": 2,
+       #       "limit": 10,
+       #       "results: [ ... ],
+       #       "max": 5,
+       #       "total": 49
+       # }
+       # ~~~
+       fun paginate(results: JsonArray, count: Int, page, limit: nullable Int): JsonObject do
+               if page == null or page <= 0 then page = 1
+               if limit == null or limit <= 0 then limit = 20
+
+               var max = count / limit
+               if max == 0 then
+                       page = 1
+                       max = 1
+               else if page > max then
+                       page = max
+               end
+
+               var lstart = (page - 1) * limit
+               var lend = limit
+               if lstart + lend > count then lend = count - lstart
+
+               var res = new JsonObject
+               res["page"] = page
+               res["limit"] = limit
+               res["results"] = new JsonArray.from(results.subarray(lstart, lend))
+               res["max"] = max
+               res["total"] = count
+               return res
+       end
+end
+
+# A Rooter dedicated to APIHandlers.
+class APIRouter
+       super Router
+
+       # App config
+       var config: NitwebConfig
+end
+
+redef class HttpResponse
+
+       # Return an HTTP error response with `status`
+       #
+       # Like the rest of the API, errors are formated as JSON:
+       # ~~~json
+       # { "status": 404, "message": "Not found" }
+       # ~~~
+       fun api_error(status: Int, message: String) do
+               json(new APIError(status, message), status)
+       end
+
+       # Write data as JSON and set the right content type header.
+       fun raw_json(json: nullable String, status: nullable Int) do
+               header["Content-Type"] = media_types["json"].as(not null)
+               if json == null then
+                       send(null, status)
+               else
+                       send(json, status)
+               end
+       end
+end
+
+# An error returned by the API.
+#
+# Can be serialized to json.
+class APIError
+       serialize
+
+       # Reponse status
+       var status: Int
+
+       # Response error message
+       var message: String
+end
+
+# Fullname representation that can be used to build decorated links
+#
+# * MPackage: `mpackage_name`
+# * MGroup: `(mpackage_name::)mgroup_name`
+class Namespace
+       super Array[nullable NSEntity]
+       super NSEntity
+       serialize
+
+       redef fun to_s do return self.join("")
+       redef fun serialize_to(v) do to_a.serialize_to(v)
+end
+
+# Something that goes in a Namespace
+#
+# Can be either:
+# * a `NSToken` for tokens like `::`, `>` and `$`
+# * a `MSRef` for references to mentities
+interface NSEntity
+       super Serializable
+end
+
+# A reference to a MEntity that can be rendered as a link.
+#
+# We do not reuse `MEntityRef` ref since NSRef can be found in a ref and create
+# an infinite loop.
+class NSRef
+       super NSEntity
+       serialize
+
+       # The mentity to link to/
+       var mentity: MEntity
+
+       redef fun core_serialize_to(v) do
+               v.serialize_attribute("web_url", mentity.web_url)
+               v.serialize_attribute("api_url", mentity.api_url)
+               v.serialize_attribute("name", mentity.name)
+       end
+end
+
+# A namespace token representation
+#
+# Used for namespace tokens like `::`, `>` and `$`
+redef class String
+       super NSEntity
 end
 
 redef class MEntity
 
        # URL to `self` within the web interface.
-       fun web_url: String is abstract
+       fun web_url: String do return "/doc/" / full_name
 
        # URL to `self` within the JSON api.
        fun api_url: String do return "/api/entity/" / full_name
 
-       redef fun json do
-               var obj = super
-               obj["web_url"] = web_url
-               obj["api_url"] = api_url
-               return obj
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("namespace", namespace)
+               v.serialize_attribute("web_url", web_url)
+               v.serialize_attribute("api_url", api_url)
        end
 
-       # Get the full json repesentation of `self` with MEntityRefs resolved.
-       fun api_json(handler: ModelHandler): JsonObject do return json
+       # Return `self.full_name` as an object that can be serialized to json.
+       fun namespace: nullable Namespace do return null
+
+       # Return a new NSRef to `self`.
+       fun to_ns_ref: NSRef do return new NSRef(self)
 end
 
 redef class MEntityRef
-       redef fun json do
-               var obj = super
-               obj["web_url"] = mentity.web_url
-               obj["api_url"] = mentity.api_url
-               obj["name"] = mentity.name
-               obj["mdoc"] = mentity.mdoc_or_fallback
-               obj["visibility"] = mentity.visibility
-               obj["location"] = mentity.location
-               var modifiers = new JsonArray
-               for modifier in mentity.collect_modifiers do
-                       modifiers.add modifier
-               end
-               obj["modifiers"] = modifiers
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("namespace", mentity.namespace)
+               v.serialize_attribute("web_url", mentity.web_url)
+               v.serialize_attribute("api_url", mentity.api_url)
+               v.serialize_attribute("name", mentity.name)
+               v.serialize_attribute("mdoc", mentity.mdoc_or_fallback)
+               v.serialize_attribute("visibility", mentity.visibility.to_s)
+               v.serialize_attribute("modifiers", mentity.collect_modifiers)
+               v.serialize_attribute("class_name", mentity.class_name)
                var mentity = self.mentity
                if mentity isa MMethod then
-                       obj["msignature"] = mentity.intro.msignature
+                       v.serialize_attribute("msignature", mentity.intro.msignature)
                else if mentity isa MMethodDef then
-                       obj["msignature"] = mentity.msignature
+                       v.serialize_attribute("msignature", mentity.msignature)
                else if mentity isa MVirtualTypeProp then
-                       obj["bound"] = to_mentity_ref(mentity.intro.bound)
+                       v.serialize_attribute("bound", to_mentity_ref(mentity.intro.bound))
                else if mentity isa MVirtualTypeDef then
-                       obj["bound"] = to_mentity_ref(mentity.bound)
+                       v.serialize_attribute("bound", to_mentity_ref(mentity.bound))
                end
-               return obj
+               v.serialize_attribute("location", mentity.location)
        end
 end
 
 redef class MDoc
 
        # Add doc down processing
-       redef fun json do
-               var obj = super
-               obj["synopsis"] = synopsis
-               obj["documentation"] = documentation
-               obj["comment"] = comment
-               obj["html_synopsis"] = html_synopsis.write_to_string
-               obj["html_documentation"] = html_documentation.write_to_string
-               obj["html_comment"] = html_comment.write_to_string
-               return obj
+       redef fun core_serialize_to(v) do
+               v.serialize_attribute("html_synopsis", html_synopsis.write_to_string)
        end
 end
 
 redef class MPackage
-       redef var web_url = "/package/{full_name}" is lazy
+       redef fun namespace do return new Namespace.from([to_ns_ref])
 end
 
 redef class MGroup
-       redef var web_url = "/group/{full_name}" is lazy
+       redef fun namespace do
+               var p = parent
+               if p == null then
+                       return new Namespace.from([to_ns_ref, ">": nullable NSEntity])
+               end
+               return new Namespace.from([p.namespace, to_ns_ref, ">": nullable NSEntity])
+       end
 end
 
 redef class MModule
-       redef var web_url = "/module/{full_name}" is lazy
-
-       redef fun api_json(handler) do
-               var obj = super
-               obj["intro_mclassdefs"] = to_mentity_refs(collect_intro_mclassdefs(private_view))
-               obj["redef_mclassdefs"] = to_mentity_refs(collect_redef_mclassdefs(private_view))
-               obj["imports"] = to_mentity_refs(in_importation.direct_greaters)
-               return obj
+       redef fun namespace do
+               var mgroup = self.mgroup
+               if mgroup == null then
+                       return new Namespace.from([to_ns_ref])
+               end
+               return new Namespace.from([mgroup.mpackage.to_ns_ref, "::", to_ns_ref: nullable NSEntity])
+       end
+
+       private fun ns_for(visibility: MVisibility): nullable Namespace do
+               if visibility <= private_visibility then return namespace
+               var mgroup = self.mgroup
+               if mgroup == null then return namespace
+               return mgroup.mpackage.namespace
        end
 end
 
 redef class MClass
-       redef var web_url = "/class/{full_name}" is lazy
-
-       redef fun api_json(handler) do
-               var obj = super
-               obj["all_mproperties"] = to_mentity_refs(collect_accessible_mproperties(private_view))
-               obj["intro_mproperties"] = to_mentity_refs(collect_intro_mproperties(private_view))
-               obj["redef_mproperties"] = to_mentity_refs(collect_redef_mproperties(private_view))
-               obj["parents"] = to_mentity_refs(collect_parents(private_view))
-               return obj
+       redef fun namespace do
+               return new Namespace.from([intro_mmodule.ns_for(visibility), "::", to_ns_ref: nullable NSEntity])
        end
 end
 
 redef class MClassDef
-       redef var web_url = "/classdef/{full_name}" is lazy
-
-       redef fun json do
-               var obj = super
-               obj["intro"] = to_mentity_ref(mclass.intro)
-               obj["mpackage"] = to_mentity_ref(mmodule.mpackage)
-               return obj
+       redef fun namespace do
+               if is_intro then
+                       return new Namespace.from([mmodule.ns_for(mclass.visibility), "$", to_ns_ref: nullable NSEntity])
+               else if mclass.intro_mmodule.mpackage != mmodule.mpackage then
+                       return new Namespace.from([mmodule.namespace, "$", mclass.namespace: nullable NSEntity])
+               else if mclass.visibility > private_visibility then
+                       return new Namespace.from([mmodule.namespace, "$", mclass.to_ns_ref: nullable NSEntity])
+               end
+               return new Namespace.from([mmodule.full_name, "$::", mclass.intro_mmodule.to_ns_ref: nullable NSEntity])
        end
 
-       redef fun api_json(handler) do
-               var obj = super
-               obj["intro_mpropdefs"] = to_mentity_refs(collect_intro_mpropdefs(private_view))
-               obj["redef_mpropdefs"] = to_mentity_refs(collect_redef_mpropdefs(private_view))
-               return obj
-       end
+       redef fun web_url do return "{mclass.web_url}/lin#{full_name}"
 end
 
 redef class MProperty
-       redef var web_url = "/property/{full_name}" is lazy
-
-       redef fun json do
-               var obj = super
-               obj["intro_mclass"] = to_mentity_ref(intro_mclassdef.mclass)
-               obj["mpackage"] = to_mentity_ref(intro_mclassdef.mmodule.mpackage)
-               return obj
+       redef fun namespace do
+               if intro_mclassdef.is_intro then
+                       return new Namespace.from([intro_mclassdef.mmodule.ns_for(visibility), "::", intro_mclassdef.mclass.to_ns_ref, "::", to_ns_ref: nullable NSEntity])
+               else
+                       return new Namespace.from([intro_mclassdef.mmodule.namespace, "::", intro_mclassdef.mclass.to_ns_ref, "::", to_ns_ref: nullable NSEntity])
+               end
        end
 end
 
 redef class MPropDef
-       redef var web_url = "/propdef/{full_name}" is lazy
-
-       redef fun json do
-               var obj = super
-               obj["intro"] = to_mentity_ref(mproperty.intro)
-               obj["intro_mclassdef"] = to_mentity_ref(mproperty.intro.mclassdef)
-               obj["mmodule"] = to_mentity_ref(mclassdef.mmodule)
-               obj["mgroup"] = to_mentity_ref(mclassdef.mmodule.mgroup)
-               obj["mpackage"] = to_mentity_ref(mclassdef.mmodule.mpackage)
-               return obj
+       redef fun namespace do
+               var res = new Namespace
+               res.add mclassdef.namespace
+               res.add "$"
+
+               if mclassdef.mclass == mproperty.intro_mclassdef.mclass then
+                       res.add to_ns_ref
+               else
+                       if mclassdef.mmodule.mpackage != mproperty.intro_mclassdef.mmodule.mpackage then
+                               res.add mproperty.intro_mclassdef.mmodule.ns_for(mproperty.visibility)
+                               res.add "::"
+                       else if mproperty.visibility <= private_visibility then
+                               if mclassdef.mmodule.namespace_for(mclassdef.mclass.visibility) != mproperty.intro_mclassdef.mmodule.mpackage then
+                                       res.add "::"
+                                       res.add mproperty.intro_mclassdef.mmodule.to_ns_ref
+                                       res.add "::"
+                               end
+                       end
+                       if mclassdef.mclass != mproperty.intro_mclassdef.mclass then
+                               res.add mproperty.intro_mclassdef.to_ns_ref
+                               res.add "::"
+                       end
+                       res.add to_ns_ref
+               end
+               return res
        end
+
+       redef fun web_url do return "{mproperty.web_url}/lin#{full_name}"
 end
 
 redef class MClassType
@@ -242,18 +373,11 @@ redef class MVirtualType
 end
 
 redef class POSetElement[E]
-       super Jsonable
+       super Serializable
 
-       # Return JSON representation of `self`.
-       fun json: JsonObject do
+       redef fun core_serialize_to(v) do
                assert self isa POSetElement[MEntity]
-               var obj = new JsonObject
-               obj["greaters"] = to_mentity_refs(greaters)
-               obj["direct_greaters"] = to_mentity_refs(direct_greaters)
-               obj["direct_smallers"] = to_mentity_refs(direct_smallers)
-               obj["smallers"] = to_mentity_refs(smallers)
-               return obj
+               v.serialize_attribute("direct_greaters", to_mentity_refs(direct_greaters))
+               v.serialize_attribute("direct_smallers", to_mentity_refs(direct_smallers))
        end
-
-       redef fun to_json do return json.to_json
 end