9bca8bef9a2d2ea027061e98a70ac097e7a6813f
[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 web_base
18 import catalog
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
39 use("/catalog/highlighted", new APICatalogHighLighted(config))
40 use("/catalog/required", new APICatalogMostRequired(config))
41 use("/catalog/bytags", new APICatalogByTags(config))
42 use("/catalog/contributors", new APICatalogContributors(config))
43 use("/catalog/stats", new APICatalogStats(config))
44
45 use("/catalog/tags", new APICatalogTags(config))
46 use("/catalog/tag/:tid", new APICatalogTag(config))
47
48 use("/catalog/person/:pid", new APICatalogPerson(config))
49 use("/catalog/person/:pid/maintaining", new APICatalogMaintaining(config))
50 use("/catalog/person/:pid/contributing", new APICatalogContributing(config))
51 end
52 end
53
54 abstract class APICatalogHandler
55 super APIHandler
56
57 # Sorter used to sort packages
58 #
59 # Sorting is based on mpackage score.
60 var mpackages_sorter = new CatalogScoreSorter(config.catalog) is lazy
61
62 # List the 10 best packages from `cpt`
63 fun list_best(cpt: Counter[MPackage]): JsonArray do
64 var res = new JsonArray
65 var best = cpt.sort
66 for i in [1..10] do
67 if i > best.length then break
68 res.add best[best.length-i]
69 end
70 return res
71 end
72
73 # List packages by group.
74 fun list_by(map: MultiHashMap[Object, MPackage]): JsonObject do
75 var res = new JsonObject
76 var keys = map.keys.to_a
77 alpha_comparator.sort(keys)
78 for k in keys do
79 var projs = map[k].to_a
80 alpha_comparator.sort(projs)
81 res[k.to_s.html_escape] = new JsonArray.from(projs)
82 end
83 return res
84 end
85 end
86
87 # Get all the packages from the catalog using pagination
88 #
89 # `GET /packages?p=1&n=10`: get the list of catalog by page
90 class APICatalogPackages
91 super APICatalogHandler
92
93 redef fun get(req, res) do
94 var page = req.int_arg("p")
95 var limit = req.int_arg("n")
96 var mpackages = config.catalog.mpackages.values.to_a
97 mpackages_sorter.sort(mpackages)
98 var response = new JsonArray.from(mpackages)
99 res.json paginate(response, response.length, page, limit)
100 end
101 end
102
103 class APICatalogHighLighted
104 super APICatalogHandler
105
106 redef fun get(req, res) do res.json list_best(config.catalog.score)
107 end
108
109 class APICatalogMostRequired
110 super APICatalogHandler
111
112 redef fun get(req, res) do
113 if config.catalog.deps.not_empty then
114 var reqs = new Counter[MPackage]
115 for p in config.model.mpackages do
116 reqs[p] = config.catalog.deps[p].smallers.length - 1
117 end
118 res.json list_best(reqs)
119 return
120 end
121 res.json new JsonArray
122 end
123 end
124
125 class APICatalogByTags
126 super APICatalogHandler
127
128 redef fun get(req, res) do res.json list_by(config.catalog.tag2proj)
129 end
130
131 class APICatalogContributors
132 super APICatalogHandler
133
134 redef fun get(req, res) do
135 var obj = new JsonObject
136 obj["maintainers"] = new JsonArray.from(config.catalog.maint2proj.keys)
137 obj["contributors"] = new JsonArray.from(config.catalog.contrib2proj.keys)
138 res.json obj
139 end
140 end
141
142 # Get the catalog statistics
143 #
144 # `GET /stats`: return the catalog statistics
145 class APICatalogStats
146 super APICatalogHandler
147
148 redef fun get(req, res) do
149 res.json config.catalog.catalog_stats
150 end
151 end
152
153 # Get all the tags from the catalog
154 #
155 # `GET /tags`: the list of tags associated with their number of packages
156 class APICatalogTags
157 super APICatalogHandler
158
159 # Sorter to sort tags alphabetically
160 var tags_sorter = new CatalogTagsSorter
161
162 redef fun get(req, res) do
163 var obj = new JsonObject
164
165 var tags = config.catalog.tag2proj.keys.to_a
166 tags_sorter.sort(tags)
167
168 for tag in tags do
169 if not config.catalog.tag2proj.has_key(tag) then continue
170 obj[tag] = config.catalog.tag2proj[tag].length
171 end
172 res.json obj
173 end
174 end
175
176 # Get the packages related to a tag
177 #
178 # `GET /tag/:tid?p=1&n=10`: return a paginated list of packages
179 class APICatalogTag
180 super APICatalogHandler
181
182 redef fun get(req, res) do
183 var page = req.int_arg("p")
184 var limit = req.int_arg("n")
185 var id = req.param("tid")
186 if id == null then
187 res.api_error(400, "Missing tag")
188 return
189 end
190 id = id.from_percent_encoding
191 if not config.catalog.tag2proj.has_key(id) then
192 res.api_error(404, "Tag not found")
193 return
194 end
195 var obj = new JsonObject
196 obj["tag"] = id
197 var mpackages = config.catalog.tag2proj[id]
198 mpackages_sorter.sort(mpackages)
199 var response = new JsonArray.from(mpackages)
200 obj["packages"] = paginate(response, response.length, page, limit)
201 res.json obj
202 end
203 end
204
205 # Get a person existing in the catalog
206 #
207 # `GET /person/:pid`: get the person with `pid`
208 class APICatalogPerson
209 super APICatalogHandler
210
211 # Get the person with `:pid` or throw a 404 error
212 fun get_person(req: HttpRequest, res: HttpResponse): nullable Person do
213 var id = req.param("pid")
214 if id == null then
215 res.api_error(400, "Missing package full_name")
216 return null
217 end
218 id = id.from_percent_encoding
219 if not config.catalog.name2person.has_key(id) then
220 res.api_error(404, "Person not found")
221 return null
222 end
223 return config.catalog.name2person[id]
224 end
225
226 redef fun get(req, res) do
227 var person = get_person(req, res)
228 if person == null then return
229 res.json person
230 end
231 end
232
233 # Get the list of mpackages maintained by a person
234 #
235 # `GET /person/:pid/maintaining?p=1&n=10`: return a paginated list of packages
236 class APICatalogMaintaining
237 super APICatalogPerson
238
239 redef fun get(req, res) do
240 var person = get_person(req, res)
241 if person == null then return
242
243 var page = req.int_arg("p")
244 var limit = req.int_arg("n")
245 var array = new Array[MPackage]
246 if config.catalog.maint2proj.has_key(person) then
247 array = config.catalog.maint2proj[person].to_a
248 end
249 mpackages_sorter.sort(array)
250 var response = new JsonArray.from(array)
251 res.json paginate(response, response.length, page, limit)
252 end
253 end
254
255 # Get the list of mpackages contributed by a person
256 #
257 # `GET /person/:pid/contributing?p=1&n=10`: return a paginated list of packages
258 class APICatalogContributing
259 super APICatalogPerson
260
261 redef fun get(req, res) do
262 var person = get_person(req, res)
263 if person == null then return
264
265 var page = req.int_arg("p")
266 var limit = req.int_arg("n")
267 var array = new Array[MPackage]
268 if config.catalog.contrib2proj.has_key(person) then
269 array = config.catalog.contrib2proj[person].to_a
270 end
271 mpackages_sorter.sort(array)
272 var response = new JsonArray.from(array)
273 res.json paginate(response, response.length, page, limit)
274 end
275 end
276
277 redef class Catalog
278
279 # Build the catalog from `mpackages`
280 fun build_catalog(mpackages: Array[MPackage]) do
281 # Compute the poset
282 for p in mpackages do
283 var g = p.root
284 assert g != null
285 modelbuilder.scan_group(g)
286
287 deps.add_node(p)
288 for gg in p.mgroups do for m in gg.mmodules do
289 for im in m.in_importation.direct_greaters do
290 var ip = im.mpackage
291 if ip == null or ip == p then continue
292 deps.add_edge(p, ip)
293 end
294 end
295 end
296 # Build the catalog
297 for mpackage in mpackages do
298 package_page(mpackage)
299 git_info(mpackage)
300 mpackage_stats(mpackage)
301 end
302 end
303 end
304
305 redef class MPackageMetadata
306 serialize
307
308 redef fun core_serialize_to(v) do
309 super
310 v.serialize_attribute("license", license)
311 v.serialize_attribute("maintainers", maintainers)
312 v.serialize_attribute("contributors", contributors)
313 v.serialize_attribute("tags", tags)
314 v.serialize_attribute("tryit", tryit)
315 v.serialize_attribute("apk", apk)
316 v.serialize_attribute("homepage", homepage)
317 v.serialize_attribute("browse", browse)
318 v.serialize_attribute("git", git)
319 v.serialize_attribute("issues", issues)
320 v.serialize_attribute("first_date", first_date)
321 v.serialize_attribute("last_date", last_date)
322 end
323 end
324
325 # Catalog statistics
326 redef class CatalogStats
327 serialize
328
329 redef fun core_serialize_to(v) do
330 super
331 v.serialize_attribute("packages", packages)
332 v.serialize_attribute("maintainers", maintainers)
333 v.serialize_attribute("contributors", contributors)
334 v.serialize_attribute("tags", tags)
335 v.serialize_attribute("modules", modules)
336 v.serialize_attribute("classes", classes)
337 v.serialize_attribute("methods", methods)
338 v.serialize_attribute("loc", loc)
339 end
340 end
341
342 # MPackage statistics for the catalog
343 redef class MPackageStats
344 serialize
345
346 redef fun core_serialize_to(v) do
347 super
348 v.serialize_attribute("mmodules", mmodules)
349 v.serialize_attribute("mclasses", mclasses)
350 v.serialize_attribute("mmethods", mmethods)
351 v.serialize_attribute("loc", loc)
352 v.serialize_attribute("errors", errors)
353 v.serialize_attribute("warnings", warnings)
354 v.serialize_attribute("warnings_per_kloc", warnings_per_kloc)
355 v.serialize_attribute("documentation_score", documentation_score)
356 v.serialize_attribute("commits", commits)
357 v.serialize_attribute("score", score)
358 end
359 end
360
361 redef class Person
362 serialize
363
364 redef fun core_serialize_to(v) do
365 super
366 v.serialize_attribute("name", name)
367 v.serialize_attribute("email", email)
368 v.serialize_attribute("gravatar", gravatar)
369 end
370 end
371
372 redef class MPackage
373 # Serialize the full catalog version of `self` to JSON
374 #
375 # See: `FullCatalogSerializer`
376 fun to_full_catalog_json(catalog: Catalog, plain, pretty: nullable Bool): String do
377 var stream = new StringWriter
378 var serializer = new FullCatalogSerializer(stream, catalog)
379 serializer.plain_json = plain or else false
380 serializer.pretty_json = pretty or else false
381 serializer.serialize self
382 stream.close
383 return stream.to_s
384 end
385
386 redef fun core_serialize_to(v) do
387 super
388 v.serialize_attribute("metadata", metadata)
389 if v isa FullCatalogSerializer then
390 v.serialize_attribute("stats", v.catalog.mpackages_stats[self])
391
392 var parents = v.catalog.deps[self].direct_greaters.to_a
393 v.serialize_attribute("dependencies", v.deps_to_json(parents))
394 var children = v.catalog.deps[self].direct_smallers.to_a
395 v.serialize_attribute("clients", v.deps_to_json(children))
396 end
397 end
398 end
399
400 # CatalogSerializer decorate the Package JSON with full catalog metadata
401 #
402 # See MEntity::to_full_catalog_json.
403 class FullCatalogSerializer
404 super FullJsonSerializer
405
406 # Catalog used to decorate the MPackages
407 var catalog: Catalog
408
409 private fun deps_to_json(mpackages: Array[MPackage]): JsonArray do
410 var res = new JsonArray
411 for mpackage in mpackages do
412 res.add dep_to_json(mpackage)
413 end
414 return res
415 end
416
417 private fun dep_to_json(mpackage: MPackage): JsonObject do
418 var obj = new JsonObject
419 obj["name"] = mpackage.name
420 var mdoc = mpackage.mdoc_or_fallback
421 if mdoc != null then
422 obj["synopsis"] = mdoc.synopsis.write_to_string
423 end
424 return obj
425 end
426 end