From 6043cc7223ac136624520d47c3c3c00f213779a8 Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Mon, 2 Mar 2026 10:43:17 -0500 Subject: [PATCH] Port HTTP transport from ponylang/http to ponylang/courier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the HTTP handler factory pattern (25 types: 8 requesters + 8 handler factories + 8 handlers + RequestFactory) with courier's actor-based connection model (6 types: 4 request actors + 1 SSL factory + 1 shared interface). Each API call creates a short-lived actor owning an HTTPClientConnection. Consolidation by response pattern: POST+PATCH share JsonRequester (differ by method/expected status), DELETE+PUT share NoContentRequester, paginated+search share LinkedJsonRequester via a LinkedResultReceiver interface. All actors close their connection in on_response_complete so the runtime exits promptly. SSLContextFactory tries the system CA store for verified HTTPS; when unavailable it falls back to unverified SSL, matching the old HTTPClient behavior. Breaking change: Credentials.auth changes from net.TCPConnectAuth to lori.TCPConnectAuth. Authorization header moves from legacy "token" format to "Bearer" format (GitHub accepts both). Public API is otherwise preserved — all operation primitives, model classes, OO convenience methods, and pagination work the same way. --- .release-notes/next-release.md | 1 - .release-notes/port-to-courier.md | 21 ++ CLAUDE.md | 25 +- corral.json | 4 +- examples/create-gist-oo/main.pony | 4 +- examples/create-gist/main.pony | 4 +- examples/create-issue-comment-oo/main.pony | 4 +- examples/create-issue-comment/main.pony | 4 +- examples/create-label-oo/main.pony | 4 +- examples/create-label/main.pony | 4 +- examples/create-release-oo/main.pony | 4 +- examples/create-release/main.pony | 4 +- examples/delete-label-oo/main.pony | 4 +- examples/delete-label/main.pony | 4 +- examples/get-commit-oo/main.pony | 4 +- examples/get-commit/main.pony | 4 +- examples/get-gist-oo/main.pony | 4 +- examples/get-gist/main.pony | 4 +- examples/get-issue-comments-oo/main.pony | 4 +- examples/get-issue-comments/main.pony | 4 +- examples/get-issue-oo/main.pony | 4 +- examples/get-issue/main.pony | 4 +- examples/get-issues-oo/main.pony | 4 +- examples/get-issues/main.pony | 4 +- examples/get-pull-request-files-oo/main.pony | 4 +- examples/get-pull-request-files/main.pony | 4 +- examples/get-pull-request-oo/main.pony | 4 +- examples/get-pull-request/main.pony | 4 +- examples/get-repository-labels/main.pony | 4 +- examples/get-repository-oo/main.pony | 4 +- examples/get-repository/main.pony | 4 +- examples/gist-comments-oo/main.pony | 4 +- examples/gist-comments/main.pony | 4 +- examples/list-gists-oo/main.pony | 4 +- examples/list-gists/main.pony | 4 +- examples/search-issues/main.pony | 4 +- examples/standard-pony-labels/main.pony | 4 +- examples/star-gist-oo/main.pony | 4 +- examples/star-gist/main.pony | 4 +- github_rest_api/commit.pony | 10 +- github_rest_api/gist.pony | 103 +-------- github_rest_api/gist_comment.pony | 54 +---- github_rest_api/github.pony | 1 - github_rest_api/issue.pony | 18 +- github_rest_api/issue_comment.pony | 24 +- github_rest_api/label.pony | 22 +- github_rest_api/paginated_list.pony | 214 +++++++++--------- github_rest_api/pull_request.pony | 11 +- github_rest_api/pull_request_file.pony | 12 +- github_rest_api/release.pony | 12 +- github_rest_api/repository.pony | 25 +- github_rest_api/request/_ssl.pony | 20 ++ github_rest_api/request/check_requester.pony | 123 ++++++++++ .../request/{http.pony => credentials.pony} | 31 +-- github_rest_api/request/http_check.pony | 117 ---------- github_rest_api/request/http_delete.pony | 102 --------- github_rest_api/request/http_get.pony | 106 --------- github_rest_api/request/http_patch.pony | 92 -------- github_rest_api/request/http_post.pony | 92 -------- github_rest_api/request/http_put.pony | 87 ------- github_rest_api/request/json.pony | 1 - github_rest_api/request/json_requester.pony | 165 ++++++++++++++ .../request/no_content_requester.pony | 141 ++++++++++++ github_rest_api/request/query_params.pony | 4 +- github_rest_api/search.pony | 140 +----------- 65 files changed, 704 insertions(+), 1214 deletions(-) create mode 100644 .release-notes/port-to-courier.md create mode 100644 github_rest_api/request/_ssl.pony create mode 100644 github_rest_api/request/check_requester.pony rename github_rest_api/request/{http.pony => credentials.pony} (55%) delete mode 100644 github_rest_api/request/http_check.pony delete mode 100644 github_rest_api/request/http_delete.pony delete mode 100644 github_rest_api/request/http_get.pony delete mode 100644 github_rest_api/request/http_patch.pony delete mode 100644 github_rest_api/request/http_post.pony delete mode 100644 github_rest_api/request/http_put.pony create mode 100644 github_rest_api/request/json_requester.pony create mode 100644 github_rest_api/request/no_content_requester.pony diff --git a/.release-notes/next-release.md b/.release-notes/next-release.md index 3937198..f7b5c42 100644 --- a/.release-notes/next-release.md +++ b/.release-notes/next-release.md @@ -33,7 +33,6 @@ end gist.update_gist(updates) ``` -This also adds three new HTTP infrastructure classes: `HTTPPatch` (PATCH with JSON response), `HTTPPut` (PUT expecting 204), and `HTTPCheck` (GET returning Bool based on status code 204/404). ## Add query parameters to GetRepositoryIssues diff --git a/.release-notes/port-to-courier.md b/.release-notes/port-to-courier.md new file mode 100644 index 0000000..e9f0d90 --- /dev/null +++ b/.release-notes/port-to-courier.md @@ -0,0 +1,21 @@ +## Port HTTP transport from ponylang/http to ponylang/courier + +The HTTP transport layer has been replaced with ponylang/courier, which uses an actor-based connection model instead of the handler factory pattern. All public API operations work the same way, but `Credentials.auth` has changed type. + +Before: +```pony +use "net" + +let auth = TCPConnectAuth(env.root) +let creds = Credentials(auth, token) +``` + +After: +```pony +use lori = "lori" + +let auth = lori.TCPConnectAuth(env.root) +let creds = Credentials(auth, token) +``` + +Authorization headers now use `Bearer` format (`Authorization: Bearer `) instead of the legacy `token` format. GitHub accepts both. diff --git a/CLAUDE.md b/CLAUDE.md index 986e4cd..765cfdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,8 +18,9 @@ Uses `corral` for dependency management. `make` automatically runs `corral fetch ## Dependencies -- `github.com/ponylang/http.git` -- HTTP client -- `github.com/ponylang/net_ssl.git` (via http) -- SSL/TLS +- `github.com/ponylang/courier.git` -- HTTP client (actor-based) +- `github.com/ponylang/lori.git` (via courier) -- TCP connections +- `github.com/ponylang/ssl.git` (via courier) -- SSL/TLS - `github.com/ponylang/web_link.git` -- RFC 8288 Link header parsing - `github.com/ponylang/json-ng.git` -- JSON parsing (immutable, persistent collections) - `github.com/ponylang/uri.git` -- RFC 6570 URI template expansion @@ -52,16 +53,14 @@ github_rest_api/ user.pony -- User model license.pony -- License model json_nav_util.pony -- JsonNavUtil (string_or_none for nullable JSON fields) - paginated_list.pony -- PaginatedList[A] with prev/next page navigation + paginated_list.pony -- PaginatedList[A] with prev/next page navigation, LinkedJsonRequester, LinkedResultReceiver _extract_pagination_links.pony -- Extracts prev/next URLs from Link headers (via web_link) request/ -- HTTP request infrastructure (temporary home, intended to be extracted to its own library) - http.pony -- Credentials, ResultReceiver, RequestFactory - http_get.pony -- JsonRequester (GET with JSON response) - http_post.pony -- HTTPPost (POST with JSON response) - http_patch.pony -- HTTPPatch (PATCH with JSON response, expects 200) - http_delete.pony -- HTTPDelete (DELETE, expects 204) - http_put.pony -- HTTPPut (PUT with no body, expects 204) - http_check.pony -- HTTPCheck (GET returning Bool: 204=true, 404=false) + credentials.pony -- Credentials (lori.TCPConnectAuth + token), ResultReceiver + _ssl.pony -- SSLContextFactory (shared SSL context creation) + json_requester.pony -- JsonRequester actor (GET/POST/PATCH with JSON response) + no_content_requester.pony -- NoContentRequester actor (DELETE/PUT expecting 204) + check_requester.pony -- CheckRequester actor (GET returning Bool: 204=true, 404=false) request_error.pony -- RequestError (status, response_body, message) json.pony -- JsonConverter interface, JsonTypeString utility query_params.pony -- QueryParams (URL query string builder with percent-encoding) @@ -78,7 +77,7 @@ All API operations return `Promise[(T | RequestError)]`. The flow is: 1. Operation primitive (e.g., `GetRepository`) creates a `Promise` 2. Creates a `ResultReceiver[T]` actor with the promise and a `JsonConverter[T]` 3. Builds URL using `ponylang/uri` RFC 6570 template expansion for path parameters -4. Issues HTTP request via `JsonRequester` / `HTTPPost` / `HTTPPatch` / `HTTPDelete` / `HTTPPut` / `HTTPCheck` +4. Creates a short-lived request actor (`JsonRequester` / `NoContentRequester` / `CheckRequester` / `LinkedJsonRequester`) that owns an `HTTPClientConnection` from `ponylang/courier` 5. On success, JSON is parsed and converted to model via `JsonConverter` 6. Promise is fulfilled with either the model or a `RequestError` @@ -102,7 +101,7 @@ Models have methods that chain to further API calls: ### Auth -`Credentials` holds a `TCPConnectAuth` and an optional token string. `RequestFactory` sets `User-Agent`, `Accept: application/vnd.github.v3+json`, and `Authorization: token ` headers. +`Credentials` holds a `lori.TCPConnectAuth` and an optional token string. Each request actor sets `User-Agent`, `Accept: application/vnd.github.v3+json`, and `Authorization: Bearer ` headers. ## Conventions @@ -115,7 +114,7 @@ Models have methods that chain to further API calls: ## Known TODOs in Code -1. Potential HTTP GET duplication with paginated variant (paginated_list.pony) +None currently tracked. ## GitHub REST API Coverage Comparison diff --git a/corral.json b/corral.json index 73dff81..a175461 100644 --- a/corral.json +++ b/corral.json @@ -9,8 +9,8 @@ "version": "0.1.0" }, { - "locator": "github.com/ponylang/http.git", - "version": "0.6.4" + "locator": "github.com/ponylang/courier.git", + "version": "0.1.0" }, { "locator": "github.com/ponylang/json-ng.git", diff --git a/examples/create-gist-oo/main.pony b/examples/create-gist-oo/main.pony index 1a6d191..a149d37 100644 --- a/examples/create-gist-oo/main.pony +++ b/examples/create-gist-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -42,7 +42,7 @@ actor Main let token = cmd.option("token").string() // ----- Create gist - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let files = recover val diff --git a/examples/create-gist/main.pony b/examples/create-gist/main.pony index 34eb474..9c9743a 100644 --- a/examples/create-gist/main.pony +++ b/examples/create-gist/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -42,7 +42,7 @@ actor Main let token = cmd.option("token").string() // ----- Create gist - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let files = recover val diff --git a/examples/create-issue-comment-oo/main.pony b/examples/create-issue-comment-oo/main.pony index 7db267d..e1d72f1 100644 --- a/examples/create-issue-comment-oo/main.pony +++ b/examples/create-issue-comment-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -39,7 +39,7 @@ actor Main let token = cmd.option("token").string() // ----- Create issue comment - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/create-issue-comment/main.pony b/examples/create-issue-comment/main.pony index aeeac40..d1ea9b8 100644 --- a/examples/create-issue-comment/main.pony +++ b/examples/create-issue-comment/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -38,7 +38,7 @@ actor Main let token = cmd.option("token").string() // ----- Create issue comment - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = CreateIssueComment(owner, repo, issue, comment, creds) diff --git a/examples/create-label-oo/main.pony b/examples/create-label-oo/main.pony index a323fc3..36e0313 100644 --- a/examples/create-label-oo/main.pony +++ b/examples/create-label-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -43,7 +43,7 @@ actor Main let token = cmd.option("token").string() // ----- Create issue comment - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/create-label/main.pony b/examples/create-label/main.pony index ef00a7a..e76ce8d 100644 --- a/examples/create-label/main.pony +++ b/examples/create-label/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -42,7 +42,7 @@ actor Main let token = cmd.option("token").string() // ----- Create issue comment - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = CreateLabel(owner, repo, name, creds, color, description) diff --git a/examples/create-release-oo/main.pony b/examples/create-release-oo/main.pony index 16d61d9..6c3b84c 100644 --- a/examples/create-release-oo/main.pony +++ b/examples/create-release-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -41,7 +41,7 @@ actor Main let token = cmd.option("token").string() // ----- Create release - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/create-release/main.pony b/examples/create-release/main.pony index 369d29b..1cba030 100644 --- a/examples/create-release/main.pony +++ b/examples/create-release/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -40,7 +40,7 @@ actor Main let token = cmd.option("token").string() // ----- Create release - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) CreateRelease(owner, repo, tag_name, name, body, creds) diff --git a/examples/delete-label-oo/main.pony b/examples/delete-label-oo/main.pony index dd76cf9..89938fc 100644 --- a/examples/delete-label-oo/main.pony +++ b/examples/delete-label-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -39,7 +39,7 @@ actor Main let token = cmd.option("token").string() // ----- Create issue comment - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/delete-label/main.pony b/examples/delete-label/main.pony index 47b8473..d5cefa7 100644 --- a/examples/delete-label/main.pony +++ b/examples/delete-label/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -38,7 +38,7 @@ actor Main let token = cmd.option("token").string() // ----- Create issue comment - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) DeleteLabel(owner, repo, name, creds) diff --git a/examples/get-commit-oo/main.pony b/examples/get-commit-oo/main.pony index 52663b8..58a5348 100644 --- a/examples/get-commit-oo/main.pony +++ b/examples/get-commit-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -39,7 +39,7 @@ actor Main let token = cmd.option("token").string() // ----- Get commit - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/get-commit/main.pony b/examples/get-commit/main.pony index e9e5286..2d9f89f 100644 --- a/examples/get-commit/main.pony +++ b/examples/get-commit/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -38,7 +38,7 @@ actor Main let token = cmd.option("token").string() // ----- Get commit - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetCommit(owner, repo, sha, creds) diff --git a/examples/get-gist-oo/main.pony b/examples/get-gist-oo/main.pony index 5637b74..c994e0d 100644 --- a/examples/get-gist-oo/main.pony +++ b/examples/get-gist-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -34,7 +34,7 @@ actor Main let token = cmd.option("token").string() // ----- Get gist - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_gist(gist_id) diff --git a/examples/get-gist/main.pony b/examples/get-gist/main.pony index 7c7c247..9c2951f 100644 --- a/examples/get-gist/main.pony +++ b/examples/get-gist/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -34,7 +34,7 @@ actor Main let token = cmd.option("token").string() // ----- Get gist - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetGist(gist_id, creds) diff --git a/examples/get-issue-comments-oo/main.pony b/examples/get-issue-comments-oo/main.pony index f69ed32..ac64680 100644 --- a/examples/get-issue-comments-oo/main.pony +++ b/examples/get-issue-comments-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -39,7 +39,7 @@ actor Main let token = cmd.option("token").string() // ----- Get issue comments - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/get-issue-comments/main.pony b/examples/get-issue-comments/main.pony index 3168718..47daad3 100644 --- a/examples/get-issue-comments/main.pony +++ b/examples/get-issue-comments/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -38,7 +38,7 @@ actor Main let token = cmd.option("token").string() // ----- Get issue comments - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetIssueComments(owner, repo, issue, creds) diff --git a/examples/get-issue-oo/main.pony b/examples/get-issue-oo/main.pony index 595dae0..5c73d0a 100644 --- a/examples/get-issue-oo/main.pony +++ b/examples/get-issue-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -39,7 +39,7 @@ actor Main let token = cmd.option("token").string() // ----- Get issue - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/get-issue/main.pony b/examples/get-issue/main.pony index 92a83ef..5267db1 100644 --- a/examples/get-issue/main.pony +++ b/examples/get-issue/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -38,7 +38,7 @@ actor Main let token = cmd.option("token").string() // ----- Get issue - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetIssue(owner, repo, issue, creds) diff --git a/examples/get-issues-oo/main.pony b/examples/get-issues-oo/main.pony index 25fbcc5..f47d632 100644 --- a/examples/get-issues-oo/main.pony +++ b/examples/get-issues-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -78,7 +78,7 @@ actor Main end // ----- Get issues - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/get-issues/main.pony b/examples/get-issues/main.pony index 9ef45cc..5f2d003 100644 --- a/examples/get-issues/main.pony +++ b/examples/get-issues/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -78,7 +78,7 @@ actor Main end // ----- Get issues - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetRepositoryIssues(owner, repo, creds diff --git a/examples/get-pull-request-files-oo/main.pony b/examples/get-pull-request-files-oo/main.pony index 0826ca7..d102d29 100644 --- a/examples/get-pull-request-files-oo/main.pony +++ b/examples/get-pull-request-files-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -39,7 +39,7 @@ actor Main let token = cmd.option("token").string() // ----- Get pull request files - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/get-pull-request-files/main.pony b/examples/get-pull-request-files/main.pony index 88a8d8d..86abf35 100644 --- a/examples/get-pull-request-files/main.pony +++ b/examples/get-pull-request-files/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -38,7 +38,7 @@ actor Main let token = cmd.option("token").string() // ----- Get pull request files - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetPullRequestFiles(owner, repo, pr, creds) diff --git a/examples/get-pull-request-oo/main.pony b/examples/get-pull-request-oo/main.pony index 9e37e4b..1f618af 100644 --- a/examples/get-pull-request-oo/main.pony +++ b/examples/get-pull-request-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -39,7 +39,7 @@ actor Main let token = cmd.option("token").string() // ----- Get pull request - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/get-pull-request/main.pony b/examples/get-pull-request/main.pony index 3e9fc15..8e31597 100644 --- a/examples/get-pull-request/main.pony +++ b/examples/get-pull-request/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -38,7 +38,7 @@ actor Main let token = cmd.option("token").string() // ----- Get pull request - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetPullRequest(owner, repo, pr, creds) diff --git a/examples/get-repository-labels/main.pony b/examples/get-repository-labels/main.pony index 279659a..16300f9 100644 --- a/examples/get-repository-labels/main.pony +++ b/examples/get-repository-labels/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -37,7 +37,7 @@ actor Main let token = cmd.option("token").string() // ----- Get repository - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetRepositoryLabels(owner, repo, creds) diff --git a/examples/get-repository-oo/main.pony b/examples/get-repository-oo/main.pony index 63572c9..0a82411 100644 --- a/examples/get-repository-oo/main.pony +++ b/examples/get-repository-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -36,7 +36,7 @@ actor Main let token = cmd.option("token").string() // ----- Get repository - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_repo(owner, repo) diff --git a/examples/get-repository/main.pony b/examples/get-repository/main.pony index 884e921..a260f40 100644 --- a/examples/get-repository/main.pony +++ b/examples/get-repository/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" actor Main new create(env: Env) => @@ -36,7 +36,7 @@ actor Main let token = cmd.option("token").string() // ----- Get repository - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetRepository(owner, repo, creds) diff --git a/examples/gist-comments-oo/main.pony b/examples/gist-comments-oo/main.pony index 72c9138..dfd1276 100644 --- a/examples/gist-comments-oo/main.pony +++ b/examples/gist-comments-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -35,7 +35,7 @@ actor Main let token = cmd.option("token").string() // ----- Get gist comments via OO API - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_gist(gist_id) diff --git a/examples/gist-comments/main.pony b/examples/gist-comments/main.pony index 7d524f0..237e218 100644 --- a/examples/gist-comments/main.pony +++ b/examples/gist-comments/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -35,7 +35,7 @@ actor Main let token = cmd.option("token").string() // ----- Get gist comments - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetGistComments(gist_id, creds) diff --git a/examples/list-gists-oo/main.pony b/examples/list-gists-oo/main.pony index 2ac8cb3..d41d247 100644 --- a/examples/list-gists-oo/main.pony +++ b/examples/list-gists-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -31,7 +31,7 @@ actor Main let token = cmd.option("token").string() // ----- List gists - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_user_gists() diff --git a/examples/list-gists/main.pony b/examples/list-gists/main.pony index d92f6a7..c4ad197 100644 --- a/examples/list-gists/main.pony +++ b/examples/list-gists/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -31,7 +31,7 @@ actor Main let token = cmd.option("token").string() // ----- List gists - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetUserGists(creds) diff --git a/examples/search-issues/main.pony b/examples/search-issues/main.pony index cc0cdf5..05515cf 100644 --- a/examples/search-issues/main.pony +++ b/examples/search-issues/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -35,7 +35,7 @@ actor Main let token = cmd.option("token").string() // ----- Search issues - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = SearchIssues(query, creds) diff --git a/examples/standard-pony-labels/main.pony b/examples/standard-pony-labels/main.pony index b48d737..c5eca48 100644 --- a/examples/standard-pony-labels/main.pony +++ b/examples/standard-pony-labels/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -37,7 +37,7 @@ actor Main let token = cmd.option("token").string() // ----- Create issue comment - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) let p = GetRepositoryLabels(owner, repo, creds) diff --git a/examples/star-gist-oo/main.pony b/examples/star-gist-oo/main.pony index 3c03724..0b1dee3 100644 --- a/examples/star-gist-oo/main.pony +++ b/examples/star-gist-oo/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -33,7 +33,7 @@ actor Main let token = cmd.option("token").string() // ----- Star gist then check via OO API - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) GitHub(creds).get_gist(gist_id) diff --git a/examples/star-gist/main.pony b/examples/star-gist/main.pony index 7b03901..79e77ce 100644 --- a/examples/star-gist/main.pony +++ b/examples/star-gist/main.pony @@ -1,7 +1,7 @@ use "../../github_rest_api" use "../../github_rest_api/request" use "cli" -use "net" +use lori = "lori" use "promises" actor Main @@ -33,7 +33,7 @@ actor Main let token = cmd.option("token").string() // ----- Star gist then check - let auth = TCPConnectAuth(env.root) + let auth = lori.TCPConnectAuth(env.root) let creds = Credentials(auth, token) StarGist(gist_id, creds) diff --git a/github_rest_api/commit.pony b/github_rest_api/commit.pony index 6397ca0..23cdf4b 100644 --- a/github_rest_api/commit.pony +++ b/github_rest_api/commit.pony @@ -60,15 +60,7 @@ primitive GetCommit let p = Promise[CommitOrError] let receiver = req.ResultReceiver[Commit](creds, p, CommitJsonConverter) - try - req.JsonRequester(creds)(url, receiver)? - else - let m = recover val - "Unable to initiate get commit request to" + url - end - p(req.RequestError(where message' = m)) - end - + req.JsonRequester.get(creds, url, receiver) p primitive CommitJsonConverter is req.JsonConverter[Commit] diff --git a/github_rest_api/gist.pony b/github_rest_api/gist.pony index 75571d3..f28740b 100644 --- a/github_rest_api/gist.pony +++ b/github_rest_api/gist.pony @@ -181,15 +181,7 @@ primitive GetGist let p = Promise[GistOrError] let r = req.ResultReceiver[Gist](creds, p, GistJsonConverter) - try - req.JsonRequester(creds)(url, r)? - else - let m = recover val - "Unable to initiate get_gist request to " + url - end - p(req.RequestError(where message' = m)) - end - + req.JsonRequester.get(creds, url, r) p primitive CreateGist @@ -231,16 +223,7 @@ primitive CreateGist end let json = obj.string() - try - req.HTTPPost(creds.auth)(url, - consume json, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to create gist at " + url)) - end - + req.JsonRequester.post(creds, url, consume json, r) p primitive UpdateGist @@ -297,16 +280,7 @@ primitive UpdateGist end let json = obj.string() - try - req.HTTPPatch(creds.auth)(url, - consume json, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to update gist at " + url)) - end - + req.JsonRequester.patch(creds, url, consume json, r) p primitive DeleteGist @@ -333,15 +307,7 @@ primitive DeleteGist let p = Promise[req.DeletedOrError] let r = req.DeletedResultReceiver(p) - try - req.HTTPDelete(creds.auth)(url, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to delete gist at " + url)) - end - + req.NoContentRequester.delete(creds, url, r) p primitive GetUserGists @@ -437,16 +403,7 @@ primitive ForkGist let p = Promise[GistOrError] let r = req.ResultReceiver[Gist](creds, p, GistJsonConverter) - try - req.HTTPPost(creds.auth)(url, - "", - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to fork gist at " + url)) - end - + req.JsonRequester.post(creds, url, "", r) p primitive GetGistForks @@ -498,15 +455,7 @@ primitive GetGistCommits let p = Promise[(PaginatedList[GistCommit] | req.RequestError)] let r = PaginatedResultReceiver[GistCommit](creds, p, plc) - try - PaginatedJsonRequester(creds).apply[GistCommit](url, r)? - else - let m = recover val - "Unable to initiate get_gist_commits request to " + url - end - p(req.RequestError(where message' = m)) - end - + LinkedJsonRequester(creds, url, r) p primitive StarGist @@ -534,15 +483,7 @@ primitive StarGist let p = Promise[req.DeletedOrError] let r = req.DeletedResultReceiver(p) - try - req.HTTPPut(creds.auth)(url, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to star gist at " + url)) - end - + req.NoContentRequester.put(creds, url, r) p primitive UnstarGist @@ -570,15 +511,7 @@ primitive UnstarGist let p = Promise[req.DeletedOrError] let r = req.DeletedResultReceiver(p) - try - req.HTTPDelete(creds.auth)(url, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to unstar gist at " + url)) - end - + req.NoContentRequester.delete(creds, url, r) p primitive CheckGistStar @@ -607,15 +540,7 @@ primitive CheckGistStar let p = Promise[req.BoolOrError] let r = req.BoolResultReceiver(p) - try - req.HTTPCheck(creds.auth)(url, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to check gist star at " + url)) - end - + req.CheckRequester(creds, url, r) p primitive _GetPaginatedGists @@ -632,15 +557,7 @@ primitive _GetPaginatedGists let p = Promise[(PaginatedList[Gist] | req.RequestError)] let r = PaginatedResultReceiver[Gist](creds, p, plc) - try - PaginatedJsonRequester(creds).apply[Gist](url, r)? - else - let m = recover val - "Unable to initiate get_gists request to " + url - end - p(req.RequestError(where message' = m)) - end - + LinkedJsonRequester(creds, url, r) p primitive GistJsonConverter is req.JsonConverter[Gist] diff --git a/github_rest_api/gist_comment.pony b/github_rest_api/gist_comment.pony index 241c246..e23eb18 100644 --- a/github_rest_api/gist_comment.pony +++ b/github_rest_api/gist_comment.pony @@ -81,15 +81,7 @@ primitive GetGistComment p, GistCommentJsonConverter) - try - req.JsonRequester(creds)(url, r)? - else - let m = recover val - "Unable to initiate get_gist_comment request to " + url - end - p(req.RequestError(where message' = m)) - end - + req.JsonRequester.get(creds, url, r) p primitive GetGistComments @@ -121,15 +113,7 @@ primitive GetGistComments let p = Promise[(PaginatedList[GistComment] | req.RequestError)] let r = PaginatedResultReceiver[GistComment](creds, p, plc) - try - PaginatedJsonRequester(creds).apply[GistComment](url, r)? - else - let m = recover val - "Unable to initiate get_gist_comments request to " + url - end - p(req.RequestError(where message' = m)) - end - + LinkedJsonRequester(creds, url, r) p primitive CreateGistComment @@ -162,17 +146,7 @@ primitive CreateGistComment GistCommentJsonConverter) let json = JsonObject.update("body", body).string() - - try - req.HTTPPost(creds.auth)(url, - consume json, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to create gist comment on " + url)) - end - + req.JsonRequester.post(creds, url, consume json, r) p primitive UpdateGistComment @@ -207,17 +181,7 @@ primitive UpdateGistComment GistCommentJsonConverter) let json = JsonObject.update("body", body).string() - - try - req.HTTPPatch(creds.auth)(url, - consume json, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to update gist comment on " + url)) - end - + req.JsonRequester.patch(creds, url, consume json, r) p primitive DeleteGistComment @@ -247,15 +211,7 @@ primitive DeleteGistComment let p = Promise[req.DeletedOrError] let r = req.DeletedResultReceiver(p) - try - req.HTTPDelete(creds.auth)(url, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to delete gist comment on " + url)) - end - + req.NoContentRequester.delete(creds, url, r) p primitive GistCommentJsonConverter is req.JsonConverter[GistComment] diff --git a/github_rest_api/github.pony b/github_rest_api/github.pony index 7b2d480..12578e8 100644 --- a/github_rest_api/github.pony +++ b/github_rest_api/github.pony @@ -1,4 +1,3 @@ -use "net" use "promises" use req = "request" diff --git a/github_rest_api/issue.pony b/github_rest_api/issue.pony index 9ed89d0..23c87e0 100644 --- a/github_rest_api/issue.pony +++ b/github_rest_api/issue.pony @@ -159,15 +159,7 @@ primitive GetIssue let p = Promise[IssueOrError] let receiver = req.ResultReceiver[Issue](creds, p, IssueJsonConverter) - try - req.JsonRequester(creds)(url, receiver)? - else - let m = recover val - "Unable to initiate get_issue request to" + url - end - p(req.RequestError(where message' = m)) - end - + req.JsonRequester.get(creds, url, receiver) p primitive GetRepositoryIssues @@ -224,13 +216,7 @@ primitive GetRepositoryIssues let p = Promise[(PaginatedList[Issue] | req.RequestError)] let r = PaginatedResultReceiver[Issue](creds, p, plc) - try - PaginatedJsonRequester(creds).apply[Issue](url, r)? - else - let m = "Unable to initiate get_repository_issues request to " + url - p(req.RequestError(where message' = consume m)) - end - + LinkedJsonRequester(creds, url, r) p diff --git a/github_rest_api/issue_comment.pony b/github_rest_api/issue_comment.pony index 81dcff9..e2a257c 100644 --- a/github_rest_api/issue_comment.pony +++ b/github_rest_api/issue_comment.pony @@ -1,5 +1,4 @@ use "json" -use "net" use "promises" use req = "request" use ut = "uri/template" @@ -60,17 +59,7 @@ primitive CreateIssueComment IssueCommentJsonConverter) let json = JsonObject.update("body", comment).string() - - try - req.HTTPPost(creds.auth)(url, - consume json, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to create issue comment on " + url)) - end - + req.JsonRequester.post(creds, url, consume json, r) p primitive GetIssueComments @@ -98,16 +87,7 @@ primitive GetIssueComments p, IssueCommentsJsonConverter) - try - req.JsonRequester(creds)(url, r)? - else - let m = recover val - "Unable to initiate get_comments request to" + url - end - - p(req.RequestError(where message' = m)) - end - + req.JsonRequester.get(creds, url, r) p primitive IssueCommentsURL diff --git a/github_rest_api/label.pony b/github_rest_api/label.pony index b6039ac..b5e4d43 100644 --- a/github_rest_api/label.pony +++ b/github_rest_api/label.pony @@ -79,17 +79,7 @@ primitive CreateLabel | let d: String => obj = obj.update("description", d) end let json = obj.string() - - try - req.HTTPPost(creds.auth)(url, - consume json, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to create label on " + url)) - end - + req.JsonRequester.post(creds, url, consume json, r) p primitive DeleteLabel @@ -123,15 +113,7 @@ primitive DeleteLabel let p = Promise[req.DeletedOrError] let r = req.DeletedResultReceiver(p) - try - req.HTTPDelete(creds.auth)(url, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to delete label on " + url)) - end - + req.NoContentRequester.delete(creds, url, r) p primitive LabelJsonConverter is req.JsonConverter[Label] diff --git a/github_rest_api/paginated_list.pony b/github_rest_api/paginated_list.pony index c3db359..0fc4da7 100644 --- a/github_rest_api/paginated_list.pony +++ b/github_rest_api/paginated_list.pony @@ -1,18 +1,17 @@ -use "http" +use courier = "courier" use "json" -use "ssl/net" +use lori = "lori" use "promises" use req = "request" -// TODO: There's potentially a ton of duplication with HTTP get here -// it exists so I don't have to warp the JsonConverter API -// but there might be other ways to address. Perhaps -// something like grabbing link headers and and passing along -// as part of standard json requester and having the results receiver -// match on 2 different "converter" interfaces for "takes headers" and "no -// headers" and call accordingly. -// so there's JsonConverter and PaginatingJsonConverter -// and a type alias that is (JsonConverter | PagingatingJsonConverter) +interface tag LinkedResultReceiver + """ + Receives the result of an HTTP GET request that returns JSON along with a + Link header. Used by both paginated list and search result requesters. + """ + be success(json: JsonNav, link_header: String) + be failure(status: U16, response_body: String, message: String) + class val PaginatedList[A: Any val] """ A page of results from a paginated GitHub API endpoint. Use `prev_page()` @@ -21,8 +20,6 @@ class val PaginatedList[A: Any val] """ let _creds: req.Credentials let _converter: PaginatedListJsonConverter[A] - // only for search. not present otherwise - //let _total_results: USize let _prev_link: (String | None) let _next_link: (String | None) @@ -65,15 +62,9 @@ class val PaginatedList[A: Any val] fun _retrieve_link(link: String): Promise[(PaginatedList[A] | req.RequestError)] => - let p = Promise[(PaginatedList[A] | req.RequestError)] + let p = Promise[(PaginatedList[A] | req.RequestError)] let r = PaginatedResultReceiver[A](_creds, p, _converter) - - try - PaginatedJsonRequester(_creds).apply[A](link, r)? - else - let m = "Unable to get " + link - p(req.RequestError(where message' = consume m)) - end + LinkedJsonRequester(_creds, link, r) p class val PaginatedListJsonConverter[A: Any val] @@ -139,118 +130,117 @@ actor PaginatedResultReceiver[A: Any val] be failure(status: U16, response_body: String, message: String) => _p(req.RequestError(status, response_body, message)) -// TODO: Could this be more generic? -class PaginatedJsonRequester +actor LinkedJsonRequester is courier.HTTPClientConnectionActor """ - Issues an HTTP GET request and delivers the JSON response along with Link - headers to a PaginatedResultReceiver for paginated endpoints. + Issues an HTTP GET request and delivers the JSON response along with the + Link header to a LinkedResultReceiver. Used by both paginated list and search + result endpoints. Follows 301/307 redirects automatically. """ + var _http: courier.HTTPClientConnection = courier.HTTPClientConnection.none() + var _collector: courier.ResponseCollector = courier.ResponseCollector let _creds: req.Credentials - let _sslctx: (SSLContext | None) - - new create(creds: req.Credentials) => - _creds = creds - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply[A: Any val](url: String, - receiver: PaginatedResultReceiver[A]) ? - => - let valid_url = URL.valid(url)? - let r = req.RequestFactory("GET", valid_url, _creds.token) - - let handler_factory = - PaginatedJsonRequesterHandlerFactory[A](_creds, receiver) - let client = HTTPClient(_creds.auth, handler_factory, _sslctx) - client(consume r)? - -class PaginatedJsonRequesterHandlerFactory[A: Any val] is HandlerFactory - """ - Creates PaginatedJsonRequesterHandler instances for each HTTP session. - """ - let _creds: req.Credentials - let _receiver: PaginatedResultReceiver[A] - - new val create(creds: req.Credentials, - receiver: PaginatedResultReceiver[A]) - => - _creds = creds - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - let requester = PaginatedJsonRequester(_creds) - PaginatedJsonRequesterHandler[A](requester, _receiver) - -class PaginatedJsonRequesterHandler[A: Any val] is HTTPHandler - """ - Handles the HTTP response for a paginated request, assembling the response - body and extracting Link headers before delivering results to the receiver. - """ - let _requester: PaginatedJsonRequester - let _receiver: PaginatedResultReceiver[A] - var _payload_body: Array[U8] iso = recover Array[U8] end + let _receiver: LinkedResultReceiver + var _request_path: String = "" + var _redirected: Bool = false var _status: U16 = 0 var _link_header: String = "" - new create(requester: PaginatedJsonRequester, - receiver: PaginatedResultReceiver[A]) + new create(creds: req.Credentials, + url: String, + receiver: LinkedResultReceiver) => - _requester = requester + """ + Issues an HTTP GET request expecting a 200 response with JSON body and + Link header for pagination. + """ + _creds = creds _receiver = receiver + _connect(url) + + fun ref _connect(url: String) => + match courier.URL.parse(url) + | let parsed: courier.ParsedURL => + _request_path = parsed.request_path() + let config = courier.ClientConnectionConfig + _http = courier.HTTPClientConnection.ssl( + _creds.auth, req.SSLContextFactory(), parsed.host, parsed.port, + this, config) + | let _: courier.URLParseError => + _fail("Unable to parse URL: " + url) + end - fun ref apply(payload: Payload val) => - _status = payload.status - try - _link_header = payload("link")? + fun ref _http_client_connection(): courier.HTTPClientConnection => + _http + + fun ref on_connected() => + let hdrs = recover trn courier.Headers end + hdrs.set("User-Agent", "Pony GitHub Rest API Client") + hdrs.set("Accept", "application/vnd.github.v3+json") + match _creds.token + | let t: String => + (let n, let v) = courier.BearerAuth(t) + hdrs.set(n, v) + end + let request = courier.HTTPRequest( + courier.GET, + _request_path, + consume hdrs) + _http.send_request(request) + + fun ref on_response(response: courier.Response val) => + _status = response.status + _link_header = match response.headers.get("link") + | let h: String => h + | None => "" end if (_status == 301) or (_status == 307) then - try - // Redirect. - // Let's start a new request to the redirect location - _requester[A](payload("Location")?, _receiver)? + match response.headers.get("location") + | let loc: String => + _redirected = true + _http.close() + LinkedJsonRequester(_creds, loc, _receiver) return end end + _collector = courier.ResponseCollector + _collector.set_response(response) + + fun ref on_body_chunk(data: Array[U8] val) => + _collector.add_chunk(data) + + fun ref on_response_complete() => + if _redirected then return end + _http.close() try - for bs in payload.body()?.values() do - _payload_body.append(bs) + let response = _collector.build()? + if _status == 200 then + match \exhaustive\ courier.ResponseJSON(response) + | let json: JsonValue => + _receiver.success(JsonNav(json), _link_header) + | let _: JsonParseError => + _receiver.failure(_status, "", "Failed to parse response") + end + else + let body_str = String.from_array(response.body) + _receiver.failure(_status, consume body_str, "") end + else + _receiver.failure(0, "", "Failed to build response") end - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => + fun ref on_connection_failure(reason: courier.ConnectionFailureReason) => let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" + | courier.ConnectionFailedDNS => "DNS resolution failed" + | courier.ConnectionFailedTCP => "Unable to connect" + | courier.ConnectionFailedSSL => "SSL handshake failed" end + _receiver.failure(0, "", consume msg) - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - let x = _payload_body = recover Array[U8] end - let y: String iso = String.from_iso_array(consume x) + fun ref on_parse_error(err: courier.ParseError) => + _http.close() + _receiver.failure(0, "", "HTTP parse error") - if _status == 200 then - match \exhaustive\ JsonParser.parse(consume y) - | let json: JsonValue => _receiver.success(JsonNav(json), _link_header) - | let _: JsonParseError => _receiver.failure(_status, "", - "Failed to parse response") - end - elseif (_status != 301) and (_status != 307) then - _receiver.failure(_status, consume y, "") - end + be _fail(message: String) => + _receiver.failure(0, "", message) diff --git a/github_rest_api/pull_request.pony b/github_rest_api/pull_request.pony index 9f40da3..5e976ff 100644 --- a/github_rest_api/pull_request.pony +++ b/github_rest_api/pull_request.pony @@ -1,5 +1,4 @@ use "json" -use "net" use "promises" use req = "request" use ut = "uri/template" @@ -82,15 +81,7 @@ primitive GetPullRequest p, PullRequestJsonConverter) - try - req.JsonRequester(creds)(url, r)? - else - let m = recover val - "Unable to initiate get_pull_request request to" + url - end - p(req.RequestError(where message' = m)) - end - + req.JsonRequester.get(creds, url, r) p primitive PullRequestJsonConverter is req.JsonConverter[PullRequest] diff --git a/github_rest_api/pull_request_file.pony b/github_rest_api/pull_request_file.pony index ed15887..96331ea 100644 --- a/github_rest_api/pull_request_file.pony +++ b/github_rest_api/pull_request_file.pony @@ -1,5 +1,4 @@ use "json" -use "net" use "promises" use req = "request" use ut = "uri/template" @@ -49,16 +48,7 @@ primitive GetPullRequestFiles p, PullRequestFilesJsonConverter) - try - req.JsonRequester(creds)(url, r)? - else - let m = recover val - "Unable to initiate get_files request to" + url - end - - p(req.RequestError(where message' = m)) - end - + req.JsonRequester.get(creds, url, r) p primitive PullRequestFilesJsonConverter is diff --git a/github_rest_api/release.pony b/github_rest_api/release.pony index d40402c..ed38f1b 100644 --- a/github_rest_api/release.pony +++ b/github_rest_api/release.pony @@ -130,17 +130,7 @@ primitive CreateRelease end obj = obj.update("draft", draft).update("prerelease", prerelease) let json = obj.string() - - try - req.HTTPPost(creds.auth)(url, - consume json, - r, - creds.token)? - else - p(req.RequestError( - where message' = "Unable to create release at " + url)) - end - + req.JsonRequester.post(creds, url, consume json, r) p primitive ReleaseJsonConverter is req.JsonConverter[Release] diff --git a/github_rest_api/repository.pony b/github_rest_api/repository.pony index 3fad22e..cc23d7b 100644 --- a/github_rest_api/repository.pony +++ b/github_rest_api/repository.pony @@ -1,5 +1,4 @@ use "json" -use "net" use "promises" use req = "request" use ut = "uri/template" @@ -411,13 +410,7 @@ primitive GetRepository p, RepositoryJsonConverter) - try - req.JsonRequester(creds)(url, r)? - else - let m = "Unable to initiate get_repo request to " + url - p(req.RequestError(where message' = consume m)) - end - + req.JsonRequester.get(creds, url, r) p primitive GetRepositoryLabels @@ -449,13 +442,7 @@ primitive GetRepositoryLabels let p = Promise[(PaginatedList[Label] | req.RequestError)] let r = PaginatedResultReceiver[Label](creds, p, plc) - try - PaginatedJsonRequester(creds).apply[Label](url, r)? - else - let m = "Unable to initiate get_repo request to " + url - p(req.RequestError(where message' = consume m)) - end - + LinkedJsonRequester(creds, url, r) p primitive GetOrganizationRepositories @@ -484,13 +471,7 @@ primitive GetOrganizationRepositories let p = Promise[(PaginatedList[Repository] | req.RequestError)] let r = PaginatedResultReceiver[Repository](creds, p, plc) - try - PaginatedJsonRequester(creds).apply[Repository](url, r)? - else - let m = "Unable to initiate get_org_repos request to " + url - p(req.RequestError(where message' = consume m)) - end - + LinkedJsonRequester(creds, url, r) p primitive RepositoryJsonConverter is req.JsonConverter[Repository] diff --git a/github_rest_api/request/_ssl.pony b/github_rest_api/request/_ssl.pony new file mode 100644 index 0000000..664c5f2 --- /dev/null +++ b/github_rest_api/request/_ssl.pony @@ -0,0 +1,20 @@ +use ssl = "ssl/net" + +primitive SSLContextFactory + """ + Creates an SSL context for HTTPS client connections. Attempts to load the + system CA store for certificate verification. If no CA store is available, + falls back to an unverified context. + """ + fun apply(): ssl.SSLContext val => + try + recover val + ssl.SSLContext + .>set_client_verify(true) + .>set_authority(None)? + end + else + recover val + ssl.SSLContext.>set_client_verify(false) + end + end diff --git a/github_rest_api/request/check_requester.pony b/github_rest_api/request/check_requester.pony new file mode 100644 index 0000000..2c1518f --- /dev/null +++ b/github_rest_api/request/check_requester.pony @@ -0,0 +1,123 @@ +use courier = "courier" +use "promises" + +interface tag CheckResultReceiver + """ + Receives the result of an HTTP status check where 204 means true and 404 + means false. Used for endpoints like "is this gist starred?" that indicate + their answer via status code rather than a response body. + """ + be success(value: Bool) + be failure(status: U16, response_body: String, message: String) + +type BoolOrError is (Bool | RequestError) + +actor BoolResultReceiver + """ + Bridges a CheckResultReceiver to a Promise[BoolOrError], fulfilling the + promise with true, false, or a RequestError. + """ + let _p: Promise[BoolOrError] + + new create(p: Promise[BoolOrError]) => + _p = p + + be success(value: Bool) => + _p(value) + + be failure(status: U16, response_body: String, message: String) => + _p(RequestError(status, response_body, message)) + +actor CheckRequester is courier.HTTPClientConnectionActor + """ + Issues an HTTP GET request and interprets the status code as a boolean: 204 + means true, 404 means false, and any other status is treated as a failure. + Used for GitHub API endpoints that answer yes/no questions via status codes + (e.g., checking whether a gist is starred). + """ + var _http: courier.HTTPClientConnection = courier.HTTPClientConnection.none() + var _collector: courier.ResponseCollector = courier.ResponseCollector + let _creds: Credentials + let _receiver: CheckResultReceiver + var _request_path: String = "" + var _status: U16 = 0 + + new create(creds: Credentials, + url: String, + receiver: CheckResultReceiver) + => + """ + Issues an HTTP GET request interpreting 204 as true and 404 as false. + """ + _creds = creds + _receiver = receiver + _connect(url) + + fun ref _connect(url: String) => + match courier.URL.parse(url) + | let parsed: courier.ParsedURL => + _request_path = parsed.request_path() + let config = courier.ClientConnectionConfig + _http = courier.HTTPClientConnection.ssl( + _creds.auth, SSLContextFactory(), parsed.host, parsed.port, + this, config) + | let _: courier.URLParseError => + _fail("Unable to parse URL: " + url) + end + + fun ref _http_client_connection(): courier.HTTPClientConnection => + _http + + fun ref on_connected() => + let hdrs = recover trn courier.Headers end + hdrs.set("User-Agent", "Pony GitHub Rest API Client") + hdrs.set("Accept", "application/vnd.github.v3+json") + match _creds.token + | let t: String => + (let n, let v) = courier.BearerAuth(t) + hdrs.set(n, v) + end + let request = courier.HTTPRequest( + courier.GET, + _request_path, + consume hdrs) + _http.send_request(request) + + fun ref on_response(response: courier.Response val) => + _status = response.status + _collector = courier.ResponseCollector + _collector.set_response(response) + + fun ref on_body_chunk(data: Array[U8] val) => + _collector.add_chunk(data) + + fun ref on_response_complete() => + _http.close() + if _status == 204 then + _receiver.success(true) + elseif _status == 404 then + _receiver.success(false) + else + try + let response = _collector.build()? + let body_str = String.from_array(response.body) + _receiver.failure(_status, consume body_str, "") + else + _receiver.failure(_status, "", "") + end + end + + fun ref on_connection_failure(reason: courier.ConnectionFailureReason) => + let msg = match \exhaustive\ reason + | courier.ConnectionFailedDNS => "DNS resolution failed" + | courier.ConnectionFailedTCP => "Unable to connect" + | courier.ConnectionFailedSSL => "SSL handshake failed" + end + _receiver.failure(0, "", consume msg) + + fun ref on_parse_error(err: courier.ParseError) => + _http.close() + _receiver.failure(0, "", "HTTP parse error") + + be _fail(message: String) => + _receiver.failure(0, "", message) diff --git a/github_rest_api/request/http.pony b/github_rest_api/request/credentials.pony similarity index 55% rename from github_rest_api/request/http.pony rename to github_rest_api/request/credentials.pony index 5bc54f3..30c205a 100644 --- a/github_rest_api/request/http.pony +++ b/github_rest_api/request/credentials.pony @@ -1,17 +1,25 @@ -use "http" use "json" -use "net" +use lori = "lori" use "promises" class val Credentials - let auth: TCPConnectAuth + """ + Holds authentication context for GitHub API requests: a TCP connection + authority and an optional personal access token. + """ + let auth: lori.TCPConnectAuth let token: (String | None) - new val create(auth': TCPConnectAuth, token': (String | None) = None) => + new val create(auth': lori.TCPConnectAuth, token': (String | None) = None) => auth = auth' token = token' actor ResultReceiver[A: Any val] + """ + Generic receiver that converts a JSON response into a model type via a + JsonConverter and fulfills the associated Promise with the result or a + RequestError. + """ let _creds: Credentials let _p: Promise[(A | RequestError)] let _converter: JsonConverter[A] @@ -37,18 +45,3 @@ actor ResultReceiver[A: Any val] be failure(status: U16, response_body: String, message: String) => _p(RequestError(status, response_body, message)) - -primitive RequestFactory - fun apply(method: String, - url: URL, - auth_token: (String | None) = None): Payload iso^ - => - let r = Payload.request(method, url) - // we get a 403 from GitHub if the user-agent header isn't supplied - r("User-Agent") = "Pony GitHub Rest API Client" - r("Accept") = "application/vnd.github.v3+json" - match auth_token - | let token: String => - r("Authorization") = recover val "token " + token end - end - consume r diff --git a/github_rest_api/request/http_check.pony b/github_rest_api/request/http_check.pony deleted file mode 100644 index 090c5db..0000000 --- a/github_rest_api/request/http_check.pony +++ /dev/null @@ -1,117 +0,0 @@ -use "http" -use "net" -use "ssl/net" -use "promises" - -interface tag CheckResultReceiver - """ - Receives the result of an HTTP status check where 204 means true and 404 - means false. Used for endpoints like "is this gist starred?" that indicate - their answer via status code rather than a response body. - """ - be success(value: Bool) - be failure(status: U16, response_body: String, message: String) - -type BoolOrError is (Bool | RequestError) - -actor BoolResultReceiver - """ - Bridges a CheckResultReceiver to a Promise[BoolOrError], fulfilling the - promise with true, false, or a RequestError. - """ - let _p: Promise[BoolOrError] - - new create(p: Promise[BoolOrError]) => - _p = p - - be success(value: Bool) => - _p(value) - - be failure(status: U16, response_body: String, message: String) => - _p(RequestError(status, response_body, message)) - -class HTTPCheck - """ - Sends an HTTP GET request and interprets the status code as a boolean: 204 - means true, 404 means false, and any other status is treated as a failure. - Used for GitHub API endpoints that answer yes/no questions via status codes - (e.g., checking whether a gist is starred). - """ - let _auth: TCPConnectAuth - let _sslctx: (SSLContext | None) - - new create(auth: TCPConnectAuth) => - _auth = auth - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply(url: String, - receiver: CheckResultReceiver, - auth_token: (String | None) = None) ? - => - let valid_url = URL.valid(url)? - let r = RequestFactory("GET", valid_url, auth_token) - - let handler_factory = HTTPCheckHandlerFactory(receiver) - let client = HTTPClient(_auth, handler_factory, _sslctx) - client(consume r)? - -class HTTPCheckHandlerFactory is HandlerFactory - let _receiver: CheckResultReceiver - - new val create(receiver: CheckResultReceiver) => - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - HTTPCheckHandler(_receiver) - -class HTTPCheckHandler is HTTPHandler - let _receiver: CheckResultReceiver - var _payload_body: Array[U8] iso = recover Array[U8] end - var _status: U16 = 0 - - new create(receiver: CheckResultReceiver) => - _receiver = receiver - - fun ref apply(payload: Payload val) => - _status = payload.status - - try - for bs in payload.body()?.values() do - _payload_body.append(bs) - end - end - - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => - let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" - end - - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - if _status == 204 then - _receiver.success(true) - elseif _status == 404 then - _receiver.success(false) - else - let x = _payload_body = recover Array[U8] end - let y = String.from_iso_array(consume x) - - _receiver.failure(_status, consume y, "") - end diff --git a/github_rest_api/request/http_delete.pony b/github_rest_api/request/http_delete.pony deleted file mode 100644 index e06be60..0000000 --- a/github_rest_api/request/http_delete.pony +++ /dev/null @@ -1,102 +0,0 @@ -use "http" -use "net" -use "ssl/net" -use "promises" - -interface tag DeleteResultReceiver - be success() - be failure(status: U16, response_body: String, message: String) - -class HTTPDelete - let _auth: TCPConnectAuth - let _sslctx: (SSLContext | None) - - new create(auth: TCPConnectAuth) => - _auth = auth - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply(url: String, - receiver: DeleteResultReceiver, - auth_token: (String | None) = None) ? - => - let valid_url = URL.valid(url)? - let r = RequestFactory("DELETE", valid_url, auth_token) - - let handler_factory = HTTPDeleteHandlerFactory(receiver) - let client = HTTPClient(_auth, handler_factory, _sslctx) - client(consume r)? - -class HTTPDeleteHandlerFactory is HandlerFactory - let _receiver: DeleteResultReceiver - - new val create(receiver: DeleteResultReceiver) => - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - HTTPDeleteHandler(_receiver) - -class HTTPDeleteHandler is HTTPHandler - let _receiver: DeleteResultReceiver - var _payload_body: Array[U8] iso = recover Array[U8] end - var _status: U16 = 0 - - new create(receiver: DeleteResultReceiver) => - _receiver = receiver - - fun ref apply(payload: Payload val) => - _status = payload.status - - try - for bs in payload.body()?.values() do - _payload_body.append(bs) - end - end - - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => - let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" - end - - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - if _status == 204 then - _receiver.success() - else - let x = _payload_body = recover Array[U8] end - let y = String.from_iso_array(consume x) - - _receiver.failure(_status, consume y, "") - end - -type DeletedOrError is (Deleted | RequestError) - -actor DeletedResultReceiver - let _p: Promise[DeletedOrError] - - new create(p: Promise[DeletedOrError]) => - _p = p - - be success() => - _p(Deleted) - - be failure(status: U16, response_body: String, message: String) => - _p(RequestError(status, response_body, message)) - -primitive Deleted diff --git a/github_rest_api/request/http_get.pony b/github_rest_api/request/http_get.pony deleted file mode 100644 index e07e976..0000000 --- a/github_rest_api/request/http_get.pony +++ /dev/null @@ -1,106 +0,0 @@ -use "http" -use "json" -use "ssl/net" -use "promises" - -class JsonRequester - let _creds: Credentials - let _sslctx: (SSLContext | None) - - new create(creds: Credentials) => - _creds = creds - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply(url: String, - receiver: JsonRequesterResultReceiver) ? - => - let valid_url = URL.valid(url)? - let r = RequestFactory("GET", valid_url, _creds.token) - - let handler_factory = JsonRequesterHandlerFactory(_creds, receiver) - let client = HTTPClient(_creds.auth, handler_factory, _sslctx) - client(consume r)? - -interface tag JsonRequesterResultReceiver - be success(json: JsonNav) - be failure(status: U16, response_body: String, message: String) - -class JsonRequesterHandlerFactory is HandlerFactory - let _creds: Credentials - let _receiver: JsonRequesterResultReceiver - - new val create(creds: Credentials, - receiver: JsonRequesterResultReceiver) - => - _creds = creds - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - let requester = JsonRequester(_creds) - JsonRequesterHandler(requester, _receiver) - -class JsonRequesterHandler is HTTPHandler - let _requester: JsonRequester - let _receiver: JsonRequesterResultReceiver - var _payload_body: Array[U8] iso = recover Array[U8] end - var _status: U16 = 0 - - new create(requester: JsonRequester, receiver: JsonRequesterResultReceiver) => - _requester = requester - _receiver = receiver - - fun ref apply(payload: Payload val) => - _status = payload.status - - - if (_status == 301) or (_status == 307) then - try - // Redirect. - // Let's start a new request to the redirect location - _requester(payload("Location")?, _receiver)? - return - end - end - - try - for bs in payload.body()?.values() do - _payload_body.append(bs) - end - end - - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => - let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" - end - - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - let x = _payload_body = recover Array[U8] end - let y: String iso = String.from_iso_array(consume x) - - if _status == 200 then - match \exhaustive\ JsonParser.parse(consume y) - | let json: JsonValue => _receiver.success(JsonNav(json)) - | let _: JsonParseError => _receiver.failure(_status, "", - "Failed to parse response") - end - elseif (_status != 301) and (_status != 307) then - _receiver.failure(_status, consume y, "") - end diff --git a/github_rest_api/request/http_patch.pony b/github_rest_api/request/http_patch.pony deleted file mode 100644 index 9b53695..0000000 --- a/github_rest_api/request/http_patch.pony +++ /dev/null @@ -1,92 +0,0 @@ -use "http" -use "json" -use "net" -use "ssl/net" - -class HTTPPatch - """ - Sends an HTTP PATCH request with a JSON body and expects a 200 response - containing JSON. Used for updating existing resources in the GitHub API. - """ - let _auth: TCPConnectAuth - let _sslctx: (SSLContext | None) - - new create(auth: TCPConnectAuth) => - _auth = auth - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply(url: String, - body: String, - receiver: PostResultReceiver, - auth_token: (String | None) = None) ? - => - let valid_url = URL.valid(url)? - let r = RequestFactory("PATCH", valid_url, auth_token) - r.add_chunk(body) - - let handler_factory = HTTPPatchHandlerFactory(receiver) - let client = HTTPClient(_auth, handler_factory, _sslctx) - client(consume r)? - -class HTTPPatchHandlerFactory is HandlerFactory - let _receiver: PostResultReceiver - - new val create(receiver: PostResultReceiver) => - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - HTTPPatchHandler(_receiver) - -class HTTPPatchHandler is HTTPHandler - let _receiver: PostResultReceiver - var _payload_body: Array[U8] iso = recover Array[U8] end - var _status: U16 = 0 - - new create(receiver: PostResultReceiver) => - _receiver = receiver - - fun ref apply(payload: Payload val) => - _status = payload.status - - try - for bs in payload.body()?.values() do - _payload_body.append(bs) - end - end - - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => - let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" - end - - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - let x = _payload_body = recover Array[U8] end - let y = String.from_iso_array(consume x) - - if _status == 200 then - match \exhaustive\ JsonParser.parse(consume y) - | let json: JsonValue => _receiver.success(JsonNav(json)) - | let _: JsonParseError => _receiver.failure(_status, "", - "Failed to parse response") - end - else - _receiver.failure(_status, consume y, "") - end diff --git a/github_rest_api/request/http_post.pony b/github_rest_api/request/http_post.pony deleted file mode 100644 index 4011420..0000000 --- a/github_rest_api/request/http_post.pony +++ /dev/null @@ -1,92 +0,0 @@ -use "http" -use "json" -use "net" -use "ssl/net" - -interface tag PostResultReceiver - be success(json: JsonNav) - be failure(status: U16, response_body: String, message: String) - -class HTTPPost - let _auth: TCPConnectAuth - let _sslctx: (SSLContext | None) - - new create(auth: TCPConnectAuth) => - _auth = auth - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply(url: String, - body: String, - receiver: PostResultReceiver, - auth_token: (String | None) = None) ? - => - let valid_url = URL.valid(url)? - let r = RequestFactory("POST", valid_url, auth_token) - r.add_chunk(body) - - let handler_factory = HTTPPostHandlerFactory(receiver) - let client = HTTPClient(_auth, handler_factory, _sslctx) - client(consume r)? - -class HTTPPostHandlerFactory is HandlerFactory - let _receiver: PostResultReceiver - - new val create(receiver: PostResultReceiver) => - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - HTTPPostHandler(_receiver) - -class HTTPPostHandler is HTTPHandler - let _receiver: PostResultReceiver - var _payload_body: Array[U8] iso = recover Array[U8] end - var _status: U16 = 0 - - new create(receiver: PostResultReceiver) => - _receiver = receiver - - fun ref apply(payload: Payload val) => - _status = payload.status - - try - for bs in payload.body()?.values() do - _payload_body.append(bs) - end - end - - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => - let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" - end - - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - let x = _payload_body = recover Array[U8] end - let y = String.from_iso_array(consume x) - - if _status == 201 then - match \exhaustive\ JsonParser.parse(consume y) - | let json: JsonValue => _receiver.success(JsonNav(json)) - | let _: JsonParseError => _receiver.failure(_status, "", - "Failed to parse response") - end - else - _receiver.failure(_status, consume y, "") - end diff --git a/github_rest_api/request/http_put.pony b/github_rest_api/request/http_put.pony deleted file mode 100644 index 3ca79e7..0000000 --- a/github_rest_api/request/http_put.pony +++ /dev/null @@ -1,87 +0,0 @@ -use "http" -use "net" -use "ssl/net" - -class HTTPPut - """ - Sends an HTTP PUT request with no body and expects a 204 response. Used for - actions like starring a gist where the request carries no payload and success - is indicated by 204 No Content. - """ - let _auth: TCPConnectAuth - let _sslctx: (SSLContext | None) - - new create(auth: TCPConnectAuth) => - _auth = auth - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply(url: String, - receiver: DeleteResultReceiver, - auth_token: (String | None) = None) ? - => - let valid_url = URL.valid(url)? - let r = RequestFactory("PUT", valid_url, auth_token) - r("Content-Length") = "0" - - let handler_factory = HTTPPutHandlerFactory(receiver) - let client = HTTPClient(_auth, handler_factory, _sslctx) - client(consume r)? - -class HTTPPutHandlerFactory is HandlerFactory - let _receiver: DeleteResultReceiver - - new val create(receiver: DeleteResultReceiver) => - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - HTTPPutHandler(_receiver) - -class HTTPPutHandler is HTTPHandler - let _receiver: DeleteResultReceiver - var _payload_body: Array[U8] iso = recover Array[U8] end - var _status: U16 = 0 - - new create(receiver: DeleteResultReceiver) => - _receiver = receiver - - fun ref apply(payload: Payload val) => - _status = payload.status - - try - for bs in payload.body()?.values() do - _payload_body.append(bs) - end - end - - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => - let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" - end - - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - if _status == 204 then - _receiver.success() - else - let x = _payload_body = recover Array[U8] end - let y = String.from_iso_array(consume x) - - _receiver.failure(_status, consume y, "") - end diff --git a/github_rest_api/request/json.pony b/github_rest_api/request/json.pony index 1d00d94..a98781d 100644 --- a/github_rest_api/request/json.pony +++ b/github_rest_api/request/json.pony @@ -1,5 +1,4 @@ use "json" -use "net" interface val JsonConverter[A: Any #share] fun apply(json: JsonNav, creds: Credentials): A ? diff --git a/github_rest_api/request/json_requester.pony b/github_rest_api/request/json_requester.pony new file mode 100644 index 0000000..0553533 --- /dev/null +++ b/github_rest_api/request/json_requester.pony @@ -0,0 +1,165 @@ +use courier = "courier" +use "json" + +interface tag JsonRequesterResultReceiver + """ + Receives the result of a JSON API request: either a parsed JSON response + on success, or status/body/message details on failure. + """ + be success(json: JsonNav) + be failure(status: U16, response_body: String, message: String) + +actor JsonRequester is courier.HTTPClientConnectionActor + """ + Issues an HTTP request that expects a JSON response. Supports GET (200), + POST (201), and PATCH (200) methods. GET requests follow 301/307 redirects + automatically. On success, the response body is parsed as JSON and delivered + to the receiver; on failure, the receiver gets the status code, raw response + body, and an error message. + """ + var _http: courier.HTTPClientConnection = courier.HTTPClientConnection.none() + var _collector: courier.ResponseCollector = courier.ResponseCollector + let _creds: Credentials + let _receiver: JsonRequesterResultReceiver + let _method: courier.Method + let _expected_status: U16 + let _body: (String | None) + var _request_path: String = "" + var _redirected: Bool = false + var _status: U16 = 0 + + new get(creds: Credentials, + url: String, + receiver: JsonRequesterResultReceiver) + => + """ + Issues an HTTP GET request expecting a 200 response with a JSON body. + """ + _creds = creds + _receiver = receiver + _method = courier.GET + _expected_status = 200 + _body = None + _connect(url) + + new post(creds: Credentials, + url: String, + body: String, + receiver: JsonRequesterResultReceiver) + => + """ + Issues an HTTP POST request expecting a 201 response with a JSON body. + """ + _creds = creds + _receiver = receiver + _method = courier.POST + _expected_status = 201 + _body = body + _connect(url) + + new patch(creds: Credentials, + url: String, + body: String, + receiver: JsonRequesterResultReceiver) + => + """ + Issues an HTTP PATCH request expecting a 200 response with a JSON body. + """ + _creds = creds + _receiver = receiver + _method = courier.PATCH + _expected_status = 200 + _body = body + _connect(url) + + fun ref _connect(url: String) => + match courier.URL.parse(url) + | let parsed: courier.ParsedURL => + _request_path = parsed.request_path() + let config = courier.ClientConnectionConfig + _http = courier.HTTPClientConnection.ssl( + _creds.auth, SSLContextFactory(), parsed.host, parsed.port, + this, config) + | let _: courier.URLParseError => + _fail("Unable to parse URL: " + url) + end + + fun ref _http_client_connection(): courier.HTTPClientConnection => + _http + + fun ref on_connected() => + let hdrs = recover trn courier.Headers end + hdrs.set("User-Agent", "Pony GitHub Rest API Client") + hdrs.set("Accept", "application/vnd.github.v3+json") + match _creds.token + | let t: String => + (let n, let v) = courier.BearerAuth(t) + hdrs.set(n, v) + end + match _body + | let b: String => + hdrs.set("Content-Length", b.size().string()) + end + let request = courier.HTTPRequest( + _method, + _request_path, + consume hdrs, + match _body + | let b: String => b.array() + | None => None + end) + _http.send_request(request) + + fun ref on_response(response: courier.Response val) => + _status = response.status + if (_method is courier.GET) + and ((_status == 301) or (_status == 307)) + then + match response.headers.get("location") + | let loc: String => + _redirected = true + _http.close() + JsonRequester.get(_creds, loc, _receiver) + return + end + end + _collector = courier.ResponseCollector + _collector.set_response(response) + + fun ref on_body_chunk(data: Array[U8] val) => + _collector.add_chunk(data) + + fun ref on_response_complete() => + if _redirected then return end + _http.close() + try + let response = _collector.build()? + if _status == _expected_status then + match \exhaustive\ courier.ResponseJSON(response) + | let json: JsonValue => + _receiver.success(JsonNav(json)) + | let _: JsonParseError => + _receiver.failure(_status, "", "Failed to parse response") + end + else + let body_str = String.from_array(response.body) + _receiver.failure(_status, consume body_str, "") + end + else + _receiver.failure(0, "", "Failed to build response") + end + + fun ref on_connection_failure(reason: courier.ConnectionFailureReason) => + let msg = match \exhaustive\ reason + | courier.ConnectionFailedDNS => "DNS resolution failed" + | courier.ConnectionFailedTCP => "Unable to connect" + | courier.ConnectionFailedSSL => "SSL handshake failed" + end + _receiver.failure(0, "", consume msg) + + fun ref on_parse_error(err: courier.ParseError) => + _http.close() + _receiver.failure(0, "", "HTTP parse error") + + be _fail(message: String) => + _receiver.failure(0, "", message) diff --git a/github_rest_api/request/no_content_requester.pony b/github_rest_api/request/no_content_requester.pony new file mode 100644 index 0000000..442456b --- /dev/null +++ b/github_rest_api/request/no_content_requester.pony @@ -0,0 +1,141 @@ +use courier = "courier" +use "promises" + +interface tag DeleteResultReceiver + """ + Receives the result of an HTTP request that expects no response body (204 No + Content). Used for DELETE and PUT operations like deleting a label or starring + a gist. + """ + be success() + be failure(status: U16, response_body: String, message: String) + +type DeletedOrError is (Deleted | RequestError) + +actor DeletedResultReceiver + """ + Bridges a DeleteResultReceiver to a Promise[DeletedOrError], fulfilling the + promise with Deleted on success or a RequestError on failure. + """ + let _p: Promise[DeletedOrError] + + new create(p: Promise[DeletedOrError]) => + _p = p + + be success() => + _p(Deleted) + + be failure(status: U16, response_body: String, message: String) => + _p(RequestError(status, response_body, message)) + +primitive Deleted + """ + Marker type indicating a successful deletion or no-content operation. + """ + +actor NoContentRequester is courier.HTTPClientConnectionActor + """ + Issues an HTTP request that expects a 204 No Content response. Supports + DELETE and PUT methods. On success, calls `receiver.success()`; on any other + status or connection failure, calls `receiver.failure()` with details. + """ + var _http: courier.HTTPClientConnection = courier.HTTPClientConnection.none() + var _collector: courier.ResponseCollector = courier.ResponseCollector + let _creds: Credentials + let _receiver: DeleteResultReceiver + let _method: courier.Method + var _request_path: String = "" + var _status: U16 = 0 + + new delete(creds: Credentials, + url: String, + receiver: DeleteResultReceiver) + => + """ + Issues an HTTP DELETE request expecting a 204 response. + """ + _creds = creds + _receiver = receiver + _method = courier.DELETE + _connect(url) + + new put(creds: Credentials, + url: String, + receiver: DeleteResultReceiver) + => + """ + Issues an HTTP PUT request with no body, expecting a 204 response. Used for + operations like starring a gist. + """ + _creds = creds + _receiver = receiver + _method = courier.PUT + _connect(url) + + fun ref _connect(url: String) => + match courier.URL.parse(url) + | let parsed: courier.ParsedURL => + _request_path = parsed.request_path() + let config = courier.ClientConnectionConfig + _http = courier.HTTPClientConnection.ssl( + _creds.auth, SSLContextFactory(), parsed.host, parsed.port, + this, config) + | let _: courier.URLParseError => + _fail("Unable to parse URL: " + url) + end + + fun ref _http_client_connection(): courier.HTTPClientConnection => + _http + + fun ref on_connected() => + let hdrs = recover trn courier.Headers end + hdrs.set("User-Agent", "Pony GitHub Rest API Client") + hdrs.set("Accept", "application/vnd.github.v3+json") + match _creds.token + | let t: String => + (let n, let v) = courier.BearerAuth(t) + hdrs.set(n, v) + end + hdrs.set("Content-Length", "0") + let request = courier.HTTPRequest( + _method, + _request_path, + consume hdrs) + _http.send_request(request) + + fun ref on_response(response: courier.Response val) => + _status = response.status + _collector = courier.ResponseCollector + _collector.set_response(response) + + fun ref on_body_chunk(data: Array[U8] val) => + _collector.add_chunk(data) + + fun ref on_response_complete() => + _http.close() + if _status == 204 then + _receiver.success() + else + try + let response = _collector.build()? + let body_str = String.from_array(response.body) + _receiver.failure(_status, consume body_str, "") + else + _receiver.failure(_status, "", "") + end + end + + fun ref on_connection_failure(reason: courier.ConnectionFailureReason) => + let msg = match \exhaustive\ reason + | courier.ConnectionFailedDNS => "DNS resolution failed" + | courier.ConnectionFailedTCP => "Unable to connect" + | courier.ConnectionFailedSSL => "SSL handshake failed" + end + _receiver.failure(0, "", consume msg) + + fun ref on_parse_error(err: courier.ParseError) => + _http.close() + _receiver.failure(0, "", "HTTP parse error") + + be _fail(message: String) => + _receiver.failure(0, "", message) diff --git a/github_rest_api/request/query_params.pony b/github_rest_api/request/query_params.pony index 0f0df0e..5a1a432 100644 --- a/github_rest_api/request/query_params.pony +++ b/github_rest_api/request/query_params.pony @@ -6,8 +6,8 @@ primitive QueryParams Both keys and values are encoded: only unreserved characters (A-Z, a-z, 0-9, `-`, `.`, `_`, `~`) pass through; everything else becomes `%XX`. - This includes characters like `&` and `=` that the http library's - `URLEncode` with `URLPartQuery` would leave unencoded. + This includes characters like `&` and `=` that some URL encoding + implementations would leave unencoded in query position. ```pony let params = recover val diff --git a/github_rest_api/search.pony b/github_rest_api/search.pony index 88f2c87..03e7774 100644 --- a/github_rest_api/search.pony +++ b/github_rest_api/search.pony @@ -1,6 +1,4 @@ -use "http" use "json" -use "ssl/net" use "promises" use req = "request" @@ -17,18 +15,13 @@ primitive SearchIssues let sc = PaginatedSearchJsonConverter[Issue](creds, IssueJsonConverter) let r = SearchResultReceiver[Issue](creds, p, sc) - try - let eq = URLEncode.encode(query, URLPartQuery)? - let url = recover val - "https://api.github.com/search/issues?q=" + eq - end - - SearchJsonRequester(creds).apply[Issue](url, r)? - else - let m = "Unable to initiate issue search request for '" + query + "'" - p(req.RequestError(where message' = consume m)) + let url = recover val + "https://api.github.com/search/issues" + + req.QueryParams(recover val [("q", query)] end) end + LinkedJsonRequester(creds, url, r) + p class val SearchResults[A: Any val] @@ -89,13 +82,7 @@ class val SearchResults[A: Any val] => let p = Promise[(SearchResults[A] | req.RequestError)] let r = SearchResultReceiver[A](_creds, p, _converter) - - try - SearchJsonRequester(_creds).apply[A](link, r)? - else - let m = "Unable to get " + link - p(req.RequestError(where message' = consume m)) - end + LinkedJsonRequester(_creds, link, r) p class val PaginatedSearchJsonConverter[A: Any val] @@ -163,118 +150,3 @@ actor SearchResultReceiver[A: Any val] be failure(status: U16, response_body: String, message: String) => _p(req.RequestError(status, response_body, message)) - -class SearchJsonRequester - """ - Issues an HTTP GET request and delivers the JSON response along with Link - headers to a SearchResultReceiver for search endpoints. - """ - let _creds: req.Credentials - let _sslctx: (SSLContext | None) - - new create(creds: req.Credentials) => - _creds = creds - - _sslctx = try - recover val - SSLContext.>set_client_verify(true).>set_authority(None)? - end - else - None - end - - fun ref apply[A: Any val](url: String, - receiver: SearchResultReceiver[A]) ? - => - let valid_url = URL.valid(url)? - let r = req.RequestFactory("GET", valid_url, _creds.token) - - let handler_factory = - SearchJsonRequesterHandlerFactory[A](_creds, receiver) - let client = HTTPClient(_creds.auth, handler_factory, _sslctx) - client(consume r)? - -class SearchJsonRequesterHandlerFactory[A: Any val] is HandlerFactory - """ - Creates SearchJsonRequesterHandler instances for each HTTP session. - """ - let _creds: req.Credentials - let _receiver: SearchResultReceiver[A] - - new val create(creds: req.Credentials, - receiver: SearchResultReceiver[A]) - => - _creds = creds - _receiver = receiver - - fun apply(session: HTTPSession tag): HTTPHandler ref^ => - let requester = SearchJsonRequester(_creds) - SearchJsonRequesterHandler[A](requester, _receiver) - -class SearchJsonRequesterHandler[A: Any val] is HTTPHandler - """ - Handles the HTTP response for a search request, assembling the response body - and extracting Link headers before delivering results to the receiver. - """ - let _requester: SearchJsonRequester - let _receiver: SearchResultReceiver[A] - var _payload_body: Array[U8] iso = recover Array[U8] end - var _status: U16 = 0 - var _link_header: String = "" - - new create(requester: SearchJsonRequester, - receiver: SearchResultReceiver[A]) - => - _requester = requester - _receiver = receiver - - fun ref apply(payload: Payload val) => - _status = payload.status - try - _link_header = payload("link")? - end - - if (_status == 301) or (_status == 307) then - try - // Redirect. - // Let's start a new request to the redirect location - _requester[A](payload("Location")?, _receiver)? - return - end - end - - try - for bs in payload.body()?.values() do - _payload_body.append(bs) - end - end - - if payload.transfer_mode is OneshotTransfer then - finished() - end - - fun ref chunk(data: ByteSeq) => - _payload_body.append(data) - - fun ref failed(reason: HTTPFailureReason) => - let msg = match \exhaustive\ reason - | AuthFailed => "Authorization failure" - | ConnectFailed => "Unable to connect" - | ConnectionClosed => "Connection was prematurely closed" - end - - _receiver.failure(_status, "", consume msg) - - fun ref finished() => - let x = _payload_body = recover Array[U8] end - let y: String iso = String.from_iso_array(consume x) - - if _status == 200 then - match \exhaustive\ JsonParser.parse(consume y) - | let json: JsonValue => _receiver.success(JsonNav(json), _link_header) - | let _: JsonParseError => _receiver.failure(_status, "", - "Failed to parse response") - end - elseif (_status != 301) and (_status != 307) then - _receiver.failure(_status, consume y, "") - end