lib/github: better type safety on json accesses
[nit.git] / lib / github / api.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 # Nit object oriented interface to [Github api](https://developer.github.com/v3/).
16 #
17 # This modules reifies Github API elements as Nit classes.
18 #
19 # For most use-cases you need to use the `GithubAPI` client.
20 module api
21
22 import github_curl
23
24 # Client to Github API
25 #
26 # To access the API you need an instance of a `GithubAPI` client.
27 #
28 # ~~~
29 # # Get Github authentification token.
30 # var token = get_github_oauth
31 # assert not token.is_empty
32 #
33 # # Init the client.
34 # var api = new GithubAPI(token)
35 # ~~~
36 #
37 # The API client allows you to get Github API entities.
38 #
39 # ~~~
40 # var repo = api.load_repo("nitlang/nit")
41 # assert repo != null
42 # assert repo.name == "nit"
43 #
44 # var user = api.load_user("Morriar")
45 # assert user != null
46 # assert user.login == "Morriar"
47 # ~~~
48 class GithubAPI
49
50 # Github API OAuth token
51 #
52 # To access your private ressources, you must
53 # [authenticate](https://developer.github.com/guides/basics-of-authentication/).
54 #
55 # For client applications, Github recommands to use the
56 # [OAuth tokens](https://developer.github.com/v3/oauth/) authentification method.
57 #
58 #
59 #
60 # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
61 # associated to the key.
62 var auth: String
63
64 # Github API base url.
65 #
66 # Default is `https://api.github.com` and should not be changed.
67 var api_url = "https://api.github.com"
68
69 # User agent used for HTTP requests.
70 #
71 # Default is `nit_github_api`.
72 #
73 # See <https://developer.github.com/v3/#user-agent-required>
74 var user_agent = "nit_github_api"
75
76 # Curl instance.
77 #
78 # Internal Curl instance used to perform API calls.
79 private var ghcurl: GithubCurl is noinit
80
81 # Verbosity level.
82 #
83 # * `0`: only errors (default)
84 # * `1`: verbose
85 var verbose_lvl = 0 is public writable
86
87 init do
88 ghcurl = new GithubCurl(auth, user_agent)
89 end
90
91 # Execute a GET request on Github API.
92 #
93 # This method returns raw json data.
94 # See other `load_*` methods to use more expressive types.
95 #
96 # var api = new GithubAPI(get_github_oauth)
97 # var obj = api.get("repos/nitlang/nit")
98 # assert obj isa JsonObject
99 # assert obj["name"] == "nit"
100 #
101 # Returns `null` in case of `error`.
102 #
103 # obj = api.get("foo/bar/baz")
104 # assert obj == null
105 # assert api.was_error
106 # var err = api.last_error
107 # assert err isa GithubError
108 # assert err.name == "GithubAPIError"
109 # assert err.message == "Not Found"
110 fun get(path: String): nullable Jsonable do
111 path = sanitize_uri(path)
112 var res = ghcurl.get_and_parse("{api_url}/{path}")
113 if res isa Error then
114 last_error = res
115 was_error = true
116 return null
117 end
118 was_error = false
119 return res
120 end
121
122 # Display a message depending on `verbose_lvl`.
123 fun message(lvl: Int, message: String) do
124 if lvl <= verbose_lvl then print message
125 end
126
127 # Escape `uri` in an acceptable format for Github.
128 private fun sanitize_uri(uri: String): String do
129 # TODO better URI escape.
130 return uri.replace(" ", "%20")
131 end
132
133 # Last error occured during Github API communications.
134 var last_error: nullable Error = null is public writable
135
136 # Does the last request provoqued an error?
137 var was_error = false is protected writable
138
139 # Load the json object from Github.
140 # See `GithubEntity::load_from_github`.
141 protected fun load_from_github(key: String): JsonObject do
142 message(1, "Get {key} (github)")
143 var res = get(key)
144 if was_error then return new JsonObject
145 return res.as(JsonObject)
146 end
147
148 # Get the Github user with `login`
149 #
150 # Loads the `User` from the API or returns `null` if the user cannot be found.
151 #
152 # var api = new GithubAPI(get_github_oauth)
153 # var user = api.load_user("Morriar")
154 # assert user.login == "Morriar"
155 fun load_user(login: String): nullable User do
156 var user = new User(self, login)
157 return user.load_from_github
158 end
159
160 # Get the Github repo with `full_name`.
161 #
162 # Loads the `Repo` from the API or returns `null` if the repo cannot be found.
163 #
164 # var api = new GithubAPI(get_github_oauth)
165 # var repo = api.load_repo("nitlang/nit")
166 # assert repo.name == "nit"
167 # assert repo.owner.login == "nitlang"
168 # assert repo.default_branch.name == "master"
169 fun load_repo(full_name: String): nullable Repo do
170 var repo = new Repo(self, full_name)
171 return repo.load_from_github
172 end
173
174 # Get the Github branch with `name`.
175 #
176 # Returns `null` if the branch cannot be found.
177 #
178 # var api = new GithubAPI(get_github_oauth)
179 # var repo = api.load_repo("nitlang/nit")
180 # assert repo != null
181 # var branch = api.load_branch(repo, "master")
182 # assert branch.name == "master"
183 # assert branch.commit isa Commit
184 fun load_branch(repo: Repo, name: String): nullable Branch do
185 var branch = new Branch(self, repo, name)
186 return branch.load_from_github
187 end
188
189 # Get the Github commit with `sha`.
190 #
191 # Returns `null` if the commit cannot be found.
192 #
193 # var api = new GithubAPI(get_github_oauth)
194 # var repo = api.load_repo("nitlang/nit")
195 # assert repo != null
196 # var commit = api.load_commit(repo, "64ce1f")
197 # assert commit isa Commit
198 fun load_commit(repo: Repo, sha: String): nullable Commit do
199 var commit = new Commit(self, repo, sha)
200 return commit.load_from_github
201 end
202
203 # Get the Github issue #`number`.
204 #
205 # Returns `null` if the issue cannot be found.
206 #
207 # var api = new GithubAPI(get_github_oauth)
208 # var repo = api.load_repo("nitlang/nit")
209 # assert repo != null
210 # var issue = api.load_issue(repo, 1)
211 # assert issue.title == "Doc"
212 fun load_issue(repo: Repo, number: Int): nullable Issue do
213 var issue = new Issue(self, repo, number)
214 return issue.load_from_github
215 end
216
217 # Get the Github pull request #`number`.
218 #
219 # Returns `null` if the pull request cannot be found.
220 #
221 # var api = new GithubAPI(get_github_oauth)
222 # var repo = api.load_repo("nitlang/nit")
223 # assert repo != null
224 # var pull = api.load_pull(repo, 1)
225 # assert pull.title == "Doc"
226 # assert pull.user.login == "Morriar"
227 fun load_pull(repo: Repo, number: Int): nullable PullRequest do
228 var pull = new PullRequest(self, repo, number)
229 return pull.load_from_github
230 end
231
232 # Get the Github label with `name`.
233 #
234 # Returns `null` if the label cannot be found.
235 #
236 # var api = new GithubAPI(get_github_oauth)
237 # var repo = api.load_repo("nitlang/nit")
238 # assert repo != null
239 # var labl = api.load_label(repo, "ok_will_merge")
240 # assert labl != null
241 fun load_label(repo: Repo, name: String): nullable Label do
242 var labl = new Label(self, repo, name)
243 return labl.load_from_github
244 end
245
246 # Get the Github milestone with `id`.
247 #
248 # Returns `null` if the milestone cannot be found.
249 #
250 # var api = new GithubAPI(get_github_oauth)
251 # var repo = api.load_repo("nitlang/nit")
252 # assert repo != null
253 # var stone = api.load_milestone(repo, 4)
254 # assert stone.title == "v1.0prealpha"
255 fun load_milestone(repo: Repo, id: Int): nullable Milestone do
256 var milestone = new Milestone(self, repo, id)
257 return milestone.load_from_github
258 end
259
260 # Get the Github issue event with `id`.
261 #
262 # Returns `null` if the event cannot be found.
263 #
264 # var api = new GithubAPI(get_github_oauth)
265 # var repo = api.load_repo("nitlang/nit")
266 # assert repo isa Repo
267 # var event = api.load_issue_event(repo, 199674194)
268 # assert event.actor.login == "privat"
269 # assert event.event == "labeled"
270 # assert event.labl.name == "need_review"
271 # assert event.issue.number == 945
272 fun load_issue_event(repo: Repo, id: Int): nullable IssueEvent do
273 var event = new IssueEvent(self, repo, id)
274 return event.load_from_github
275 end
276
277 # Get the Github commit comment with `id`.
278 #
279 # Returns `null` if the comment cannot be found.
280 #
281 # var api = new GithubAPI(get_github_oauth)
282 # var repo = api.load_repo("nitlang/nit")
283 # assert repo != null
284 # var comment = api.load_commit_comment(repo, 8982707)
285 # assert comment.user.login == "Morriar"
286 # assert comment.body == "For testing purposes..."
287 # assert comment.commit.sha == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
288 fun load_commit_comment(repo: Repo, id: Int): nullable CommitComment do
289 var comment = new CommitComment(self, repo, id)
290 return comment.load_from_github
291 end
292
293 # Get the Github issue comment with `id`.
294 #
295 # Returns `null` if the comment cannot be found.
296 #
297 # var api = new GithubAPI(get_github_oauth)
298 # var repo = api.load_repo("nitlang/nit")
299 # assert repo != null
300 # var comment = api.load_issue_comment(repo, 6020149)
301 # assert comment.user.login == "privat"
302 # assert comment.created_at.to_s == "2012-05-30T20:16:54Z"
303 # assert comment.issue.number == 10
304 fun load_issue_comment(repo: Repo, id: Int): nullable IssueComment do
305 var comment = new IssueComment(self, repo, id)
306 return comment.load_from_github
307 end
308
309 # Get the Github diff comment with `id`.
310 #
311 # Returns `null` if the comment cannot be found.
312 #
313 # var api = new GithubAPI(get_github_oauth)
314 # var repo = api.load_repo("nitlang/nit")
315 # assert repo != null
316 # var comment = api.load_review_comment(repo, 21010363)
317 # assert comment.path == "src/modelize/modelize_property.nit"
318 # assert comment.original_position == 26
319 # assert comment.pull.number == 945
320 fun load_review_comment(repo: Repo, id: Int): nullable ReviewComment do
321 var comment = new ReviewComment(self, repo, id)
322 return comment.load_from_github
323 end
324 end
325
326 # Something returned by the Github API.
327 #
328 # Mainly a Nit wrapper around a JSON objet.
329 abstract class GithubEntity
330
331 # Github API instance.
332 var api: GithubAPI
333
334 # FIXME constructor should be private
335
336 # Key used to access this entity from Github api base.
337 fun key: String is abstract
338
339 # JSON representation of `self`.
340 #
341 # This is the same json structure than used by Github API.
342 var json: JsonObject is noinit, protected writable
343
344 # Load `json` from Github API.
345 private fun load_from_github: nullable SELF do
346 json = api.load_from_github(key)
347 if api.was_error then return null
348 return self
349 end
350
351 redef fun to_s do return json.to_json
352
353 # Github page url.
354 fun html_url: String do return json["html_url"].as(String)
355 end
356
357 # A Github user
358 #
359 # Provides access to [Github user data](https://developer.github.com/v3/users/).
360 # Should be accessed from `GithubAPI::load_user`.
361 class User
362 super GithubEntity
363
364 redef var key is lazy do return "users/{login}"
365
366 # Github login.
367 var login: String
368
369 # Init `self` from a `json` object.
370 init from_json(api: GithubAPI, json: JsonObject) do
371 init(api, json["login"].as(String))
372 self.json = json
373 end
374
375 # Avatar image url for this user.
376 fun avatar_url: String do return json["avatar_url"].as(String)
377 end
378
379 # A Github repository.
380 #
381 # Provides access to [Github repo data](https://developer.github.com/v3/repos/).
382 # Should be accessed from `GithubAPI::load_repo`.
383 class Repo
384 super GithubEntity
385
386 redef var key is lazy do return "repos/{full_name}"
387
388 # Repo full name on Github.
389 var full_name: String
390
391 # Init `self` from a `json` object.
392 init from_json(api: GithubAPI, json: JsonObject) do
393 init(api, json["full_name"].as(String))
394 self.json = json
395 end
396
397 # Repo short name on Github.
398 fun name: String do return json["name"].as(String)
399
400 # Get the repo owner.
401 fun owner: User do
402 return new User.from_json(api, json["owner"].as(JsonObject))
403 end
404
405 # List of branches associated with their names.
406 fun branches: Map[String, Branch] do
407 api.message(1, "Get branches for {full_name}")
408 var array = api.get("repos/{full_name}/branches")
409 var res = new HashMap[String, Branch]
410 if not array isa JsonArray then return res
411 for obj in array do
412 if not obj isa JsonObject then continue
413 var name = obj["name"].as(String)
414 res[name] = new Branch.from_json(api, self, obj)
415 end
416 return res
417 end
418
419 # List of issues associated with their ids.
420 fun issues: Map[Int, Issue] do
421 api.message(1, "Get issues for {full_name}")
422 var res = new HashMap[Int, Issue]
423 var issue = last_issue
424 if issue == null then return res
425 res[issue.number] = issue
426 while issue.number > 1 do
427 issue = api.load_issue(self, issue.number - 1)
428 assert issue isa Issue
429 res[issue.number] = issue
430 end
431 return res
432 end
433
434 # Search issues in this repo form an advanced query.
435 #
436 # Example:
437 #
438 # ~~~nitish
439 # var issues = repo.search_issues("is:open label:need_review")
440 # ~~~
441 #
442 # See <https://developer.github.com/v3/search/#search-issues>.
443 fun search_issues(query: String): nullable Array[Issue] do
444 query = "search/issues?q={query} repo:{full_name}"
445 var response = api.get(query)
446 if api.was_error then return null
447 var arr = response.as(JsonObject)["items"].as(JsonArray)
448 var res = new Array[Issue]
449 for obj in arr do
450 res.add new Issue.from_json(api, self, obj.as(JsonObject))
451 end
452 return res
453 end
454
455 # Get the last published issue.
456 fun last_issue: nullable Issue do
457 var array = api.get("repos/{full_name}/issues")
458 if not array isa JsonArray then return null
459 if array.is_empty then return null
460 var obj = array.first
461 if not obj isa JsonObject then return null
462 return new Issue.from_json(api, self, obj)
463 end
464
465 # List of labels associated with their names.
466 fun labels: Map[String, Label] do
467 api.message(1, "Get labels for {full_name}")
468 var array = api.get("repos/{full_name}/labels")
469 var res = new HashMap[String, Label]
470 if not array isa JsonArray then return res
471 for obj in array do
472 if not obj isa JsonObject then continue
473 var name = obj["name"].as(String)
474 res[name] = new Label.from_json(api, self, obj)
475 end
476 return res
477 end
478
479 # List of milestones associated with their ids.
480 fun milestones: Map[Int, Milestone] do
481 api.message(1, "Get milestones for {full_name}")
482 var array = api.get("repos/{full_name}/milestones")
483 var res = new HashMap[Int, Milestone]
484 if array isa JsonArray then
485 for obj in array do
486 if not obj isa JsonObject then continue
487 var number = obj["number"].as(Int)
488 res[number] = new Milestone.from_json(api, self, obj)
489 end
490 end
491 return res
492 end
493
494 # List of pull-requests associated with their ids.
495 #
496 # Implementation notes: because PR numbers are not consecutive,
497 # PR are loaded from pages.
498 # See: https://developer.github.com/v3/pulls/#list-pull-requests
499 fun pulls: Map[Int, PullRequest] do
500 api.message(1, "Get pulls for {full_name}")
501 var res = new HashMap[Int, PullRequest]
502 var page = 1
503 var array = api.get("{key}/pulls?page={page}").as(JsonArray)
504 while not array.is_empty do
505 for obj in array do
506 if not obj isa JsonObject then continue
507 var number = obj["number"].as(Int)
508 res[number] = new PullRequest.from_json(api, self, obj)
509 end
510 page += 1
511 array = api.get("{key}/pulls?page={page}").as(JsonArray)
512 end
513 return res
514 end
515
516 # List of contributor related statistics.
517 fun contrib_stats: Array[ContributorStats] do
518 api.message(1, "Get contributor stats for {full_name}")
519 var res = new Array[ContributorStats]
520 var array = api.get("{key}/stats/contributors")
521 if array isa JsonArray then
522 for obj in array do
523 res.add new ContributorStats.from_json(api, obj.as(JsonObject))
524 end
525 end
526 return res
527 end
528
529 # Repo default branch.
530 fun default_branch: Branch do
531 var name = json["default_branch"].as(String)
532 var branch = api.load_branch(self, name)
533 assert branch isa Branch
534 return branch
535 end
536 end
537
538 # A `RepoEntity` is something contained in a `Repo`.
539 abstract class RepoEntity
540 super GithubEntity
541
542 # Repo that contains `self`.
543 var repo: Repo
544
545 # Init `self` from a `json` object.
546 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
547 self.api = api
548 self.repo = repo
549 self.json = json
550 end
551 end
552
553 # A Github branch.
554 #
555 # Should be accessed from `GithubAPI::load_branch`.
556 #
557 # See <https://developer.github.com/v3/repos/#list-branches>.
558 class Branch
559 super RepoEntity
560
561 redef var key is lazy do return "{repo.key}/branches/{name}"
562
563 # Branch name.
564 var name: String
565
566 redef init from_json(api, repo, json) do
567 self.name = json["name"].as(String)
568 super
569 end
570
571 # Get the last commit of `self`.
572 fun commit: Commit do
573 return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
574 end
575
576 # List all commits in `self`.
577 #
578 # This can be long depending on the branch size.
579 # Commit are returned in an unspecified order.
580 fun commits: Array[Commit] do
581 var res = new Array[Commit]
582 var done = new HashSet[String]
583 var todos = new Array[Commit]
584 todos.add commit
585 while not todos.is_empty do
586 var commit = todos.pop
587 if done.has(commit.sha) then continue
588 done.add commit.sha
589 res.add commit
590 for parent in commit.parents do
591 todos.add parent
592 end
593 end
594 return res
595 end
596 end
597
598 # A Github commit.
599 #
600 # Should be accessed from `GithubAPI::load_commit`.
601 #
602 # See <https://developer.github.com/v3/repos/commits/>.
603 class Commit
604 super RepoEntity
605
606 redef var key is lazy do return "{repo.key}/commits/{sha}"
607
608 # Commit SHA.
609 var sha: String
610
611 redef init from_json(api, repo, json) do
612 self.sha = json["sha"].as(String)
613 super
614 end
615
616 # Parent commits of `self`.
617 fun parents: Array[Commit] do
618 var res = new Array[Commit]
619 var parents = json.get_or_null("parents")
620 if not parents isa JsonArray then return res
621 for obj in parents do
622 if not obj isa JsonObject then continue
623 res.add(api.load_commit(repo, obj["sha"].as(String)).as(not null))
624 end
625 return res
626 end
627
628 # Author of the commit.
629 fun author: nullable User do
630 var user = json.get_or_null("author")
631 if user isa JsonObject then return new User.from_json(api, user)
632 return null
633 end
634
635 # Committer of the commit.
636 fun committer: nullable User do
637 var user = json.get_or_null("author")
638 if user isa JsonObject then return new User.from_json(api, user)
639 return null
640 end
641
642 # Authoring date as ISODate.
643 fun author_date: ISODate do
644 var commit = json["commit"].as(JsonObject)
645 var author = commit["author"].as(JsonObject)
646 return new ISODate.from_string(author["date"].as(String))
647 end
648
649 # Commit date as ISODate.
650 fun commit_date: ISODate do
651 var commit = json["commit"].as(JsonObject)
652 var author = commit["committer"].as(JsonObject)
653 return new ISODate.from_string(author["date"].as(String))
654 end
655
656 # List files staged in this commit.
657 fun files: Array[GithubFile] do
658 var res = new Array[GithubFile]
659 var files = json.get_or_null("files")
660 if not files isa JsonArray then return res
661 for obj in files do
662 res.add(new GithubFile(obj.as(JsonObject)))
663 end
664 return res
665 end
666
667 # Commit message.
668 fun message: String do return json["commit"].as(JsonObject)["message"].as(String)
669 end
670
671 # A Github issue.
672 #
673 # Should be accessed from `GithubAPI::load_issue`.
674 #
675 # See <https://developer.github.com/v3/issues/>.
676 class Issue
677 super RepoEntity
678
679 redef var key is lazy do return "{repo.key}/issues/{number}"
680
681 # Issue Github ID.
682 var number: Int
683
684 redef init from_json(api, repo, json) do
685 self.number = json["number"].as(Int)
686 super
687 end
688
689 # Issue title.
690 fun title: String do return json["title"].as(String)
691
692 # User that created this issue.
693 fun user: User do
694 return new User.from_json(api, json["user"].as(JsonObject))
695 end
696
697 # List of labels on this issue associated to their names.
698 fun labels: Map[String, Label] do
699 var res = new HashMap[String, Label]
700 var lbls = json.get_or_null("labels")
701 if not lbls isa JsonArray then return res
702 for obj in lbls do
703 if not obj isa JsonObject then continue
704 var name = obj["name"].as(String)
705 res[name] = new Label.from_json(api, repo, obj)
706 end
707 return res
708 end
709
710 # State of the issue on Github.
711 fun state: String do return json["state"].as(String)
712
713 # Is the issue locked?
714 fun locked: Bool do return json["locked"].as(Bool)
715
716 # Assigned `User` (if any).
717 fun assignee: nullable User do
718 var assignee = json.get_or_null("assignee")
719 if assignee isa JsonObject then return new User.from_json(api, assignee)
720 return null
721 end
722
723 # `Milestone` (if any).
724 fun milestone: nullable Milestone do
725 var milestone = json.get_or_null("milestone")
726 if milestone isa JsonObject then return new Milestone.from_json(api, repo, milestone)
727 return null
728 end
729
730 # List of comments made on this issue.
731 fun comments: Array[IssueComment] do
732 var res = new Array[IssueComment]
733 var count = comments_count
734 var page = 1
735 var array = api.get("{key}/comments?page={page}")
736 if not array isa JsonArray then
737 return res
738 end
739 while not array.is_empty and res.length < count do
740 for obj in array do
741 if not obj isa JsonObject then continue
742 var id = obj["id"].as(Int)
743 res.add(api.load_issue_comment(repo, id).as(not null))
744 end
745 page += 1
746 array = api.get("{key}/comments?page={page}").as(JsonArray)
747 end
748 return res
749 end
750
751 # Number of comments on this issue.
752 fun comments_count: Int do return json["comments"].as(Int)
753
754 # Creation time in ISODate format.
755 fun created_at: ISODate do
756 return new ISODate.from_string(json["created_at"].as(String))
757 end
758
759 # Last update time in ISODate format (if any).
760 fun updated_at: nullable ISODate do
761 var res = json.get_or_null("updated_at")
762 if res isa String then return new ISODate.from_string(res)
763 return null
764 end
765
766 # Close time in ISODate format (if any).
767 fun closed_at: nullable ISODate do
768 var res = json.get_or_null("closed_at")
769 if res isa String then return new ISODate.from_string(res)
770 return null
771 end
772
773 # TODO link to pull request
774
775 # Full description of the issue.
776 fun body: String do return json["body"].as(String)
777
778 # List of events on this issue.
779 fun events: Array[IssueEvent] do
780 var res = new Array[IssueEvent]
781 var page = 1
782 var array = api.get("{key}/events?page={page}")
783 if not array isa JsonArray then return res
784 while not array.is_empty do
785 for obj in array do
786 if not obj isa JsonObject then continue
787 res.add new IssueEvent.from_json(api, repo, obj)
788 end
789 page += 1
790 array = api.get("{key}/events?page={page}").as(JsonArray)
791 end
792 return res
793 end
794
795 # User that closed this issue (if any).
796 fun closed_by: nullable User do
797 var closer = json.get_or_null("closed_by")
798 if closer isa JsonObject then return new User.from_json(api, closer)
799 return null
800 end
801 end
802
803 # A Github pull request.
804 #
805 # Should be accessed from `GithubAPI::load_pull`.
806 #
807 # PullRequest are basically Issues with more data.
808 # See <https://developer.github.com/v3/pulls/>.
809 class PullRequest
810 super Issue
811
812 redef var key is lazy do return "{repo.key}/pulls/{number}"
813
814 # Merge time in ISODate format (if any).
815 fun merged_at: nullable ISODate do
816 var res = json.get_or_null("merged_at")
817 if res isa String then return new ISODate.from_string(res)
818 return null
819 end
820
821 # Merge commit SHA.
822 fun merge_commit_sha: String do return json["merge_commit_sha"].as(String)
823
824 # Count of comments made on the pull request diff.
825 fun review_comments: Int do return json["review_comments"].as(Int)
826
827 # Pull request head (can be a commit SHA or a branch name).
828 fun head: PullRef do
829 var json = json["head"].as(JsonObject)
830 return new PullRef(api, json)
831 end
832
833 # Pull request base (can be a commit SHA or a branch name).
834 fun base: PullRef do
835 var json = json["base"].as(JsonObject)
836 return new PullRef(api, json)
837 end
838
839 # Is this pull request merged?
840 fun merged: Bool do return json["merged"].as(Bool)
841
842 # Is this pull request mergeable?
843 fun mergeable: Bool do return json["mergeable"].as(Bool)
844
845 # Mergeable state of this pull request.
846 #
847 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
848 fun mergeable_state: Int do return json["mergeable_state"].as(Int)
849
850 # User that merged this pull request (if any).
851 fun merged_by: nullable User do
852 var merger = json.get_or_null("merged_by")
853 if merger isa JsonObject then return new User.from_json(api, merger)
854 return null
855 end
856
857 # Count of commits in this pull request.
858 fun commits: Int do return json["commits"].as(Int)
859
860 # Added line count.
861 fun additions: Int do return json["additions"].as(Int)
862
863 # Deleted line count.
864 fun deletions: Int do return json["deletions"].as(Int)
865
866 # Changed files count.
867 fun changed_files: Int do return json["changed_files"].as(Int)
868 end
869
870 # A pull request reference (used for head and base).
871 class PullRef
872
873 # Api instance that maintains self.
874 var api: GithubAPI
875
876 # JSON representation.
877 var json: JsonObject
878
879 # Label pointed by `self`.
880 fun labl: String do return json["label"].as(String)
881
882 # Reference pointed by `self`.
883 fun ref: String do return json["ref"].as(String)
884
885 # Commit SHA pointed by `self`.
886 fun sha: String do return json["sha"].as(String)
887
888 # User pointed by `self`.
889 fun user: User do
890 return new User.from_json(api, json["user"].as(JsonObject))
891 end
892
893 # Repo pointed by `self`.
894 fun repo: Repo do
895 return new Repo.from_json(api, json["repo"].as(JsonObject))
896 end
897 end
898
899 # A Github label.
900 #
901 # Should be accessed from `GithubAPI::load_label`.
902 #
903 # See <https://developer.github.com/v3/issues/labels/>.
904 class Label
905 super RepoEntity
906
907 redef var key is lazy do return "{repo.key}/labels/{name}"
908
909 # Label name.
910 var name: String
911
912 redef init from_json(api, repo, json) do
913 self.name = json["name"].as(String)
914 super
915 end
916
917 # Label color code.
918 fun color: String do return json["color"].as(String)
919 end
920
921 # A Github milestone.
922 #
923 # Should be accessed from `GithubAPI::load_milestone`.
924 #
925 # See <https://developer.github.com/v3/issues/milestones/>.
926 class Milestone
927 super RepoEntity
928
929 redef var key is lazy do return "{repo.key}/milestones/{number}"
930
931 # The milestone id on Github.
932 var number: Int
933
934 redef init from_json(api, repo, json) do
935 super
936 self.number = json["number"].as(Int)
937 end
938
939 # Milestone title.
940 fun title: String do return json["title"].as(String)
941
942 # Milestone long description.
943 fun description: String do return json["description"].as(String)
944
945 # Count of opened issues linked to this milestone.
946 fun open_issues: Int do return json["open_issues"].as(Int)
947
948 # Count of closed issues linked to this milestone.
949 fun closed_issues: Int do return json["closed_issues"].as(Int)
950
951 # Milestone state.
952 fun state: String do return json["state"].as(String)
953
954 # Creation time in ISODate format.
955 fun created_at: ISODate do
956 return new ISODate.from_string(json["created_at"].as(String))
957 end
958
959 # User that created this milestone.
960 fun creator: User do
961 return new User.from_json(api, json["creator"].as(JsonObject))
962 end
963
964 # Due time in ISODate format (if any).
965 fun due_on: nullable ISODate do
966 var res = json.get_or_null("updated_at")
967 if res isa String then return new ISODate.from_string(res)
968 return null
969 end
970
971 # Update time in ISODate format (if any).
972 fun updated_at: nullable ISODate do
973 var res = json.get_or_null("updated_at")
974 if res isa String then return new ISODate.from_string(res)
975 return null
976 end
977
978 # Close time in ISODate format (if any).
979 fun closed_at: nullable ISODate do
980 var res = json.get_or_null("closed_at")
981 if res isa String then return new ISODate.from_string(res)
982 return null
983 end
984 end
985
986 # A Github comment
987 #
988 # There is two kinds of comments:
989 #
990 # * `CommitComment` are made on a commit page.
991 # * `IssueComment` are made on an issue or pull request page.
992 # * `ReviewComment` are made on the diff associated to a pull request.
993 abstract class Comment
994 super RepoEntity
995
996 # Identifier of this comment.
997 var id: Int
998
999 redef init from_json(api, repo, json) do
1000 self.id = json["id"].as(Int)
1001 super
1002 end
1003
1004 # User that made this comment.
1005 fun user: User do
1006 return new User.from_json(api, json["user"].as(JsonObject))
1007 end
1008
1009 # Creation time in ISODate format.
1010 fun created_at: ISODate do
1011 return new ISODate.from_string(json["created_at"].as(String))
1012 end
1013
1014 # Last update time in ISODate format (if any).
1015 fun updated_at: nullable ISODate do
1016 var res = json.get_or_null("updated_at")
1017 if res isa String then return new ISODate.from_string(res)
1018 return null
1019 end
1020
1021 # Comment body text.
1022 fun body: String do return json["body"].as(String)
1023
1024 # Does the comment contain an acknowledgement (+1)
1025 fun is_ack: Bool
1026 do
1027 return body.has("\\+1\\b".to_re) or body.has(":+1:") or body.has(":shipit:")
1028 end
1029 end
1030
1031 # A comment made on a commit.
1032 class CommitComment
1033 super Comment
1034
1035 redef var key is lazy do return "{repo.key}/comments/{id}"
1036
1037 # Commented commit.
1038 fun commit: Commit do
1039 return api.load_commit(repo, json["commit_id"].as(String)).as(not null)
1040 end
1041
1042 # Position of the comment on the line.
1043 fun position: nullable String do
1044 var res = json.get_or_null("position")
1045 if res isa String then return res
1046 return null
1047 end
1048
1049 # Line of the comment.
1050 fun line: nullable String do
1051 var res = json.get_or_null("line")
1052 if res isa String then return res
1053 return null
1054 end
1055
1056 # Path of the commented file.
1057 fun path: String do return json["path"].as(String)
1058 end
1059
1060 # Comments made on Github issue and pull request pages.
1061 #
1062 # Should be accessed from `GithubAPI::load_issue_comment`.
1063 #
1064 # See <https://developer.github.com/v3/issues/comments/>.
1065 class IssueComment
1066 super Comment
1067
1068 redef var key is lazy do return "{repo.key}/issues/comments/{id}"
1069
1070 # Issue that contains `self`.
1071 fun issue: Issue do
1072 var number = issue_url.split("/").last.to_i
1073 return api.load_issue(repo, number).as(not null)
1074 end
1075
1076 # Link to the issue document on API.
1077 fun issue_url: String do return json["issue_url"].as(String)
1078 end
1079
1080 # Comments made on Github pull request diffs.
1081 #
1082 # Should be accessed from `GithubAPI::load_diff_comment`.
1083 #
1084 # See <https://developer.github.com/v3/pulls/comments/>.
1085 class ReviewComment
1086 super Comment
1087
1088 redef var key is lazy do return "{repo.key}/pulls/comments/{id}"
1089
1090 # Pull request that contains `self`.
1091 fun pull: PullRequest do
1092 var number = pull_request_url.split("/").last.to_i
1093 return api.load_pull(repo, number).as(not null)
1094 end
1095
1096 # Link to the pull request on API.
1097 fun pull_request_url: String do return json["pull_request_url"].as(String)
1098
1099 # Diff hunk.
1100 fun diff_hunk: String do return json["diff_hunk"].as(String)
1101
1102 # Path of commented file.
1103 fun path: String do return json["path"].as(String)
1104
1105 # Position of the comment on the file.
1106 fun position: Int do return json["position"].as(Int)
1107
1108 # Original position in the diff.
1109 fun original_position: Int do return json["original_position"].as(Int)
1110
1111 # Commit referenced by this comment.
1112 fun commit_id: String do return json["commit_id"].as(String)
1113
1114 # Original commit id.
1115 fun original_commit_id: String do return json["original_commit_id"].as(String)
1116 end
1117
1118 # An event that occurs on a Github `Issue`.
1119 #
1120 # Should be accessed from `GithubAPI::load_issue_event`.
1121 #
1122 # See <https://developer.github.com/v3/issues/events/>.
1123 class IssueEvent
1124 super RepoEntity
1125
1126 redef var key is lazy do return "{repo.key}/issues/events/{id}"
1127
1128 # Event id on Github.
1129 var id: Int
1130
1131 redef init from_json(api, repo, json) do
1132 self.id = json["id"].as(Int)
1133 super
1134 end
1135
1136 # Issue that contains `self`.
1137 fun issue: Issue do
1138 return new Issue.from_json(api, repo, json["issue"].as(JsonObject))
1139 end
1140
1141 # User that initiated the event.
1142 fun actor: User do
1143 return new User.from_json(api, json["actor"].as(JsonObject))
1144 end
1145
1146 # Creation time in ISODate format.
1147 fun created_at: ISODate do
1148 return new ISODate.from_string(json["created_at"].as(String))
1149 end
1150
1151 # Event descriptor.
1152 fun event: String do return json["event"].as(String)
1153
1154 # Commit linked to this event (if any).
1155 fun commit_id: nullable String do
1156 var res = json.get_or_null("commit_id")
1157 if res isa String then return res
1158 return null
1159 end
1160
1161 # Label linked to this event (if any).
1162 fun labl: nullable Label do
1163 var res = json.get_or_null("label")
1164 if res isa JsonObject then return new Label.from_json(api, repo, res)
1165 return null
1166 end
1167
1168 # User linked to this event (if any).
1169 fun assignee: nullable User do
1170 var res = json.get_or_null("assignee")
1171 if res isa JsonObject then return new User.from_json(api, res)
1172 return null
1173 end
1174
1175 # Milestone linked to this event (if any).
1176 fun milestone: nullable Milestone do
1177 var res = json.get_or_null("milestone")
1178 if res isa JsonObject then return new Milestone.from_json(api, repo, res)
1179 return null
1180 end
1181
1182 # Rename linked to this event (if any).
1183 fun rename: nullable RenameAction do
1184 var res = json.get_or_null("rename")
1185 if res isa JsonObject then return new RenameAction(res)
1186 return null
1187 end
1188 end
1189
1190 # A rename action maintains the name before and after a renaming action.
1191 class RenameAction
1192
1193 # JSON content.
1194 var json: JsonObject
1195
1196 # Name before renaming.
1197 fun from: String do return json["from"].as(String)
1198
1199 # Name after renaming.
1200 fun to: String do return json["to"].as(String)
1201 end
1202
1203 # Contributors list with additions, deletions, and commit counts.
1204 #
1205 # Should be accessed from `Repo::contrib_stats`.
1206 #
1207 # See <https://developer.github.com/v3/repos/statistics/>.
1208 class ContributorStats
1209 super Comparable
1210
1211 redef type OTHER: ContributorStats
1212
1213 # Github API client.
1214 var api: GithubAPI
1215
1216 # Json content.
1217 var json: JsonObject
1218
1219 # Init `self` from a `json` object.
1220 init from_json(api: GithubAPI, json: JsonObject) do
1221 self.api = api
1222 self.json = json
1223 end
1224
1225 # User these statistics are about.
1226 fun author: User do
1227 return new User.from_json(api, json["author"].as(JsonObject))
1228 end
1229
1230 # Total number of commit.
1231 fun total: Int do return json["total"].as(Int)
1232
1233 # Are of weeks of activity with detailed statistics.
1234 fun weeks: JsonArray do return json["weeks"].as(JsonArray)
1235
1236 # ContributorStats can be compared on the total amount of commits.
1237 redef fun <(o) do return total < o.total
1238 end
1239
1240 # A Github file representation.
1241 #
1242 # Mostly a wrapper around a json object.
1243 class GithubFile
1244
1245 # Json content.
1246 var json: JsonObject
1247
1248 # File name.
1249 fun filename: String do return json["filename"].as(String)
1250 end