lib/github: handles pull requests
[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.
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 # Interface to Github REST API.
25 #
26 # Used by all `GithubEntity` to perform requests.
27 #
28 # Usage:
29 #
30 # ~~~
31 # # Get Github authentification token.
32 # var token = get_github_oauth
33 # assert not token.is_empty
34 #
35 # # Init the client.
36 # var api = new GithubAPI(token)
37 # ~~~
38 #
39 # The API client allows to get Github API entities:
40 #
41 # ~~~
42 # var repo = api.load_repo("privat/nit")
43 # assert repo isa Repo
44 # assert repo.name == "nit"
45 #
46 # var user = api.load_user("Morriar")
47 # assert user isa User
48 # assert user.login == "Morriar"
49 # ~~~
50 class GithubAPI
51
52 # Github API OAuth token.
53 #
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.
57 var auth: String
58
59 # Github API base url.
60 #
61 # Default is `https://api.github.com` and should not be changed.
62 var api_url = "https://api.github.com"
63
64 # User agent used for HTTP requests.
65 #
66 # Default is `nit_github_api`.
67 #
68 # See <https://developer.github.com/v3/#user-agent-required>
69 var user_agent = "nit_github_api"
70
71 # Curl instance.
72 #
73 # Internal Curl instance used to perform API calls.
74 private var ghcurl: GithubCurl is noinit
75
76 # Verbosity level.
77 #
78 # * `0`: only errors (default)
79 # * `1`: verbose
80 var verbose_lvl = 0 is public writable
81
82 init do
83 ghcurl = new GithubCurl(auth, user_agent)
84 end
85
86 # Execute a GET request on Github API.
87 #
88 # This method returns raw json data.
89 # See other `load_*` methods to use more expressive types.
90 #
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"
95 #
96 # Returns `null` in case of `error`.
97 #
98 # obj = api.get("foo/bar/baz")
99 # assert obj == null
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
109 last_error = res
110 was_error = true
111 return null
112 end
113 was_error = false
114 return res
115 end
116
117 # Display a message depending on `verbose_lvl`.
118 fun message(lvl: Int, message: String) do
119 if lvl <= verbose_lvl then print message
120 end
121
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")
126 end
127
128 # Last error occured during Github API communications.
129 var last_error: nullable Error = null is public writable
130
131 # Does the last request provoqued an error?
132 var was_error = false is protected writable
133
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)")
138 var res = get(key)
139 if was_error then return new JsonObject
140 return res.as(JsonObject)
141 end
142
143 # Get the Github user with `login`.
144 #
145 # Returns `null` if the user cannot be found.
146 #
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
154 return user
155 end
156
157 # Get the Github repo with `full_name`.
158 #
159 # Returns `null` if the repo cannot be found.
160 #
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
170 return repo
171 end
172
173 # Get the Github branch with `name`.
174 #
175 # Returns `null` if the branch cannot be found.
176 #
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
187 return branch
188 end
189
190 # Get the Github commit with `sha`.
191 #
192 # Returns `null` if the commit cannot be found.
193 #
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
203 return commit
204 end
205
206 # Get the Github issue #`number`.
207 #
208 # Returns `null` if the issue cannot be found.
209 #
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
219 return issue
220 end
221
222 # Get the Github pull request #`number`.
223 #
224 # Returns `null` if the pull request cannot be found.
225 #
226 # var api = new GithubAPI(get_github_oauth)
227 # var repo = api.load_repo("privat/nit")
228 # assert repo != null
229 # var pull = api.load_pull(repo, 1)
230 # assert pull.title == "Doc"
231 # assert pull.user.login == "Morriar"
232 fun load_pull(repo: Repo, number: Int): nullable PullRequest do
233 var pull = new PullRequest(self, repo, number)
234 pull.load_from_github
235 if was_error then return null
236 return pull
237 end
238
239 # Get the Github label with `name`.
240 #
241 # Returns `null` if the label cannot be found.
242 #
243 # var api = new GithubAPI(get_github_oauth)
244 # var repo = api.load_repo("privat/nit")
245 # assert repo isa Repo
246 # var labl = api.load_label(repo, "ok_will_merge")
247 # assert labl != null
248 fun load_label(repo: Repo, name: String): nullable Label do
249 var labl = new Label(self, repo, name)
250 labl.load_from_github
251 if was_error then return null
252 return labl
253 end
254
255 # Get the Github milestone with `id`.
256 #
257 # Returns `null` if the milestone cannot be found.
258 #
259 # var api = new GithubAPI(get_github_oauth)
260 # var repo = api.load_repo("privat/nit")
261 # assert repo isa Repo
262 # var stone = api.load_milestone(repo, 4)
263 # assert stone.title == "v1.0prealpha"
264 fun load_milestone(repo: Repo, id: Int): nullable Milestone do
265 var milestone = new Milestone(self, repo, id)
266 milestone.load_from_github
267 if was_error then return null
268 return milestone
269 end
270 end
271
272 # Something returned by the Github API.
273 #
274 # Mainly a Nit wrapper around a JSON objet.
275 abstract class GithubEntity
276
277 # Github API instance.
278 var api: GithubAPI
279
280 # FIXME constructor should be private
281
282 # Key used to access this entity from Github api base.
283 fun key: String is abstract
284
285 # JSON representation of `self`.
286 #
287 # This is the same json structure than used by Github API.
288 var json: JsonObject is noinit, protected writable
289
290 # Load `json` from Github API.
291 private fun load_from_github do
292 json = api.load_from_github(key)
293 end
294
295 redef fun to_s do return json.to_json
296 end
297
298 # A Github user.
299 #
300 # Should be accessed from `GithubAPI::load_user`.
301 #
302 # See <https://developer.github.com/v3/users/>.
303 class User
304 super GithubEntity
305
306 redef var key is lazy do return "users/{login}"
307
308 # Github login.
309 var login: String
310
311 # Init `self` from a `json` object.
312 init from_json(api: GithubAPI, json: JsonObject) do
313 init(api, json["login"].to_s)
314 self.json = json
315 end
316
317 # Github User page url.
318 fun html_url: String do return json["html_url"].to_s
319
320 # Avatar image url for this user.
321 fun avatar_url: String do return json["avatar_url"].to_s
322 end
323
324 # A Github repository.
325 #
326 # Should be accessed from `GithubAPI::load_repo`.
327 #
328 # See <https://developer.github.com/v3/repos/>.
329 class Repo
330 super GithubEntity
331
332 redef var key is lazy do return "repos/{full_name}"
333
334 # Repo full name on Github.
335 var full_name: String
336
337 # Init `self` from a `json` object.
338 init from_json(api: GithubAPI, json: JsonObject) do
339 init(api, json["full_name"].to_s)
340 self.json = json
341 end
342
343 # Repo short name on Github.
344 fun name: String do return json["name"].to_s
345
346 # Github User page url.
347 fun html_url: String do return json["html_url"].to_s
348
349 # Get the repo owner.
350 fun owner: User do
351 return new User.from_json(api, json["owner"].as(JsonObject))
352 end
353
354 # List of branches associated with their names.
355 fun branches: Map[String, Branch] do
356 api.message(1, "Get branches for {full_name}")
357 var array = api.get("repos/{full_name}/branches")
358 var res = new HashMap[String, Branch]
359 if not array isa JsonArray then return res
360 for obj in array do
361 if not obj isa JsonObject then continue
362 var name = obj["name"].to_s
363 res[name] = new Branch.from_json(api, self, obj)
364 end
365 return res
366 end
367
368 # List of issues associated with their ids.
369 fun issues: Map[Int, Issue] do
370 api.message(1, "Get issues for {full_name}")
371 var res = new HashMap[Int, Issue]
372 var issue = last_issue
373 if issue == null then return res
374 res[issue.number] = issue
375 while issue.number > 1 do
376 issue = api.load_issue(self, issue.number - 1)
377 assert issue isa Issue
378 res[issue.number] = issue
379 end
380 return res
381 end
382
383 # Get the last published issue.
384 fun last_issue: nullable Issue do
385 var array = api.get("repos/{full_name}/issues")
386 if not array isa JsonArray then return null
387 if array.is_empty then return null
388 var obj = array.first
389 if not obj isa JsonObject then return null
390 return new Issue.from_json(api, self, obj)
391 end
392
393 # List of labels associated with their names.
394 fun labels: Map[String, Label] do
395 api.message(1, "Get labels for {full_name}")
396 var array = api.get("repos/{full_name}/labels")
397 var res = new HashMap[String, Label]
398 if not array isa JsonArray then return res
399 for obj in array do
400 if not obj isa JsonObject then continue
401 var name = obj["name"].to_s
402 res[name] = new Label.from_json(api, self, obj)
403 end
404 return res
405 end
406
407 # List of milestones associated with their ids.
408 fun milestones: Map[Int, Milestone] do
409 api.message(1, "Get milestones for {full_name}")
410 var array = api.get("repos/{full_name}/milestones")
411 var res = new HashMap[Int, Milestone]
412 if array isa JsonArray then
413 for obj in array do
414 if not obj isa JsonObject then continue
415 var number = obj["number"].as(Int)
416 res[number] = new Milestone.from_json(api, self, obj)
417 end
418 end
419 return res
420 end
421
422 # List of pull-requests associated with their ids.
423 #
424 # Implementation notes: because PR numbers are not consecutive,
425 # PR are loaded from pages.
426 # See: https://developer.github.com/v3/pulls/#list-pull-requests
427 fun pulls: Map[Int, PullRequest] do
428 api.message(1, "Get pulls for {full_name}")
429 var res = new HashMap[Int, PullRequest]
430 var page = 1
431 var array = api.get("{key}/pulls?page={page}").as(JsonArray)
432 while not array.is_empty do
433 for obj in array do
434 if not obj isa JsonObject then continue
435 var number = obj["number"].as(Int)
436 res[number] = new PullRequest.from_json(api, self, obj)
437 end
438 page += 1
439 array = api.get("{key}/pulls?page={page}").as(JsonArray)
440 end
441 return res
442 end
443
444 # Repo default branch.
445 fun default_branch: Branch do
446 var name = json["default_branch"].to_s
447 var branch = api.load_branch(self, name)
448 assert branch isa Branch
449 return branch
450 end
451 end
452
453 # A `RepoEntity` is something contained in a `Repo`.
454 abstract class RepoEntity
455 super GithubEntity
456
457 # Repo that contains `self`.
458 var repo: Repo
459
460 # Init `self` from a `json` object.
461 init from_json(api: GithubAPI, repo: Repo, json: JsonObject) do
462 self.api = api
463 self.repo = repo
464 self.json = json
465 end
466 end
467
468 # A Github branch.
469 #
470 # Should be accessed from `GithubAPI::load_branch`.
471 #
472 # See <https://developer.github.com/v3/repos/#list-branches>.
473 class Branch
474 super RepoEntity
475
476 redef var key is lazy do return "{repo.key}/branches/{name}"
477
478 # Branch name.
479 var name: String
480
481 redef init from_json(api, repo, json) do
482 self.name = json["name"].to_s
483 super
484 end
485
486 # Get the last commit of `self`.
487 fun commit: Commit do
488 return new Commit.from_json(api, repo, json["commit"].as(JsonObject))
489 end
490
491 # List all commits in `self`.
492 #
493 # This can be long depending on the branch size.
494 # Commit are returned in an unspecified order.
495 fun commits: Array[Commit] do
496 var res = new Array[Commit]
497 var done = new HashSet[String]
498 var todos = new Array[Commit]
499 todos.add commit
500 while not todos.is_empty do
501 var commit = todos.pop
502 if done.has(commit.sha) then continue
503 done.add commit.sha
504 res.add commit
505 for parent in commit.parents do
506 todos.add parent
507 end
508 end
509 return res
510 end
511 end
512
513 # A Github commit.
514 #
515 # Should be accessed from `GithubAPI::load_commit`.
516 #
517 # See <https://developer.github.com/v3/commits/>.
518 class Commit
519 super RepoEntity
520
521 redef var key is lazy do return "{repo.key}/commits/{sha}"
522
523 # Commit SHA.
524 var sha: String
525
526 redef init from_json(api, repo, json) do
527 self.sha = json["sha"].to_s
528 super
529 end
530
531 # Parent commits of `self`.
532 fun parents: Array[Commit] do
533 var res = new Array[Commit]
534 var parents = json["parents"]
535 if not parents isa JsonArray then return res
536 for obj in parents do
537 if not obj isa JsonObject then continue
538 res.add(api.load_commit(repo, obj["sha"].to_s).as(not null))
539 end
540 return res
541 end
542
543 # Author of the commit.
544 fun author: nullable User do
545 if not json.has_key("author") then return null
546 var user = json["author"]
547 if not user isa JsonObject then return null
548 return new User.from_json(api, user)
549 end
550
551 # Committer of the commit.
552 fun committer: nullable User do
553 if not json.has_key("committer") then return null
554 var user = json["author"]
555 if not user isa JsonObject then return null
556 return new User.from_json(api, user)
557 end
558
559 # Authoring date as ISODate.
560 fun author_date: ISODate do
561 var commit = json["commit"].as(JsonObject)
562 var author = commit["author"].as(JsonObject)
563 return new ISODate.from_string(author["date"].to_s)
564 end
565
566 # Commit date as ISODate.
567 fun commit_date: ISODate do
568 var commit = json["commit"].as(JsonObject)
569 var author = commit["committer"].as(JsonObject)
570 return new ISODate.from_string(author["date"].to_s)
571 end
572
573 # Commit message.
574 fun message: String do return json["commit"].as(JsonObject)["message"].to_s
575 end
576
577 # A Github issue.
578 #
579 # Should be accessed from `GithubAPI::load_issue`.
580 #
581 # See <https://developer.github.com/v3/issues/>.
582 class Issue
583 super RepoEntity
584
585 redef var key is lazy do return "{repo.key}/issues/{number}"
586
587 # Issue Github ID.
588 var number: Int
589
590 redef init from_json(api, repo, json) do
591 self.number = json["number"].as(Int)
592 super
593 end
594
595 # Issue title.
596 fun title: String do return json["title"].to_s
597
598 # User that created this issue.
599 fun user: User do
600 return new User.from_json(api, json["user"].as(JsonObject))
601 end
602
603 # List of labels on this issue associated to their names.
604 fun labels: Map[String, Label] do
605 var res = new HashMap[String, Label]
606 for obj in json["labels"].as(JsonArray) do
607 if not obj isa JsonObject then continue
608 var name = obj["name"].to_s
609 res[name] = new Label.from_json(api, repo, obj)
610 end
611 return res
612 end
613
614 # State of the issue on Github.
615 fun state: String do return json["state"].to_s
616
617 # Is the issue locked?
618 fun locked: Bool do return json["locked"].as(Bool)
619
620 # Assigned `User` (if any).
621 fun assignee: nullable User do
622 var assignee = json["assignee"]
623 if not assignee isa JsonObject then return null
624 return new User.from_json(api, assignee)
625 end
626
627 # `Milestone` (if any).
628 fun milestone: nullable Milestone do
629 var milestone = json["milestone"]
630 if not milestone isa JsonObject then return null
631 return new Milestone.from_json(api, repo, milestone)
632 end
633
634 # Number of comments on this issue.
635 fun comments_count: Int do return json["comments"].to_s.to_i
636
637 # Creation time in ISODate format.
638 fun created_at: ISODate do
639 return new ISODate.from_string(json["created_at"].to_s)
640 end
641
642 # Last update time in ISODate format (if any).
643 fun updated_at: nullable ISODate do
644 var res = json["updated_at"]
645 if res == null then return null
646 return new ISODate.from_string(res.to_s)
647 end
648
649 # Close time in ISODate format (if any).
650 fun closed_at: nullable ISODate do
651 var res = json["closed_at"]
652 if res == null then return null
653 return new ISODate.from_string(res.to_s)
654 end
655
656 # TODO link to pull request
657
658 # Full description of the issue.
659 fun body: String do return json["body"].to_s
660
661 # User that closed this issue (if any).
662 fun closed_by: nullable User do
663 var closer = json["closed_by"]
664 if not closer isa JsonObject then return null
665 return new User.from_json(api, closer)
666 end
667 end
668
669 # A Github pull request.
670 #
671 # Should be accessed from `GithubAPI::load_pull`.
672 #
673 # PullRequest are basically Issues with more data.
674 # See <https://developer.github.com/v3/pulls/>.
675 class PullRequest
676 super Issue
677
678 redef var key is lazy do return "{repo.key}/pulls/{number}"
679
680 # Merge time in ISODate format (if any).
681 fun merged_at: nullable ISODate do
682 var res = json["merged_at"]
683 if res == null then return null
684 return new ISODate.from_string(res.to_s)
685 end
686
687 # Merge commit SHA.
688 fun merge_commit_sha: String do return json["merge_commit_sha"].to_s
689
690 # Count of comments made on the pull request diff.
691 fun review_comments: Int do return json["review_comments"].to_s.to_i
692
693 # Pull request head (can be a commit SHA or a branch name).
694 fun head: PullRef do
695 var json = json["head"].as(JsonObject)
696 return new PullRef(api, json)
697 end
698
699 # Pull request base (can be a commit SHA or a branch name).
700 fun base: PullRef do
701 var json = json["base"].as(JsonObject)
702 return new PullRef(api, json)
703 end
704
705 # Is this pull request merged?
706 fun merged: Bool do return json["merged"].as(Bool)
707
708 # Is this pull request mergeable?
709 fun mergeable: Bool do return json["mergeable"].as(Bool)
710
711 # Mergeable state of this pull request.
712 #
713 # See <https://developer.github.com/v3/pulls/#list-pull-requests>.
714 fun mergeable_state: Int do return json["mergeable_state"].to_s.to_i
715
716 # User that merged this pull request (if any).
717 fun merged_by: nullable User do
718 var merger = json["merged_by"]
719 if not merger isa JsonObject then return null
720 return new User.from_json(api, merger)
721 end
722
723 # Count of commits in this pull request.
724 fun commits: Int do return json["commits"].to_s.to_i
725
726 # Added line count.
727 fun additions: Int do return json["additions"].to_s.to_i
728
729 # Deleted line count.
730 fun deletions: Int do return json["deletions"].to_s.to_i
731
732 # Changed files count.
733 fun changed_files: Int do return json["changed_files"].to_s.to_i
734 end
735
736 # A pull request reference (used for head and base).
737 class PullRef
738
739 # Api instance that maintains self.
740 var api: GithubAPI
741
742 # JSON representation.
743 var json: JsonObject
744
745 # Label pointed by `self`.
746 fun labl: String do return json["label"].to_s
747
748 # Reference pointed by `self`.
749 fun ref: String do return json["ref"].to_s
750
751 # Commit SHA pointed by `self`.
752 fun sha: String do return json["sha"].to_s
753
754 # User pointed by `self`.
755 fun user: User do
756 return new User.from_json(api, json["user"].as(JsonObject))
757 end
758
759 # Repo pointed by `self`.
760 fun repo: Repo do
761 return new Repo.from_json(api, json["repo"].as(JsonObject))
762 end
763 end
764
765 # A Github label.
766 #
767 # Should be accessed from `GithubAPI::load_label`.
768 #
769 # See <https://developer.github.com/v3/issues/labels/>.
770 class Label
771 super RepoEntity
772
773 redef var key is lazy do return "{repo.key}/labels/{name}"
774
775 # Label name.
776 var name: String
777
778 redef init from_json(api, repo, json) do
779 self.name = json["name"].to_s
780 super
781 end
782
783 # Label color code.
784 fun color: String do return json["color"].to_s
785 end
786
787 # A Github milestone.
788 #
789 # Should be accessed from `GithubAPI::load_milestone`.
790 #
791 # See <https://developer.github.com/v3/issues/milestones/>.
792 class Milestone
793 super RepoEntity
794
795 redef var key is lazy do return "{repo.key}/milestones/{number}"
796
797 # The milestone id on Github.
798 var number: Int
799
800 redef init from_json(api, repo, json) do
801 super
802 self.number = json["number"].as(Int)
803 end
804
805 # Milestone title.
806 fun title: String do return json["title"].to_s
807
808 # Milestone long description.
809 fun description: String do return json["description"].to_s
810
811 # Count of opened issues linked to this milestone.
812 fun open_issues: Int do return json["open_issues"].to_s.to_i
813
814 # Count of closed issues linked to this milestone.
815 fun closed_issues: Int do return json["closed_issues"].to_s.to_i
816
817 # Milestone state.
818 fun state: String do return json["state"].to_s
819
820 # Creation time in ISODate format.
821 fun created_at: ISODate do
822 return new ISODate.from_string(json["created_at"].to_s)
823 end
824
825 # User that created this milestone.
826 fun creator: User do
827 return new User.from_json(api, json["creator"].as(JsonObject))
828 end
829
830 # Due time in ISODate format (if any).
831 fun due_on: nullable ISODate do
832 var res = json["updated_at"]
833 if res == null then return null
834 return new ISODate.from_string(res.to_s)
835 end
836
837 # Update time in ISODate format (if any).
838 fun updated_at: nullable ISODate do
839 var res = json["updated_at"]
840 if res == null then return null
841 return new ISODate.from_string(res.to_s)
842 end
843
844 # Close time in ISODate format (if any).
845 fun closed_at: nullable ISODate do
846 var res = json["closed_at"]
847 if res == null then return null
848 return new ISODate.from_string(res.to_s)
849 end
850 end