github: Decouple GithubAPI from `GithubCurl`
authorAlexandre Terrasa <alexandre@moz-code.org>
Fri, 21 Jun 2019 01:22:17 +0000 (21:22 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Thu, 11 Jul 2019 00:11:58 +0000 (20:11 -0400)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

lib/github/api.nit
lib/github/tests/test_api.nit

index 292b45b..4a38fe5 100644 (file)
 # For most use-cases you need to use the `GithubAPI` client.
 module api
 
-import github_curl
+# TODO to remove
 intrude import json::serialization_read
+import json::static
+
+import base64
+import curl
+import json
 
 # Client to Github API
 #
@@ -69,10 +74,19 @@ class GithubAPI
        # See <https://developer.github.com/v3/#user-agent-required>
        var user_agent: String = "nit_github_api" is optional
 
-       # Curl instance.
-       #
-       # Internal Curl instance used to perform API calls.
-       private var ghcurl = new GithubCurl(auth or else "", user_agent) is lazy
+       # Headers to use on all requests
+       fun new_headers: HeaderMap do
+               var map = new HeaderMap
+               var auth = self.auth
+               if auth != null then
+                       map["Authorization"] = "token {auth}"
+               end
+               map["User-Agent"] = user_agent
+               # FIXME remove when projects and team are no more in beta
+               map["Accept"] = "application/vnd.github.inertia-preview+json"
+               map["Accept"] = "application/vnd.github.hellcat-preview+json"
+               return map
+       end
 
        # Github API base url.
        #
@@ -85,11 +99,49 @@ class GithubAPI
        # * `1`: verbose
        var verbose_lvl = 0 is public writable
 
+       # Send a HTTPRequest to the Github API
+       fun send(method, path: String, headers: nullable HeaderMap, body: nullable String): nullable String do
+               last_error = null
+               path = sanitize_uri(path)
+               var uri = "{api_url}{path}"
+               var request = new CurlHTTPRequest(uri)
+               request.method = method
+               request.user_agent = user_agent
+               request.headers = headers or else self.new_headers
+               request.body = body
+               return check_response(uri, request.execute)
+       end
+
+       private fun check_response(uri: String, response: CurlResponse): nullable String do
+               if response isa CurlResponseSuccess then
+                       was_error = false
+                       return response.body_str
+               else if response isa CurlResponseFailed then
+                       last_error = new GithubAPIError(
+                               response.error_msg,
+                               response.error_code,
+                               uri
+                       )
+                       was_error = true
+                       return null
+               else abort
+       end
+
        # Deserialize an object
-       fun deserialize(string: String): nullable Object do
-               var deserializer = new GithubDeserializer(string)
+       fun deserialize(string: nullable Serializable): nullable Object do
+               if string == null then return null
+               var deserializer = new GithubDeserializer(string.to_s)
                var res = deserializer.deserialize
-               # print deserializer.errors.join("\n") # DEBUG
+               if deserializer.errors.not_empty then
+                       was_error = true
+                       last_error = new GithubDeserializerErrors("Deserialization failed", deserializer.errors)
+                       return null
+               else if res isa GithubError then
+                       was_error = true
+                       last_error = res
+                       return null
+               end
+               was_error = false
                return res
        end
 
@@ -116,16 +168,8 @@ class GithubAPI
        # assert err.name == "GithubAPIError"
        # assert err.message == "Not Found"
        # ~~~
-       fun get(path: String): nullable Serializable do
-               path = sanitize_uri(path)
-               var res = ghcurl.get_and_parse("{api_url}{path}")
-               if res isa Error then
-                       last_error = res
-                       was_error = true
-                       return null
-               end
-               was_error = false
-               return res
+       fun get(path: String): nullable String do
+               return send("GET", path)
        end
 
        # Display a message depending on `verbose_lvl`.
@@ -150,8 +194,8 @@ class GithubAPI
        protected fun load_from_github(key: String): nullable GithubEntity do
                message(1, "Get {key} (github)")
                var res = get(key)
-               if was_error then return null
-               return deserialize(res.as(JsonObject).to_json).as(nullable GithubEntity)
+               if res == null then return null
+               return deserialize(res).as(nullable GithubEntity)
        end
 
        # Get the Github logged user from `auth` token.
@@ -203,8 +247,8 @@ class GithubAPI
                message(1, "Get branches for {repo.full_name}")
                var array = get("/repos/{repo.full_name}/branches")
                var res = new Array[Branch]
-               if not array isa JsonArray then return res
-               var deser = deserialize(array.to_json)
+               if array == null then return res
+               var deser = deserialize(array)
                if not deser isa Array[Object] then return res # empty array
                for branch in deser do
                        if not branch isa Branch then continue
@@ -533,6 +577,36 @@ class GithubAPI
        end
 end
 
+# An Error returned by GithubAPI
+class GithubError
+       super Error
+end
+
+# An Error returned by https://api.github.com
+#
+# Anything that can occurs when sending request to the API:
+# * Can't connect to API
+# * Ressource not found
+# * Validation error
+# * ...
+class GithubAPIError
+       super GithubError
+
+       # Status code obtained
+       var status_code: Int
+
+       # URI that returned the error
+       var requested_uri: String
+end
+
+# An Error returned while deserializing GithubEntity objects
+class GithubDeserializerErrors
+       super GithubError
+
+       # Errors returned by the deserizalization process
+       var deserizalization_errors: Array[Error]
+end
+
 # Something returned by the Github API.
 #
 # Mainly a Nit wrapper around a JSON objet.
@@ -1201,3 +1275,16 @@ class GithubDeserializer
                return super
        end
 end
+
+# Gets the Github token from `git` configuration
+#
+# Return the value of `git config --get github.oauthtoken`
+# or `""` if no key exists.
+fun get_github_oauth: String
+do
+       var p = new ProcessReader("git", "config", "--get", "github.oauthtoken")
+       var token = p.read_line
+       p.wait
+       p.close
+       return token.trim
+end
index 7176aaa..c8e7b6a 100644 (file)
@@ -23,31 +23,31 @@ intrude import api
 #
 # Cache files can be automatically created and updated by setting
 # `update_responses_cache` to `true` then running `nitunit`.
-class MockGithubCurl
-       super GithubCurl
+class MockGithubAPI
+       super GithubAPI
 
        # Mock so it returns the response from a file
        #
        # See `update_responses_cache`.
-       redef fun get_and_parse(uri) do
-               print uri # for debugging
+       redef fun send(method, path, headers, body) do
+               print path # for debugging
 
-               var path = uri.replace("https://api.github.com/", "/")
                assert has_response(path)
 
                if update_responses_cache then
                        var file = response_file(path)
-                       save_actual_response(uri, file)
+                       save_actual_response(path, file)
                end
 
-               var response = response_string(path).parse_json
+               var response = response_string(path)
                if response_is_error(path) then
-                       var title = "GithubAPIError"
-                       var msg = response.as(JsonObject)["message"].as(String)
-                       var err = new GithubError(msg, title)
-                       err.json["requested_uri"] = uri
-                       err.json["status_code"] = response_code(path)
-                       return err
+                       last_error = new GithubAPIError(
+                               response.parse_json.as(JsonObject)["message"].as(String),
+                               response_code(path).to_i,
+                               path
+                       )
+                       was_error = true
+                       return null
                end
                return response
        end
@@ -126,9 +126,9 @@ class MockGithubCurl
        private fun save_actual_response(uri, file: String) do
                assert update_responses_cache
 
-               var request = new CurlHTTPRequest(uri)
-               request.user_agent = actual_curl.user_agent
-               request.headers = actual_curl.header
+               var request = new CurlHTTPRequest("{api_url}{sanitize_uri(uri)}")
+               request.user_agent = actual_api.user_agent
+               request.headers = actual_api.new_headers
                var response = request.execute
 
                if response isa CurlResponseSuccess then
@@ -141,22 +141,16 @@ class MockGithubCurl
        end
 
        # Actual GithubCurl instance used for caching
-       private var actual_curl = new GithubCurl(get_github_oauth, "nitunit")
+       private var actual_api = new GithubAPI(get_github_oauth, "nitunit")
 end
 
 class TestGithubAPI
        test
 
-       var mock = new MockGithubCurl("test", "test")
-
-       fun api: GithubAPI do
-               var api = new GithubAPI("test")
-               api.ghcurl = mock
-               return api
-       end
+       fun api: MockGithubAPI do return new MockGithubAPI("test", "test")
 
        fun test_deserialize is test do
-               var response = mock.response_string("/users/Morriar")
+               var response = api.response_string("/users/Morriar")
                var obj = api.deserialize(response)
                assert obj isa User
                assert obj.login == "Morriar"
@@ -172,8 +166,8 @@ class TestGithubAPI
                var obj = api.get("/users/Morriar")
                assert not api.was_error
                assert api.last_error == null
-               assert obj isa JsonObject
-               assert obj["login"] == "Morriar"
+               assert obj != null
+               assert obj.parse_json.as(JsonObject)["login"] == "Morriar"
        end
 
        fun test_get_404 is test do
@@ -182,8 +176,8 @@ class TestGithubAPI
                assert res == null
                assert api.was_error
                var err = api.last_error
-               assert err isa GithubError
-               assert err.name == "GithubAPIError"
+               assert err isa GithubAPIError
+               assert err.status_code == 404
                assert err.message == "Not Found"
        end
 
@@ -202,8 +196,8 @@ class TestGithubAPI
                assert res == null
                assert api.was_error
                var err = api.last_error
-               assert err isa GithubError
-               assert err.name == "GithubAPIError"
+               assert err isa GithubAPIError
+               assert err.status_code == 404
                assert err.message == "Not Found"
        end