--- /dev/null
+# 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.
+
+module api_catalog
+
+import api_model
+import catalog::catalog_json
+
+redef class NitwebConfig
+
+ # Catalog to pass to handlers.
+ 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
+
+redef class APIRouter
+ redef init do
+ super
+ use("/catalog/packages/", new APICatalogPackages(config))
+ use("/catalog/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
+
+ # Sorter used to sort packages
+ #
+ # Sorting is based on mpackage score.
+ var mpackages_sorter = new CatalogScoreSorter(config.catalog) is lazy
+end
+
+# 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.api_json(req, 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
+ res.api_json(req, config.catalog.catalog_stats)
+ end
+end
+
+# Get all the tags from the catalog
+#
+# `GET /tags`: the list of tags associated with their number of packages
+class APICatalogTags
+ super APICatalogHandler
+
+ # 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.api_json(req, obj)
+ end
+end
+
+# 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
+ 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
+ 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.api_json(req, obj)
+ end
+end
+
+# Get a person existing in the catalog
+#
+# `GET /person/:pid`: get the person with `pid`
+class APICatalogPerson
+ super APICatalogHandler
+
+ # 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.api_json(req, person)
+ end
+end
+
+# 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 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.api_json(req, 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.api_json(req, 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(config.catalog, plain = true, pretty = req.bool_arg("pretty"))
+ else
+ res.api_full_json(req, mentity)
+ 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 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 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"] = mpackage.name
+ var mdoc = mpackage.mdoc_or_fallback
+ if mdoc != null then
+ obj["synopsis"] = mdoc.synopsis.write_to_string
+ end
+ return obj
+ end
+end