nitweb: use catalog_json
[nit.git] / src / web / api_catalog.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 module api_catalog
16
17 import api_model
18 import catalog::catalog_json
19
20 redef class NitwebConfig
21
22 # Catalog to pass to handlers.
23 var catalog: Catalog is noinit
24
25 # Build the catalog
26 #
27 # This method should be called at nitweb startup.
28 fun build_catalog do
29 self.catalog = new Catalog(modelbuilder)
30 self.catalog.build_catalog(model.mpackages)
31 end
32 end
33
34 redef class APIRouter
35 redef init do
36 super
37 use("/catalog/packages/", new APICatalogPackages(config))
38 use("/catalog/stats", new APICatalogStats(config))
39
40 use("/catalog/tags", new APICatalogTags(config))
41 use("/catalog/tag/:tid", new APICatalogTag(config))
42
43 use("/catalog/person/:pid", new APICatalogPerson(config))
44 use("/catalog/person/:pid/maintaining", new APICatalogMaintaining(config))
45 use("/catalog/person/:pid/contributing", new APICatalogContributing(config))
46 end
47 end
48
49 abstract class APICatalogHandler
50 super APIHandler
51
52 # Sorter used to sort packages
53 #
54 # Sorting is based on mpackage score.
55 var mpackages_sorter = new CatalogScoreSorter(config.catalog) is lazy
56 end
57
58 # Get all the packages from the catalog using pagination
59 #
60 # `GET /packages?p=1&n=10`: get the list of catalog by page
61 class APICatalogPackages
62 super APICatalogHandler
63
64 redef fun get(req, res) do
65 var page = req.int_arg("p")
66 var limit = req.int_arg("n")
67 var mpackages = config.catalog.mpackages.values.to_a
68 mpackages_sorter.sort(mpackages)
69 var response = new JsonArray.from(mpackages)
70 res.json paginate(response, response.length, page, limit)
71 end
72 end
73
74 # Get the catalog statistics
75 #
76 # `GET /stats`: return the catalog statistics
77 class APICatalogStats
78 super APICatalogHandler
79
80 redef fun get(req, res) do
81 res.json config.catalog.catalog_stats
82 end
83 end
84
85 # Get all the tags from the catalog
86 #
87 # `GET /tags`: the list of tags associated with their number of packages
88 class APICatalogTags
89 super APICatalogHandler
90
91 # Sorter to sort tags alphabetically
92 var tags_sorter = new CatalogTagsSorter
93
94 redef fun get(req, res) do
95 var obj = new JsonObject
96
97 var tags = config.catalog.tag2proj.keys.to_a
98 tags_sorter.sort(tags)
99
100 for tag in tags do
101 if not config.catalog.tag2proj.has_key(tag) then continue
102 obj[tag] = config.catalog.tag2proj[tag].length
103 end
104 res.json obj
105 end
106 end
107
108 # Get the packages related to a tag
109 #
110 # `GET /tag/:tid?p=1&n=10`: return a paginated list of packages
111 class APICatalogTag
112 super APICatalogHandler
113
114 redef fun get(req, res) do
115 var page = req.int_arg("p")
116 var limit = req.int_arg("n")
117 var id = req.param("tid")
118 if id == null then
119 res.api_error(400, "Missing tag")
120 return
121 end
122 id = id.from_percent_encoding
123 if not config.catalog.tag2proj.has_key(id) then
124 res.api_error(404, "Tag not found")
125 return
126 end
127 var obj = new JsonObject
128 obj["tag"] = id
129 var mpackages = config.catalog.tag2proj[id]
130 mpackages_sorter.sort(mpackages)
131 var response = new JsonArray.from(mpackages)
132 obj["packages"] = paginate(response, response.length, page, limit)
133 res.json obj
134 end
135 end
136
137 # Get a person existing in the catalog
138 #
139 # `GET /person/:pid`: get the person with `pid`
140 class APICatalogPerson
141 super APICatalogHandler
142
143 # Get the person with `:pid` or throw a 404 error
144 fun get_person(req: HttpRequest, res: HttpResponse): nullable Person do
145 var id = req.param("pid")
146 if id == null then
147 res.api_error(400, "Missing package full_name")
148 return null
149 end
150 id = id.from_percent_encoding
151 if not config.catalog.name2person.has_key(id) then
152 res.api_error(404, "Person not found")
153 return null
154 end
155 return config.catalog.name2person[id]
156 end
157
158 redef fun get(req, res) do
159 var person = get_person(req, res)
160 if person == null then return
161 res.json person
162 end
163 end
164
165 # Get the list of mpackages maintained by a person
166 #
167 # `GET /person/:pid/maintaining?p=1&n=10`: return a paginated list of packages
168 class APICatalogMaintaining
169 super APICatalogPerson
170
171 redef fun get(req, res) do
172 var person = get_person(req, res)
173 if person == null then return
174
175 var page = req.int_arg("p")
176 var limit = req.int_arg("n")
177 var array = new Array[MPackage]
178 if config.catalog.maint2proj.has_key(person) then
179 array = config.catalog.maint2proj[person].to_a
180 end
181 mpackages_sorter.sort(array)
182 var response = new JsonArray.from(array)
183 res.json paginate(response, response.length, page, limit)
184 end
185 end
186
187 # Get the list of mpackages contributed by a person
188 #
189 # `GET /person/:pid/contributing?p=1&n=10`: return a paginated list of packages
190 class APICatalogContributing
191 super APICatalogPerson
192
193 redef fun get(req, res) do
194 var person = get_person(req, res)
195 if person == null then return
196
197 var page = req.int_arg("p")
198 var limit = req.int_arg("n")
199 var array = new Array[MPackage]
200 if config.catalog.contrib2proj.has_key(person) then
201 array = config.catalog.contrib2proj[person].to_a
202 end
203 mpackages_sorter.sort(array)
204 var response = new JsonArray.from(array)
205 res.json paginate(response, response.length, page, limit)
206 end
207 end
208
209 redef class APIEntity
210 redef fun get(req, res) do
211 var mentity = mentity_from_uri(req, res)
212 if mentity == null then return
213
214 # Special case for packages (catalog view)
215 if mentity isa MPackage then
216 res.raw_json mentity.to_full_catalog_json(plain=true, config.mainmodule, config.catalog)
217 else
218 res.raw_json mentity.to_full_json(config.mainmodule)
219 end
220 end
221 end
222
223 redef class APISearch
224 super APICatalogHandler
225
226 redef fun search(query, limit) do
227 var index = config.view.index
228
229 # lookup by name prefix
230 var matches = index.find_by_name_prefix(query).uniq.
231 sort(lname_sorter, name_sorter, kind_sorter)
232 matches = matches.rerank.sort(vis_sorter, score_sorter)
233
234 # lookup by tags
235 var malus = matches.length
236 if config.catalog.tag2proj.has_key(query) then
237 for mpackage in config.catalog.tag2proj[query] do
238 matches.add new IndexMatch(mpackage, malus)
239 malus += 1
240 end
241 matches = matches.uniq.rerank.sort(vis_sorter, score_sorter)
242 end
243
244 # lookup by full_name prefix
245 malus = matches.length
246 var full_matches = new IndexMatches
247 for match in index.find_by_full_name_prefix(query).
248 sort(lfname_sorter, fname_sorter) do
249 match.score += 1
250 full_matches.add match
251 end
252 matches = matches.uniq
253
254 # lookup by similarity
255 malus = matches.length
256 var sim_matches = new IndexMatches
257 for match in index.find_by_similarity(query).sort(score_sorter, lname_sorter, name_sorter) do
258 if match.score > query.length then break
259 match.score += 1
260 sim_matches.add match
261 end
262 matches.add_all sim_matches
263 matches = matches.uniq
264 return matches.rerank.sort(vis_sorter, score_sorter).mentities
265 end
266
267 private var score_sorter = new ScoreComparator
268 private var vis_sorter = new VisibilityComparator
269 private var name_sorter = new NameComparator
270 private var lname_sorter = new NameLengthComparator
271 private var fname_sorter = new FullNameComparator
272 private var lfname_sorter = new FullNameLengthComparator
273 private var kind_sorter = new MEntityComparator
274 end
275
276 redef class Catalog
277
278 # Build the catalog from `mpackages`
279 fun build_catalog(mpackages: Array[MPackage]) do
280 # Compute the poset
281 for p in mpackages do
282 var g = p.root
283 assert g != null
284 modelbuilder.scan_group(g)
285
286 deps.add_node(p)
287 for gg in p.mgroups do for m in gg.mmodules do
288 for im in m.in_importation.direct_greaters do
289 var ip = im.mpackage
290 if ip == null or ip == p then continue
291 deps.add_edge(p, ip)
292 end
293 end
294 end
295 # Build the catalog
296 for mpackage in mpackages do
297 package_page(mpackage)
298 git_info(mpackage)
299 mpackage_stats(mpackage)
300 end
301 end
302 end
303
304 redef class MPackage
305 # Serialize the full catalog version of `self` to JSON
306 #
307 # See: `FullCatalogSerializer`
308 fun to_full_catalog_json(mainmodule: MModule, catalog: Catalog, plain, pretty: nullable Bool): String do
309 var stream = new StringWriter
310 var serializer = new FullCatalogSerializer(stream, mainmodule, catalog)
311 serializer.plain_json = plain or else false
312 serializer.pretty_json = pretty or else false
313 serializer.serialize self
314 stream.close
315 return stream.to_s
316 end
317
318 redef fun core_serialize_to(v) do
319 super
320 v.serialize_attribute("metadata", metadata)
321 if v isa FullCatalogSerializer then
322 v.serialize_attribute("stats", v.catalog.mpackages_stats[self])
323
324 var parents = v.catalog.deps[self].direct_greaters.to_a
325 v.serialize_attribute("dependencies", v.deps_to_json(parents))
326 var children = v.catalog.deps[self].direct_smallers.to_a
327 v.serialize_attribute("clients", v.deps_to_json(children))
328 end
329 end
330 end
331
332 # CatalogSerializer decorate the Package JSON with full catalog metadata
333 #
334 # See MEntity::to_full_catalog_json.
335 class FullCatalogSerializer
336 super FullJsonSerializer
337
338 # Catalog used to decorate the MPackages
339 var catalog: Catalog
340
341 private fun deps_to_json(mpackages: Array[MPackage]): JsonArray do
342 var res = new JsonArray
343 for mpackage in mpackages do
344 res.add dep_to_json(mpackage)
345 end
346 return res
347 end
348
349 private fun dep_to_json(mpackage: MPackage): JsonObject do
350 var obj = new JsonObject
351 obj["name"] = mpackage.name
352 var mdoc = mpackage.mdoc_or_fallback
353 if mdoc != null then
354 obj["synopsis"] = mdoc.synopsis.write_to_string
355 end
356 return obj
357 end
358 end