1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Nit object oriented interface to Github api.
17 # This modules reifies Github API elements as Nit classes.
19 # For most use-cases you need to use the `GithubAPI` client.
24 # Interface to Github REST API.
26 # Used by all `GithubEntity` to perform requests.
31 # # Get Github authentification token.
32 # var token = get_github_oauth
33 # assert not token.is_empty
36 # var api = new GithubAPI(token)
39 # The API client allows to get Github API entities:
42 # var repo = api.load_repo("privat/nit")
43 # assert repo isa Repo
44 # assert repo.name == "nit"
46 # var user = api.load_user("Morriar")
47 # assert user isa User
48 # assert user.login == "Morriar"
52 # Github API OAuth token.
54 # This token is used to authenticate the application on Github API.
55 # Be aware that there is [rate limits](https://developer.github.com/v3/rate_limit/)
56 # associated to the key.
59 # Github API base url.
61 # Default is `https://api.github.com` and should not be changed.
62 var api_url
= "https://api.github.com"
64 # User agent used for HTTP requests.
66 # Default is `nit_github_api`.
68 # See <https://developer.github.com/v3/#user-agent-required>
69 var user_agent
= "nit_github_api"
73 # Internal Curl instance used to perform API calls.
74 private var ghcurl
: GithubCurl is noinit
78 # * `0`: only errors (default)
80 var verbose_lvl
= 0 is public
writable
83 ghcurl
= new GithubCurl(auth
, user_agent
)
86 # Execute a GET request on Github API.
88 # This method returns raw json data.
89 # See other `load_*` methods to use more expressive types.
91 # var api = new GithubAPI(get_github_oauth)
92 # var obj = api.get("repos/privat/nit")
93 # assert obj isa JsonObject
94 # assert obj["name"] == "nit"
96 # Returns `null` in case of `error`.
98 # obj = api.get("foo/bar/baz")
100 # assert api.was_error
101 # var err = api.last_error
102 # assert err isa GithubError
103 # assert err.name == "GithubAPIError"
104 # assert err.message == "Not Found"
105 fun get
(path
: String): nullable Jsonable do
106 path
= sanitize_uri
(path
)
107 var res
= ghcurl
.get_and_parse
("{api_url}/{path}")
108 if res
isa Error then
117 # Display a message depending on `verbose_lvl`.
118 fun message
(lvl
: Int, message
: String) do
119 if lvl
<= verbose_lvl
then print message
122 # Escape `uri` in an acceptable format for Github.
123 private fun sanitize_uri
(uri
: String): String do
124 # TODO better URI escape.
125 return uri
.replace
(" ", "%20")
128 # Last error occured during Github API communications.
129 var last_error
: nullable Error = null is public
writable
131 # Does the last request provoqued an error?
132 var was_error
= false is protected writable
134 # Load the json object from Github.
135 # See `GithubEntity::load_from_github`.
136 private fun load_from_github
(key
: String): JsonObject do
137 message
(1, "Get {key} (github)")
139 if was_error
then return new JsonObject
140 return res
.as(JsonObject)
143 # Get the Github user with `login`.
145 # Returns `null` if the user cannot be found.
147 # var api = new GithubAPI(get_github_oauth)
148 # var user = api.load_user("Morriar")
149 # assert user.login == "Morriar"
150 fun load_user
(login
: String): nullable User do
151 var user
= new User(self, login
)
152 user
.load_from_github
153 if was_error
then return null
157 # Get the Github repo with `full_name`.
159 # Returns `null` if the repo cannot be found.
161 # var api = new GithubAPI(get_github_oauth)
162 # var repo = api.load_repo("privat/nit")
163 # assert repo.name == "nit"
164 # assert repo.owner.login == "privat"
165 # assert repo.default_branch.name == "master"
166 fun load_repo
(full_name
: String): nullable Repo do
167 var repo
= new Repo(self, full_name
)
168 repo
.load_from_github
169 if was_error
then return null
173 # Get the Github branch with `name`.
175 # Returns `null` if the branch cannot be found.
177 # var api = new GithubAPI(get_github_oauth)
178 # var repo = api.load_repo("privat/nit")
179 # assert repo isa Repo
180 # var branch = api.load_branch(repo, "master")
181 # assert branch.name == "master"
182 # assert branch.commit isa Commit
183 fun load_branch
(repo
: Repo, name
: String): nullable Branch do
184 var branch
= new Branch(self, repo
, name
)
185 branch
.load_from_github
186 if was_error
then return null
190 # Get the Github commit with `sha`.
192 # Returns `null` if the commit cannot be found.
194 # var api = new GithubAPI(get_github_oauth)
195 # var repo = api.load_repo("privat/nit")
196 # assert repo isa Repo
197 # var commit = api.load_commit(repo, "64ce1f")
198 # assert commit isa Commit
199 fun load_commit
(repo
: Repo, sha
: String): nullable Commit do
200 var commit
= new Commit(self, repo
, sha
)
201 commit
.load_from_github
202 if was_error
then return null
206 # Get the Github issue #`number`.
208 # Returns `null` if the issue cannot be found.
210 # var api = new GithubAPI(get_github_oauth)
211 # var repo = api.load_repo("privat/nit")
212 # assert repo != null
213 # var issue = api.load_issue(repo, 1)
214 # assert issue.title == "Doc"
215 fun load_issue
(repo
: Repo, number
: Int): nullable Issue do
216 var issue
= new Issue(self, repo
, number
)
217 issue
.load_from_github
218 if was_error
then return null
222 # Get the Github label with `name`.
224 # Returns `null` if the label cannot be found.
226 # var api = new GithubAPI(get_github_oauth)
227 # var repo = api.load_repo("privat/nit")
228 # assert repo isa Repo
229 # var labl = api.load_label(repo, "ok_will_merge")
230 # assert labl != null
231 fun load_label
(repo
: Repo, name
: String): nullable Label do
232 var labl
= new Label(self, repo
, name
)
233 labl
.load_from_github
234 if was_error
then return null
238 # Get the Github milestone with `id`.
240 # Returns `null` if the milestone cannot be found.
242 # var api = new GithubAPI(get_github_oauth)
243 # var repo = api.load_repo("privat/nit")
244 # assert repo isa Repo
245 # var stone = api.load_milestone(repo, 4)
246 # assert stone.title == "v1.0prealpha"
247 fun load_milestone
(repo
: Repo, id
: Int): nullable Milestone do
248 var milestone
= new Milestone(self, repo
, id
)
249 milestone
.load_from_github
250 if was_error
then return null
255 # Something returned by the Github API.
257 # Mainly a Nit wrapper around a JSON objet.
258 abstract class GithubEntity
260 # Github API instance.
263 # FIXME constructor should be private
265 # Key used to access this entity from Github api base.
266 fun key
: String is abstract
268 # JSON representation of `self`.
270 # This is the same json structure than used by Github API.
271 var json
: JsonObject is noinit
, protected writable
273 # Load `json` from Github API.
274 private fun load_from_github
do
275 json
= api
.load_from_github
(key
)
278 redef fun to_s
do return json
.to_json
283 # Should be accessed from `GithubAPI::load_user`.
285 # See <https://developer.github.com/v3/users/>.
289 redef var key
is lazy
do return "users/{login}"
294 # Init `self` from a `json` object.
295 init from_json
(api
: GithubAPI, json
: JsonObject) do
296 init(api
, json
["login"].to_s
)
300 # Github User page url.
301 fun html_url
: String do return json
["html_url"].to_s
303 # Avatar image url for this user.
304 fun avatar_url
: String do return json
["avatar_url"].to_s
307 # A Github repository.
309 # Should be accessed from `GithubAPI::load_repo`.
311 # See <https://developer.github.com/v3/repos/>.
315 redef var key
is lazy
do return "repos/{full_name}"
317 # Repo full name on Github.
318 var full_name
: String
320 # Init `self` from a `json` object.
321 init from_json
(api
: GithubAPI, json
: JsonObject) do
322 init(api
, json
["full_name"].to_s
)
326 # Repo short name on Github.
327 fun name
: String do return json
["name"].to_s
329 # Github User page url.
330 fun html_url
: String do return json
["html_url"].to_s
332 # Get the repo owner.
334 return new User.from_json
(api
, json
["owner"].as(JsonObject))
337 # List of branches associated with their names.
338 fun branches
: Map[String, Branch] do
339 api
.message
(1, "Get branches for {full_name}")
340 var array
= api
.get
("repos/{full_name}/branches")
341 var res
= new HashMap[String, Branch]
342 if not array
isa JsonArray then return res
344 if not obj
isa JsonObject then continue
345 var name
= obj
["name"].to_s
346 res
[name
] = new Branch.from_json
(api
, self, obj
)
351 # List of issues associated with their ids.
352 fun issues
: Map[Int, Issue] do
353 api
.message
(1, "Get issues for {full_name}")
354 var res
= new HashMap[Int, Issue]
355 var issue
= last_issue
356 if issue
== null then return res
357 res
[issue
.number
] = issue
358 while issue
.number
> 1 do
359 issue
= api
.load_issue
(self, issue
.number
- 1)
360 assert issue
isa Issue
361 res
[issue
.number
] = issue
366 # Get the last published issue.
367 fun last_issue
: nullable Issue do
368 var array
= api
.get
("repos/{full_name}/issues")
369 if not array
isa JsonArray then return null
370 if array
.is_empty
then return null
371 var obj
= array
.first
372 if not obj
isa JsonObject then return null
373 return new Issue.from_json
(api
, self, obj
)
376 # List of labels associated with their names.
377 fun labels
: Map[String, Label] do
378 api
.message
(1, "Get labels for {full_name}")
379 var array
= api
.get
("repos/{full_name}/labels")
380 var res
= new HashMap[String, Label]
381 if not array
isa JsonArray then return res
383 if not obj
isa JsonObject then continue
384 var name
= obj
["name"].to_s
385 res
[name
] = new Label.from_json
(api
, self, obj
)
390 # List of milestones associated with their ids.
391 fun milestones
: Map[Int, Milestone] do
392 api
.message
(1, "Get milestones for {full_name}")
393 var array
= api
.get
("repos/{full_name}/milestones")
394 var res
= new HashMap[Int, Milestone]
395 if array
isa JsonArray then
397 if not obj
isa JsonObject then continue
398 var number
= obj
["number"].as(Int)
399 res
[number
] = new Milestone.from_json
(api
, self, obj
)
405 # Repo default branch.
406 fun default_branch
: Branch do
407 var name
= json
["default_branch"].to_s
408 var branch
= api
.load_branch
(self, name
)
409 assert branch
isa Branch
414 # A `RepoEntity` is something contained in a `Repo`.
415 abstract class RepoEntity
418 # Repo that contains `self`.
421 # Init `self` from a `json` object.
422 init from_json
(api
: GithubAPI, repo
: Repo, json
: JsonObject) do
431 # Should be accessed from `GithubAPI::load_branch`.
433 # See <https://developer.github.com/v3/repos/#list-branches>.
437 redef var key
is lazy
do return "{repo.key}/branches/{name}"
442 redef init from_json
(api
, repo
, json
) do
443 self.name
= json
["name"].to_s
447 # Get the last commit of `self`.
448 fun commit
: Commit do
449 return new Commit.from_json
(api
, repo
, json
["commit"].as(JsonObject))
452 # List all commits in `self`.
454 # This can be long depending on the branch size.
455 # Commit are returned in an unspecified order.
456 fun commits
: Array[Commit] do
457 var res
= new Array[Commit]
458 var done
= new HashSet[String]
459 var todos
= new Array[Commit]
461 while not todos
.is_empty
do
462 var commit
= todos
.pop
463 if done
.has
(commit
.sha
) then continue
466 for parent
in commit
.parents
do
476 # Should be accessed from `GithubAPI::load_commit`.
478 # See <https://developer.github.com/v3/commits/>.
482 redef var key
is lazy
do return "{repo.key}/commits/{sha}"
487 redef init from_json
(api
, repo
, json
) do
488 self.sha
= json
["sha"].to_s
492 # Parent commits of `self`.
493 fun parents
: Array[Commit] do
494 var res
= new Array[Commit]
495 var parents
= json
["parents"]
496 if not parents
isa JsonArray then return res
497 for obj
in parents
do
498 if not obj
isa JsonObject then continue
499 res
.add
(api
.load_commit
(repo
, obj
["sha"].to_s
).as(not null))
504 # Author of the commit.
505 fun author
: nullable User do
506 if not json
.has_key
("author") then return null
507 var user
= json
["author"]
508 if not user
isa JsonObject then return null
509 return new User.from_json
(api
, user
)
512 # Committer of the commit.
513 fun committer
: nullable User do
514 if not json
.has_key
("committer") then return null
515 var user
= json
["author"]
516 if not user
isa JsonObject then return null
517 return new User.from_json
(api
, user
)
520 # Authoring date as ISODate.
521 fun author_date
: ISODate do
522 var commit
= json
["commit"].as(JsonObject)
523 var author
= commit
["author"].as(JsonObject)
524 return new ISODate.from_string
(author
["date"].to_s
)
527 # Commit date as ISODate.
528 fun commit_date
: ISODate do
529 var commit
= json
["commit"].as(JsonObject)
530 var author
= commit
["committer"].as(JsonObject)
531 return new ISODate.from_string
(author
["date"].to_s
)
535 fun message
: String do return json
["commit"].as(JsonObject)["message"].to_s
540 # Should be accessed from `GithubAPI::load_issue`.
542 # See <https://developer.github.com/v3/issues/>.
546 redef var key
is lazy
do return "{repo.key}/issues/{number}"
551 redef init from_json
(api
, repo
, json
) do
552 self.number
= json
["number"].as(Int)
557 fun title
: String do return json
["title"].to_s
559 # User that created this issue.
561 return new User.from_json
(api
, json
["user"].as(JsonObject))
564 # List of labels on this issue associated to their names.
565 fun labels
: Map[String, Label] do
566 var res
= new HashMap[String, Label]
567 for obj
in json
["labels"].as(JsonArray) do
568 if not obj
isa JsonObject then continue
569 var name
= obj
["name"].to_s
570 res
[name
] = new Label.from_json
(api
, repo
, obj
)
575 # State of the issue on Github.
576 fun state
: String do return json
["state"].to_s
578 # Is the issue locked?
579 fun locked
: Bool do return json
["locked"].as(Bool)
581 # Assigned `User` (if any).
582 fun assignee
: nullable User do
583 var assignee
= json
["assignee"]
584 if not assignee
isa JsonObject then return null
585 return new User.from_json
(api
, assignee
)
588 # `Milestone` (if any).
589 fun milestone
: nullable Milestone do
590 var milestone
= json
["milestone"]
591 if not milestone
isa JsonObject then return null
592 return new Milestone.from_json
(api
, repo
, milestone
)
595 # Number of comments on this issue.
596 fun comments_count
: Int do return json
["comments"].to_s
.to_i
598 # Creation time in ISODate format.
599 fun created_at
: ISODate do
600 return new ISODate.from_string
(json
["created_at"].to_s
)
603 # Last update time in ISODate format (if any).
604 fun updated_at
: nullable ISODate do
605 var res
= json
["updated_at"]
606 if res
== null then return null
607 return new ISODate.from_string
(res
.to_s
)
610 # Close time in ISODate format (if any).
611 fun closed_at
: nullable ISODate do
612 var res
= json
["closed_at"]
613 if res
== null then return null
614 return new ISODate.from_string
(res
.to_s
)
617 # Full description of the issue.
618 fun body
: String do return json
["body"].to_s
620 # User that closed this issue (if any).
621 fun closed_by
: nullable User do
622 var closer
= json
["closed_by"]
623 if not closer
isa JsonObject then return null
624 return new User.from_json
(api
, closer
)
629 # Should be accessed from `GithubAPI::load_label`.
631 # See <https://developer.github.com/v3/issues/labels/>.
635 redef var key
is lazy
do return "{repo.key}/labels/{name}"
640 redef init from_json
(api
, repo
, json
) do
641 self.name
= json
["name"].to_s
646 fun color
: String do return json
["color"].to_s
649 # A Github milestone.
651 # Should be accessed from `GithubAPI::load_milestone`.
653 # See <https://developer.github.com/v3/issues/milestones/>.
657 redef var key
is lazy
do return "{repo.key}/milestones/{number}"
659 # The milestone id on Github.
662 redef init from_json
(api
, repo
, json
) do
664 self.number
= json
["number"].as(Int)
668 fun title
: String do return json
["title"].to_s
670 # Milestone long description.
671 fun description
: String do return json
["description"].to_s
673 # Count of opened issues linked to this milestone.
674 fun open_issues
: Int do return json
["open_issues"].to_s
.to_i
676 # Count of closed issues linked to this milestone.
677 fun closed_issues
: Int do return json
["closed_issues"].to_s
.to_i
680 fun state
: String do return json
["state"].to_s
682 # Creation time in ISODate format.
683 fun created_at
: ISODate do
684 return new ISODate.from_string
(json
["created_at"].to_s
)
687 # User that created this milestone.
689 return new User.from_json
(api
, json
["creator"].as(JsonObject))
692 # Due time in ISODate format (if any).
693 fun due_on
: nullable ISODate do
694 var res
= json
["updated_at"]
695 if res
== null then return null
696 return new ISODate.from_string
(res
.to_s
)
699 # Update time in ISODate format (if any).
700 fun updated_at
: nullable ISODate do
701 var res
= json
["updated_at"]
702 if res
== null then return null
703 return new ISODate.from_string
(res
.to_s
)
706 # Close time in ISODate format (if any).
707 fun closed_at
: nullable ISODate do
708 var res
= json
["closed_at"]
709 if res
== null then return null
710 return new ISODate.from_string
(res
.to_s
)