diff --git a/AGENTS.md b/AGENTS.md index 6ed2277..cc131a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # github_rest_api -A Pony library for interacting with the GitHub REST API. Provides typed models, HTTP request infrastructure, and Promise-based async results. +A Pony library for the GitHub REST API: typed models, HTTP request infrastructure, and Promise-based async results. ## Contributing with an AI assistant @@ -24,439 +24,27 @@ When you start working on this project, load the `pony-skills` skill — it tell Read [CONTRIBUTING.md](CONTRIBUTING.md). -## Building and Testing +## Building and testing ``` -make ssl=3.0.x # build + run unit tests + build examples -make unit-tests ssl=3.0.x # unit tests only -make test-one t=TestName ssl=3.0.x # run a single test by name -make examples ssl=3.0.x # build examples only -make clean # clean build artifacts + corral deps -make config=debug ssl=3.0.x # debug build +make ssl=3.0.x # build + run unit tests + build examples +make unit-tests ssl=3.0.x # unit tests only +make test-one t=TestName ssl=3.0.x # run a single test by name +make examples ssl=3.0.x # build examples only +make config=debug ssl=3.0.x # debug build +make clean # clean build artifacts + corral deps ``` -**SSL version is required** for all build/test targets. This machine has OpenSSL 3.x, so use `ssl=3.0.x`. - -Uses `corral` for dependency management. `make` automatically runs `corral fetch` before compiling. - -## Dependencies - -- `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/uri.git` -- RFC 6570 URI template expansion - -## Source Layout - -``` -assets/ -- Test certificates (self-signed cert.pem + key.pem from lori) -github_rest_api/ - github.pony -- GitHub class (entry point, has get_repo, get_org_repos, gist operations) - repository.pony -- Repository model + GetRepository, GetRepositoryLabels - issue.pony -- Issue model + GetIssue, GetRepositoryIssues, IssueSort, SortDirection - issue_pull_request.pony -- IssuePullRequest model (PR metadata on issues) - pull_request.pony -- PullRequest model + GetPullRequest - pull_request_base.pony -- PullRequestBase model (head/base refs) - pull_request_file.pony -- PullRequestFile model + GetPullRequestFiles - commit.pony -- Commit model + GetCommit - commit_file.pony -- CommitFile model (sha, status, filename) - git_commit.pony -- GitCommit model (author, committer, message) - git_person.pony -- GitPerson model (name, email) - release.pony -- Release model + CreateRelease - asset.pony -- Asset model (release assets) - label.pony -- Label model + CreateLabel, DeleteLabel - issue_comment.pony -- IssueComment model + CreateIssueComment, GetIssueComments - gist.pony -- Gist model + 15 gist operations (CRUD, lists, forks, commits, star) - gist_file.pony -- GistFile model (file within a gist) - gist_file_update.pony -- GistFileEdit, GistFileRename, GistFileDelete for update operations - gist_commit.pony -- GistCommit + GistChangeStatus models - gist_comment.pony -- GistComment model + 5 comment operations (CRUD + list) - search.pony -- SearchIssues + SearchResults generic - 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, LinkedJsonRequester, LinkedResultReceiver - _extract_pagination_links.pony -- Extracts prev/next URLs from Link headers (via web_link) - _test_json_converters.pony -- Property-based tests for JSON converter primitives - _test_result_receivers.pony -- Async tests for result receiver actors - _test_mock_http_server.pony -- Mock HTTPS server infrastructure for request actor tests - _test_request_actors.pony -- Integration tests for all four request actor types - _test_search_and_pagination.pony -- Converter unit tests + mock HTTP pagination behavior tests - request/ -- HTTP request infrastructure (temporary home, intended to be extracted to its own library) - credentials.pony -- Credentials (lori.TCPConnectAuth + token + optional ssl_ctx), ResultReceiver - _ssl.pony -- SSLContextFactory (shared SSL context creation, fallback when Credentials.ssl_ctx is None) - 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) - _test.pony -- QueryParams tests (example + property-based) - _test.pony -- Test runner (pagination link extraction + delegates to subpackage tests) -``` +`ssl=` is required on every build and test target, set to your installed TLS library: `3.0.x`, `1.1.x`, or `0.9.0`. `make` runs `corral fetch` before compiling. ## Architecture -### Request/Response Pattern - -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. 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` - -### OO Convenience API +Every API operation returns `Promise[(T | RequestError)]`. An operation primitive (for example `GetRepository`) builds the URL by RFC 6570 template expansion (`ponylang/uri`), then hands off to a short-lived request actor — `JsonRequester`, `NoContentRequester`, or `CheckRequester` in the `request/` subpackage — that owns a `courier` HTTP connection; the response is turned into a model by a `JsonConverter[T]`, and the promise is fulfilled with the model or a `RequestError`. Paginated results come back as `PaginatedList[A]`, whose `prev_page()`/`next_page()` fetch through `LinkedJsonRequester`, following the HTTP `Link` header (parsed by `ponylang/web_link`). -Models have methods that chain to further API calls: -- `GitHub.get_repo(owner, repo)` -> `Repository` -- `GitHub.get_org_repos(org)` -> `PaginatedList[Repository]` -- `GitHub.get_gist(gist_id)` -> `Gist` -- `GitHub.create_gist(files, description, is_public)` -> `Gist` -- `GitHub.get_user_gists()`, `.get_public_gists()`, `.get_starred_gists()`, `.get_username_gists(username)` -> `PaginatedList[Gist]` -- `Repository.create_label(...)`, `.create_release(...)`, `.delete_label(...)`, `.get_commit(...)`, `.get_issue(...)`, `.get_issues(labels, state, sort, direction, since, per_page)`, `.get_pull_request(...)` -- `Issue.create_comment(...)`, `.get_comments()` -- `PullRequest.get_files()` -- `Gist.update_gist(files, description)`, `.delete_gist()`, `.get_revision(sha)`, `.fork()`, `.get_forks()`, `.get_commits()`, `.star()`, `.unstar()`, `.is_starred()`, `.create_comment(body)`, `.get_comments()` -- `GistComment.update(new_body)`, `.delete()` - -### Pagination - -`PaginatedList[A]` wraps an array of results with `prev_page()` / `next_page()` methods that return `(Promise | None)`. Pagination links are extracted from HTTP `Link` headers using the `ponylang/web_link` library (via `_ExtractPaginationLinks`). Used by `GetRepositoryLabels`, `GetOrganizationRepositories`, `GetRepositoryIssues`, `SearchIssues`, `GetUserGists`, `GetPublicGists`, `GetStarredGists`, `GetUsernameGists`, `GetGistForks`, `GetGistCommits`, and `GetGistComments`. - -### Auth - -`Credentials` holds a `lori.TCPConnectAuth`, an optional token string, and an optional `SSLContext val`. When `ssl_ctx` is provided, request actors use it instead of the default `SSLContextFactory`. Each request actor sets `User-Agent`, `Accept: application/vnd.github.v3+json`, and `Authorization: Bearer ` headers. +The `request/` subpackage is self-contained HTTP infrastructure — it imports nothing from the parent package. ## Conventions -- All models are `class val` (immutable, shareable) -- JSON converters are primitives implementing `JsonConverter[T]` interface -- Type aliases for result unions: `RepositoryOrError`, `IssueOrError`, etc. -- `\nodoc\` annotation on test classes -- Tests cover infrastructure (Link header parsing + query params), JSON converter primitives, result receiver actors, and request actors (via mock HTTPS server) -- Keep CLAUDE.md in sync when adding or changing features — update the source layout, OO convenience API, pagination section, and coverage table as part of the PR that introduces the change - -## Known TODOs in Code - -None currently tracked. - -## GitHub REST API Coverage Comparison - -What this library currently implements vs what GitHub's REST API offers. -Scope is limited to the API categories the library already touches plus -commonly-used categories that a GitHub API library would typically need. - -### Repositories - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}` | GET | GetRepository | -| `/repos/{owner}/{repo}` | PATCH | **missing** | -| `/repos/{owner}/{repo}` | DELETE | **missing** | -| `/orgs/{org}/repos` | GET | GetOrganizationRepositories | -| `/orgs/{org}/repos` | POST | **missing** | -| `/user/repos` | GET | **missing** | -| `/user/repos` | POST | **missing** | -| `/users/{username}/repos` | GET | **missing** | -| `/repos/{owner}/{repo}/contributors` | GET | **missing** | -| `/repos/{owner}/{repo}/languages` | GET | **missing** | -| `/repos/{owner}/{repo}/tags` | GET | **missing** | -| `/repos/{owner}/{repo}/topics` | GET/PUT | **missing** | -| `/repos/{owner}/{repo}/forks` | GET | **missing** | -| `/repos/{owner}/{repo}/forks` | POST | **missing** | - -### Issues - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/issues/{number}` | GET | GetIssue | -| `/repos/{owner}/{repo}/issues` | GET (list) | GetRepositoryIssues | -| `/repos/{owner}/{repo}/issues` | POST | **missing** | -| `/repos/{owner}/{repo}/issues/{number}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/issues/{number}/lock` | PUT | **missing** | -| `/repos/{owner}/{repo}/issues/{number}/lock` | DELETE | **missing** | - -### Issue Comments - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/issues/{number}/comments` | GET | GetIssueComments | -| `/repos/{owner}/{repo}/issues/{number}/comments` | POST | CreateIssueComment | -| `/repos/{owner}/{repo}/issues/comments` | GET (list all) | **missing** | -| `/repos/{owner}/{repo}/issues/comments/{id}` | GET | **missing** | -| `/repos/{owner}/{repo}/issues/comments/{id}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/issues/comments/{id}` | DELETE | **missing** | - -### Issue Assignees - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/assignees` | GET | **missing** | -| `/repos/{owner}/{repo}/issues/{number}/assignees` | POST | **missing** | -| `/repos/{owner}/{repo}/issues/{number}/assignees` | DELETE | **missing** | - -### Labels - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/labels` | GET | GetRepositoryLabels (paginated) | -| `/repos/{owner}/{repo}/labels` | POST | CreateLabel | -| `/repos/{owner}/{repo}/labels/{name}` | GET | **missing** | -| `/repos/{owner}/{repo}/labels/{name}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/labels/{name}` | DELETE | DeleteLabel | -| `/repos/{owner}/{repo}/issues/{number}/labels` | GET | **missing** | -| `/repos/{owner}/{repo}/issues/{number}/labels` | POST | **missing** | -| `/repos/{owner}/{repo}/issues/{number}/labels` | PUT | **missing** | -| `/repos/{owner}/{repo}/issues/{number}/labels/{name}` | DELETE | **missing** | -| `/repos/{owner}/{repo}/milestones/{number}/labels` | GET | **missing** | - -### Milestones - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/milestones` | GET | **missing** | -| `/repos/{owner}/{repo}/milestones` | POST | **missing** | -| `/repos/{owner}/{repo}/milestones/{number}` | GET | **missing** | -| `/repos/{owner}/{repo}/milestones/{number}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/milestones/{number}` | DELETE | **missing** | - -### Pull Requests - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/pulls/{number}` | GET | GetPullRequest | -| `/repos/{owner}/{repo}/pulls/{number}/files` | GET | GetPullRequestFiles | -| `/repos/{owner}/{repo}/pulls` | GET (list) | **missing** | -| `/repos/{owner}/{repo}/pulls` | POST | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/commits` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/merge` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/merge` | PUT | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/update-branch` | POST | **missing** | - -### Pull Request Reviews - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/pulls/{number}/reviews` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/reviews` | POST | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/reviews/{id}` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/reviews/{id}` | PUT | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/reviews/{id}` | DELETE | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/reviews/{id}/comments` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/reviews/{id}/dismissals` | PUT | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/reviews/{id}/events` | POST | **missing** | - -### Pull Request Review Comments - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/pulls/comments` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/comments/{id}` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/comments/{id}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/pulls/comments/{id}` | DELETE | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/comments` | GET | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/comments` | POST | **missing** | -| `/repos/{owner}/{repo}/pulls/{number}/comments/{id}/replies` | POST | **missing** | - -### Commits - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/commits/{ref}` | GET | GetCommit | -| `/repos/{owner}/{repo}/commits` | GET (list) | **missing** | -| `/repos/{owner}/{repo}/compare/{basehead}` | GET | **missing** | -| `/repos/{owner}/{repo}/commits/{sha}/branches-where-head` | GET | **missing** | -| `/repos/{owner}/{repo}/commits/{sha}/pulls` | GET | **missing** | - -### Commit Statuses - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/commits/{ref}/status` | GET | **missing** | -| `/repos/{owner}/{repo}/commits/{ref}/statuses` | GET | **missing** | -| `/repos/{owner}/{repo}/statuses/{sha}` | POST | **missing** | - -### Releases - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/releases` | POST | CreateRelease | -| `/repos/{owner}/{repo}/releases` | GET (list) | **missing** | -| `/repos/{owner}/{repo}/releases/{id}` | GET | **missing** | -| `/repos/{owner}/{repo}/releases/{id}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/releases/{id}` | DELETE | **missing** | -| `/repos/{owner}/{repo}/releases/latest` | GET | **missing** | -| `/repos/{owner}/{repo}/releases/tags/{tag}` | GET | **missing** | -| `/repos/{owner}/{repo}/releases/generate-notes` | POST | **missing** | - -### Release Assets - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/releases/{id}/assets` | GET | **missing** (Asset model exists but no GET operation) | -| `/repos/{owner}/{repo}/releases/{id}/assets` | POST (upload) | **missing** | -| `/repos/{owner}/{repo}/releases/assets/{id}` | GET | **missing** | -| `/repos/{owner}/{repo}/releases/assets/{id}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/releases/assets/{id}` | DELETE | **missing** | - -### Gists - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/gists/{gist_id}` | GET | GetGist | -| `/gists` | POST | CreateGist | -| `/gists/{gist_id}` | PATCH | UpdateGist | -| `/gists/{gist_id}` | DELETE | DeleteGist | -| `/gists` | GET (list) | GetUserGists (paginated) | -| `/gists/public` | GET (list) | GetPublicGists (paginated) | -| `/gists/starred` | GET (list) | GetStarredGists (paginated) | -| `/users/{username}/gists` | GET (list) | GetUsernameGists (paginated) | -| `/gists/{gist_id}/{sha}` | GET | GetGistRevision | -| `/gists/{gist_id}/forks` | POST | ForkGist | -| `/gists/{gist_id}/forks` | GET (list) | GetGistForks (paginated) | -| `/gists/{gist_id}/commits` | GET (list) | GetGistCommits (paginated) | -| `/gists/{gist_id}/star` | PUT | StarGist | -| `/gists/{gist_id}/star` | DELETE | UnstarGist | -| `/gists/{gist_id}/star` | GET | CheckGistStar | - -### Gist Comments - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/gists/{gist_id}/comments/{comment_id}` | GET | GetGistComment | -| `/gists/{gist_id}/comments` | GET (list) | GetGistComments (paginated) | -| `/gists/{gist_id}/comments` | POST | CreateGistComment | -| `/gists/{gist_id}/comments/{comment_id}` | PATCH | UpdateGistComment | -| `/gists/{gist_id}/comments/{comment_id}` | DELETE | DeleteGistComment | - -### Search - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/search/issues` | GET | SearchIssues | -| `/search/code` | GET | **missing** | -| `/search/commits` | GET | **missing** | -| `/search/labels` | GET | **missing** | -| `/search/repositories` | GET | **missing** | -| `/search/topics` | GET | **missing** | -| `/search/users` | GET | **missing** | - -### Users - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/user` | GET | **missing** (User model exists but no GET operation) | -| `/users/{username}` | GET | **missing** | -| `/users` | GET (list) | **missing** | - -### Check Runs - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/check-runs` | POST | **missing** | -| `/repos/{owner}/{repo}/check-runs/{id}` | GET | **missing** | -| `/repos/{owner}/{repo}/check-runs/{id}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/commits/{ref}/check-runs` | GET | **missing** | - -### Branches - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/branches` | GET | **missing** | -| `/repos/{owner}/{repo}/branches/{branch}` | GET | **missing** | -| `/repos/{owner}/{repo}/branches/{branch}/rename` | POST | **missing** | - -### Repository Contents - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/contents/{path}` | GET | **missing** | -| `/repos/{owner}/{repo}/contents/{path}` | PUT | **missing** | -| `/repos/{owner}/{repo}/contents/{path}` | DELETE | **missing** | -| `/repos/{owner}/{repo}/readme` | GET | **missing** | - -### Collaborators - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/collaborators` | GET | **missing** | -| `/repos/{owner}/{repo}/collaborators/{username}` | GET | **missing** | -| `/repos/{owner}/{repo}/collaborators/{username}` | PUT | **missing** | -| `/repos/{owner}/{repo}/collaborators/{username}` | DELETE | **missing** | -| `/repos/{owner}/{repo}/collaborators/{username}/permission` | GET | **missing** | - -### Webhooks - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/hooks` | GET | **missing** | -| `/repos/{owner}/{repo}/hooks` | POST | **missing** | -| `/repos/{owner}/{repo}/hooks/{id}` | GET | **missing** | -| `/repos/{owner}/{repo}/hooks/{id}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/hooks/{id}` | DELETE | **missing** | -| `/repos/{owner}/{repo}/hooks/{id}/pings` | POST | **missing** | - -### Organizations - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/orgs/{org}` | GET | **missing** | -| `/orgs/{org}` | PATCH | **missing** | -| `/user/orgs` | GET | **missing** | -| `/users/{username}/orgs` | GET | **missing** | - -### Reactions - -| Endpoint | Method | Library | -|----------|--------|---------| -| All reaction endpoints | GET/POST/DELETE | **missing** | - -### Git References - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/git/refs` | POST | **missing** | -| `/repos/{owner}/{repo}/git/ref/{ref}` | GET | **missing** | -| `/repos/{owner}/{repo}/git/refs/{ref}` | PATCH | **missing** | -| `/repos/{owner}/{repo}/git/refs/{ref}` | DELETE | **missing** | - -### Starring - -| Endpoint | Method | Library | -|----------|--------|---------| -| `/repos/{owner}/{repo}/stargazers` | GET | **missing** | -| `/user/starred` | GET | **missing** | -| `/user/starred/{owner}/{repo}` | GET/PUT/DELETE | **missing** | - -### Not covered at all (entire categories) - -These API categories have zero coverage in the library: - -- Actions (workflows, runners, secrets, artifacts) -- Activity (events, feeds, notifications, watching) -- Apps (GitHub App management) -- Code scanning -- Codespaces -- Deployments -- Git database (blobs, trees, tags beyond refs) -- GitHub Pages -- Packages -- Projects -- Teams -- Branch protection - -### Infrastructure gaps - -| Gap | Notes | -|-----|-------| -| List operations | Most resources only have "get one", not "list many" | -| GetPullRequestFiles not paginated | GitHub paginates this but library returns plain Array | -| PullRequestFile sparse | Only has `filename`; GitHub returns sha, status, additions, deletions, changes, patch, etc. | -| CommitFile sparse | Only has sha, status, filename; missing additions, deletions, changes, patch | -| No rate limiting | No handling of rate limit headers or 429 responses | -| No conditional requests | No ETag/If-None-Match support | +- Models are `class val` — immutable and shareable. +- `\nodoc\` on test classes. +- An operation that has an object convenience method ships two examples — a plain one using the operation primitive and an `-oo` one using the method; an operation without a convenience method ships only the plain example.