nitweb: update ModelView
[nit.git] / src / web / api_catalog.nit
index 294d072..42391c0 100644 (file)
 
 module api_catalog
 
-import web_base
+import api_model
 import catalog
 
 redef class NitwebConfig
 
        # Catalog to pass to handlers.
-       var catalog: Catalog is lazy do
-               var catalog = new Catalog(modelbuilder)
-               for mpackage in model.mpackages do
-                       catalog.deps.add_node(mpackage)
-                       for mgroup in mpackage.mgroups do
-                               for mmodule in mgroup.mmodules do
-                                       for imported in mmodule.in_importation.direct_greaters do
-                                               var ip = imported.mpackage
-                                               if ip == null or ip == mpackage then continue
-                                               catalog.deps.add_edge(mpackage, ip)
-                                       end
-                               end
-                       end
-                       catalog.git_info(mpackage)
-                       catalog.package_page(mpackage)
-               end
-               return catalog
+       var catalog: Catalog is noinit
+
+       # Build the catalog
+       #
+       # This method should be called at nitweb startup.
+       fun build_catalog do
+               self.catalog = new Catalog(modelbuilder)
+               self.catalog.build_catalog(model.mpackages)
        end
 end
 
-# Group all api handlers in one router.
-class APICatalogRouter
-       super APIRouter
+redef class APIRouter
+       redef init do
+               super
+               use("/catalog/packages/", new APICatalogPackages(config))
+               use("/catalog/stats", new APICatalogStats(config))
 
-       init do
-               use("/highlighted", new APICatalogHighLighted(config))
-               use("/required", new APICatalogMostRequired(config))
-               use("/bytags", new APICatalogByTags(config))
-               use("/contributors", new APICatalogContributors(config))
-               use("/stats", new APICatalogStats(config))
+               use("/catalog/tags", new APICatalogTags(config))
+               use("/catalog/tag/:tid", new APICatalogTag(config))
+
+               use("/catalog/person/:pid", new APICatalogPerson(config))
+               use("/catalog/person/:pid/maintaining", new APICatalogMaintaining(config))
+               use("/catalog/person/:pid/contributing", new APICatalogContributing(config))
        end
 end
 
 abstract class APICatalogHandler
        super APIHandler
 
-       # List the 10 best packages from `cpt`
-       fun list_best(cpt: Counter[MPackage]): JsonArray do
-               var res = new JsonArray
-               var best = cpt.sort
-               for i in [1..10] do
-                       if i > best.length then break
-                       res.add best[best.length-i]
-               end
-               return res
-       end
+       # Sorter used to sort packages
+       #
+       # Sorting is based on mpackage score.
+       var mpackages_sorter = new CatalogScoreSorter(config.catalog) is lazy
+end
 
-       # List packages by group.
-       fun list_by(map: MultiHashMap[Object, MPackage]): JsonObject do
-               var res = new JsonObject
-               var keys = map.keys.to_a
-               alpha_comparator.sort(keys)
-               for k in keys do
-                       var projs = map[k].to_a
-                       alpha_comparator.sort(projs)
-                       res[k.to_s.html_escape] = new JsonArray.from(projs)
-               end
-               return res
+# Get all the packages from the catalog using pagination
+#
+# `GET /packages?p=1&n=10`: get the list of catalog by page
+class APICatalogPackages
+       super APICatalogHandler
+
+       redef fun get(req, res) do
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var mpackages = config.catalog.mpackages.values.to_a
+               mpackages_sorter.sort(mpackages)
+               var response = new JsonArray.from(mpackages)
+               res.json paginate(response, response.length, page, limit)
        end
 end
 
+# Get the catalog statistics
+#
+# `GET /stats`: return the catalog statistics
 class APICatalogStats
        super APICatalogHandler
 
        redef fun get(req, res) do
-               var obj = new JsonObject
-               obj["packages"] = config.model.mpackages.length
-               obj["maintainers"] = config.catalog.maint2proj.length
-               obj["contributors"] = config.catalog.contrib2proj.length
-               obj["modules"] = config.catalog.mmodules.sum
-               obj["classes"] = config.catalog.mclasses.sum
-               obj["methods"] = config.catalog.mmethods.sum
-               obj["loc"] = config.catalog.loc.sum
-               res.json obj
+               res.json config.catalog.catalog_stats
        end
 end
 
-class APICatalogHighLighted
+# Get all the tags from the catalog
+#
+# `GET /tags`: the list of tags associated with their number of packages
+class APICatalogTags
        super APICatalogHandler
 
-       redef fun get(req, res) do res.json list_best(config.catalog.score)
+       # Sorter to sort tags alphabetically
+       var tags_sorter = new CatalogTagsSorter
+
+       redef fun get(req, res) do
+               var obj = new JsonObject
+
+               var tags = config.catalog.tag2proj.keys.to_a
+               tags_sorter.sort(tags)
+
+               for tag in tags do
+                       if not config.catalog.tag2proj.has_key(tag) then continue
+                       obj[tag] = config.catalog.tag2proj[tag].length
+               end
+               res.json obj
+       end
 end
 
-class APICatalogMostRequired
+# Get the packages related to a tag
+#
+# `GET /tag/:tid?p=1&n=10`: return a paginated list of packages
+class APICatalogTag
        super APICatalogHandler
 
        redef fun get(req, res) do
-               if config.catalog.deps.not_empty then
-                       var reqs = new Counter[MPackage]
-                       for p in config.model.mpackages do
-                               reqs[p] = config.catalog.deps[p].smallers.length - 1
-                       end
-                       res.json list_best(reqs)
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var id = req.param("tid")
+               if id == null then
+                       res.api_error(400, "Missing tag")
+                       return
+               end
+               id = id.from_percent_encoding
+               if not config.catalog.tag2proj.has_key(id) then
+                       res.api_error(404, "Tag not found")
                        return
                end
-               res.json new JsonArray
+               var obj = new JsonObject
+               obj["tag"] = id
+               var mpackages = config.catalog.tag2proj[id]
+               mpackages_sorter.sort(mpackages)
+               var response = new JsonArray.from(mpackages)
+               obj["packages"] = paginate(response, response.length, page, limit)
+               res.json obj
        end
 end
 
-class APICatalogByTags
+# Get a person existing in the catalog
+#
+# `GET /person/:pid`: get the person with `pid`
+class APICatalogPerson
        super APICatalogHandler
 
-       redef fun get(req, res) do res.json list_by(config.catalog.tag2proj)
+       # Get the person with `:pid` or throw a 404 error
+       fun get_person(req: HttpRequest, res: HttpResponse): nullable Person do
+               var id = req.param("pid")
+               if id == null then
+                       res.api_error(400, "Missing package full_name")
+                       return null
+               end
+               id = id.from_percent_encoding
+               if not config.catalog.name2person.has_key(id) then
+                       res.api_error(404, "Person not found")
+                       return null
+               end
+               return config.catalog.name2person[id]
+       end
+
+       redef fun get(req, res) do
+               var person = get_person(req, res)
+               if person == null then return
+               res.json person
+       end
 end
 
-class APICatalogContributors
-       super APICatalogHandler
+# Get the list of mpackages maintained by a person
+#
+# `GET /person/:pid/maintaining?p=1&n=10`: return a paginated list of packages
+class APICatalogMaintaining
+       super APICatalogPerson
 
        redef fun get(req, res) do
-               var obj = new JsonObject
-               obj["maintainers"] = new JsonArray.from(config.catalog.maint2proj.keys)
-               obj["contributors"] = new JsonArray.from(config.catalog.contrib2proj.keys)
-               res.json obj
+               var person = get_person(req, res)
+               if person == null then return
+
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var array = new Array[MPackage]
+               if config.catalog.maint2proj.has_key(person) then
+                       array = config.catalog.maint2proj[person].to_a
+               end
+               mpackages_sorter.sort(array)
+               var response = new JsonArray.from(array)
+               res.json paginate(response, response.length, page, limit)
+       end
+end
+
+# Get the list of mpackages contributed by a person
+#
+# `GET /person/:pid/contributing?p=1&n=10`: return a paginated list of packages
+class APICatalogContributing
+       super APICatalogPerson
+
+       redef fun get(req, res) do
+               var person = get_person(req, res)
+               if person == null then return
+
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var array = new Array[MPackage]
+               if config.catalog.contrib2proj.has_key(person) then
+                       array = config.catalog.contrib2proj[person].to_a
+               end
+               mpackages_sorter.sort(array)
+               var response = new JsonArray.from(array)
+               res.json paginate(response, response.length, page, limit)
+       end
+end
+
+redef class APIEntity
+       redef fun get(req, res) do
+               var mentity = mentity_from_uri(req, res)
+               if mentity == null then return
+
+               # Special case for packages (catalog view)
+               if mentity isa MPackage then
+                       res.raw_json mentity.to_full_catalog_json(plain=true, config.mainmodule, config.catalog)
+               else
+                       res.raw_json mentity.to_full_json(config.mainmodule)
+               end
+       end
+end
+
+redef class APISearch
+       super APICatalogHandler
+
+       redef fun search(query, limit) do
+               var index = config.view.index
+
+               # lookup by name prefix
+               var matches = index.find_by_name_prefix(query).uniq.
+                       sort(lname_sorter, name_sorter, kind_sorter)
+               matches = matches.rerank.sort(vis_sorter, score_sorter)
+
+               # lookup by tags
+               var malus = matches.length
+               if config.catalog.tag2proj.has_key(query) then
+                       for mpackage in config.catalog.tag2proj[query] do
+                               matches.add new IndexMatch(mpackage, malus)
+                               malus += 1
+                       end
+                       matches = matches.uniq.rerank.sort(vis_sorter, score_sorter)
+               end
+
+               # lookup by full_name prefix
+               malus = matches.length
+               var full_matches = new IndexMatches
+               for match in index.find_by_full_name_prefix(query).
+                       sort(lfname_sorter, fname_sorter) do
+                       match.score += 1
+                       full_matches.add match
+               end
+               matches = matches.uniq
+
+               # lookup by similarity
+               malus = matches.length
+               var sim_matches = new IndexMatches
+               for match in index.find_by_similarity(query).sort(score_sorter, lname_sorter, name_sorter) do
+                       if match.score > query.length then break
+                       match.score += 1
+                       sim_matches.add match
+               end
+               matches.add_all sim_matches
+               matches = matches.uniq
+               return matches.rerank.sort(vis_sorter, score_sorter).mentities
+       end
+
+       private var score_sorter = new ScoreComparator
+       private var vis_sorter = new VisibilityComparator
+       private var name_sorter = new NameComparator
+       private var lname_sorter = new NameLengthComparator
+       private var fname_sorter = new FullNameComparator
+       private var lfname_sorter = new FullNameLengthComparator
+       private var kind_sorter = new MEntityComparator
+end
+
+redef class Catalog
+
+       # Build the catalog from `mpackages`
+       fun build_catalog(mpackages: Array[MPackage]) do
+               # Compute the poset
+               for p in mpackages do
+                       var g = p.root
+                       assert g != null
+                       modelbuilder.scan_group(g)
+
+                       deps.add_node(p)
+                       for gg in p.mgroups do for m in gg.mmodules do
+                               for im in m.in_importation.direct_greaters do
+                                       var ip = im.mpackage
+                                       if ip == null or ip == p then continue
+                                       deps.add_edge(p, ip)
+                               end
+                       end
+               end
+               # Build the catalog
+               for mpackage in mpackages do
+                       package_page(mpackage)
+                       git_info(mpackage)
+                       mpackage_stats(mpackage)
+               end
+       end
+end
+
+redef class MPackageMetadata
+       serialize
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("license", license)
+               v.serialize_attribute("maintainers", maintainers)
+               v.serialize_attribute("contributors", contributors)
+               v.serialize_attribute("tags", tags)
+               v.serialize_attribute("tryit", tryit)
+               v.serialize_attribute("apk", apk)
+               v.serialize_attribute("homepage", homepage)
+               v.serialize_attribute("browse", browse)
+               v.serialize_attribute("git", git)
+               v.serialize_attribute("issues", issues)
+               v.serialize_attribute("first_date", first_date)
+               v.serialize_attribute("last_date", last_date)
+       end
+end
+
+# Catalog statistics
+redef class CatalogStats
+       serialize
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("packages", packages)
+               v.serialize_attribute("maintainers", maintainers)
+               v.serialize_attribute("contributors", contributors)
+               v.serialize_attribute("tags", tags)
+               v.serialize_attribute("modules", modules)
+               v.serialize_attribute("classes", classes)
+               v.serialize_attribute("methods", methods)
+               v.serialize_attribute("loc", loc)
+       end
+end
+
+# MPackage statistics for the catalog
+redef class MPackageStats
+       serialize
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("mmodules", mmodules)
+               v.serialize_attribute("mclasses", mclasses)
+               v.serialize_attribute("mmethods", mmethods)
+               v.serialize_attribute("loc", loc)
+               v.serialize_attribute("errors", errors)
+               v.serialize_attribute("warnings", warnings)
+               v.serialize_attribute("warnings_per_kloc", warnings_per_kloc)
+               v.serialize_attribute("documentation_score", documentation_score)
+               v.serialize_attribute("commits", commits)
+               v.serialize_attribute("score", score)
        end
 end
 
 redef class Person
-       super Jsonable
+       serialize
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("name", name)
+               v.serialize_attribute("email", email)
+               v.serialize_attribute("gravatar", gravatar)
+       end
+end
+
+redef class MPackage
+       # Serialize the full catalog version of `self` to JSON
+       #
+       # See: `FullCatalogSerializer`
+       fun to_full_catalog_json(mainmodule: MModule, catalog: Catalog, plain, pretty: nullable Bool): String do
+               var stream = new StringWriter
+               var serializer = new FullCatalogSerializer(stream, mainmodule, catalog)
+               serializer.plain_json = plain or else false
+               serializer.pretty_json = pretty or else false
+               serializer.serialize self
+               stream.close
+               return stream.to_s
+       end
 
-       redef fun to_json do
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("metadata", metadata)
+               if v isa FullCatalogSerializer then
+                       v.serialize_attribute("stats", v.catalog.mpackages_stats[self])
+
+                       var parents = v.catalog.deps[self].direct_greaters.to_a
+                       v.serialize_attribute("dependencies", v.deps_to_json(parents))
+                       var children = v.catalog.deps[self].direct_smallers.to_a
+                       v.serialize_attribute("clients", v.deps_to_json(children))
+               end
+       end
+end
+
+# CatalogSerializer decorate the Package JSON with full catalog metadata
+#
+# See MEntity::to_full_catalog_json.
+class FullCatalogSerializer
+       super FullJsonSerializer
+
+       # Catalog used to decorate the MPackages
+       var catalog: Catalog
+
+       private fun deps_to_json(mpackages: Array[MPackage]): JsonArray do
+               var res = new JsonArray
+               for mpackage in mpackages do
+                       res.add dep_to_json(mpackage)
+               end
+               return res
+       end
+
+       private fun dep_to_json(mpackage: MPackage): JsonObject do
                var obj = new JsonObject
-               obj["name"] = name
-               obj["email"] = email
-               obj["page"] = page
-               obj["hash"] = (email or else "").md5.to_lower
-               return obj.to_json
+               obj["name"] = mpackage.name
+               var mdoc = mpackage.mdoc_or_fallback
+               if mdoc != null then
+                       obj["synopsis"] = mdoc.synopsis.write_to_string
+               end
+               return obj
        end
 end