d22289b3385b98bc564b891674756423a62a7107
[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
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 APIEntity
278 redef fun get(req, res) do
279 var mentity = mentity_from_uri(req, res)
280 if mentity == null then return
281
282 # Special case for packages (catalog view)
283 if mentity isa MPackage then
284 res.raw_json mentity.to_full_catalog_json(plain=true, config.catalog)
285 else
286 res.raw_json mentity.to_full_json
287 end
288 end
289 end
290
291 redef class Catalog
292
293 # Build the catalog from `mpackages`
294 fun build_catalog(mpackages: Array[MPackage]) do
295 # Compute the poset
296 for p in mpackages do
297 var g = p.root
298 assert g != null
299 modelbuilder.scan_group(g)
300
301 deps.add_node(p)
302 for gg in p.mgroups do for m in gg.mmodules do
303 for im in m.in_importation.direct_greaters do
304 var ip = im.mpackage
305 if ip == null or ip == p then continue
306 deps.add_edge(p, ip)
307 end
308 end
309 end
310 # Build the catalog
311 for mpackage in mpackages do
312 package_page(mpackage)
313 git_info(mpackage)
314 mpackage_stats(mpackage)
315 end
316 end
317 end
318
319 redef class MPackageMetadata
320 serialize
321
322 redef fun core_serialize_to(v) do
323 super
324 v.serialize_attribute("license", license)
325 v.serialize_attribute("maintainers", maintainers)
326 v.serialize_attribute("contributors", contributors)
327 v.serialize_attribute("tags", tags)
328 v.serialize_attribute("tryit", tryit)
329 v.serialize_attribute("apk", apk)
330 v.serialize_attribute("homepage", homepage)
331 v.serialize_attribute("browse", browse)
332 v.serialize_attribute("git", git)
333 v.serialize_attribute("issues", issues)
334 v.serialize_attribute("first_date", first_date)
335 v.serialize_attribute("last_date", last_date)
336 end
337 end
338
339 # Catalog statistics
340 redef class CatalogStats
341 serialize
342
343 redef fun core_serialize_to(v) do
344 super
345 v.serialize_attribute("packages", packages)
346 v.serialize_attribute("maintainers", maintainers)
347 v.serialize_attribute("contributors", contributors)
348 v.serialize_attribute("tags", tags)
349 v.serialize_attribute("modules", modules)
350 v.serialize_attribute("classes", classes)
351 v.serialize_attribute("methods", methods)
352 v.serialize_attribute("loc", loc)
353 end
354 end
355
356 # MPackage statistics for the catalog
357 redef class MPackageStats
358 serialize
359
360 redef fun core_serialize_to(v) do
361 super
362 v.serialize_attribute("mmodules", mmodules)
363 v.serialize_attribute("mclasses", mclasses)
364 v.serialize_attribute("mmethods", mmethods)
365 v.serialize_attribute("loc", loc)
366 v.serialize_attribute("errors", errors)
367 v.serialize_attribute("warnings", warnings)
368 v.serialize_attribute("warnings_per_kloc", warnings_per_kloc)
369 v.serialize_attribute("documentation_score", documentation_score)
370 v.serialize_attribute("commits", commits)
371 v.serialize_attribute("score", score)
372 end
373 end
374
375 redef class Person
376 serialize
377
378 redef fun core_serialize_to(v) do
379 super
380 v.serialize_attribute("name", name)
381 v.serialize_attribute("email", email)
382 v.serialize_attribute("gravatar", gravatar)
383 end
384 end
385
386 redef class MPackage
387 # Serialize the full catalog version of `self` to JSON
388 #
389 # See: `FullCatalogSerializer`
390 fun to_full_catalog_json(catalog: Catalog, plain, pretty: nullable Bool): String do
391 var stream = new StringWriter
392 var serializer = new FullCatalogSerializer(stream, catalog)
393 serializer.plain_json = plain or else false
394 serializer.pretty_json = pretty or else false
395 serializer.serialize self
396 stream.close
397 return stream.to_s
398 end
399
400 redef fun core_serialize_to(v) do
401 super
402 v.serialize_attribute("metadata", metadata)
403 if v isa FullCatalogSerializer then
404 v.serialize_attribute("stats", v.catalog.mpackages_stats[self])
405
406 var parents = v.catalog.deps[self].direct_greaters.to_a
407 v.serialize_attribute("dependencies", v.deps_to_json(parents))
408 var children = v.catalog.deps[self].direct_smallers.to_a
409 v.serialize_attribute("clients", v.deps_to_json(children))
410 end
411 end
412 end
413
414 # CatalogSerializer decorate the Package JSON with full catalog metadata
415 #
416 # See MEntity::to_full_catalog_json.
417 class FullCatalogSerializer
418 super FullJsonSerializer
419
420 # Catalog used to decorate the MPackages
421 var catalog: Catalog
422
423 private fun deps_to_json(mpackages: Array[MPackage]): JsonArray do
424 var res = new JsonArray
425 for mpackage in mpackages do
426 res.add dep_to_json(mpackage)
427 end
428 return res
429 end
430
431 private fun dep_to_json(mpackage: MPackage): JsonObject do
432 var obj = new JsonObject
433 obj["name"] = mpackage.name
434 var mdoc = mpackage.mdoc_or_fallback
435 if mdoc != null then
436 obj["synopsis"] = mdoc.synopsis.write_to_string
437 end
438 return obj
439 end
440 end