diff --git a/.release-notes/get-repository-issues.md b/.release-notes/get-repository-issues.md new file mode 100644 index 0000000..b250c53 --- /dev/null +++ b/.release-notes/get-repository-issues.md @@ -0,0 +1,26 @@ +## Add GetRepositoryIssues with paginated issue listing + +List issues for a repository with pagination support. Supports filtering by label and state. + +```pony +// Via Repository chaining +github.get_repo("ponylang", "ponyc").next[None]({ + (result: RepositoryOrError) => + match result + | let repo: Repository => + repo.get_issues(where labels = "discuss during sync").next[None]({ + (r: (PaginatedList[Issue] | RequestError)) => + match r + | let issues: PaginatedList[Issue] => + for issue in issues.results.values() do + if not issue.is_pull_request then + env.out.print(issue.title) + end + end + end + }) + end +}) +``` + +The `is_pull_request` field on `Issue` indicates whether an issue is actually a pull request, since the GitHub issues API returns both. diff --git a/CLAUDE.md b/CLAUDE.md index 6062c46..769e30c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,9 +27,9 @@ Uses `corral` for dependency management. `make` automatically runs `corral fetch ``` github_rest_api/ - github.pony -- GitHub class (entry point, only has get_repo) + github.pony -- GitHub class (entry point, has get_repo and get_org_repos) repository.pony -- Repository model + GetRepository, GetRepositoryLabels - issue.pony -- Issue model + GetIssue + issue.pony -- Issue model + GetIssue, GetRepositoryIssues pull_request.pony -- PullRequest model + GetPullRequest pull_request_base.pony -- PullRequestBase model (head/base refs) pull_request_file.pony -- PullRequestFile model + GetPullRequestFiles @@ -74,13 +74,14 @@ All API operations return `Promise[(T | RequestError)]`. The flow is: Models have methods that chain to further API calls: - `GitHub.get_repo(owner, repo)` -> `Repository` -- `Repository.create_label(...)`, `.create_release(...)`, `.delete_label(...)`, `.get_commit(...)`, `.get_issue(...)`, `.get_pull_request(...)` +- `GitHub.get_org_repos(org)` -> `PaginatedList[Repository]` +- `Repository.create_label(...)`, `.create_release(...)`, `.delete_label(...)`, `.get_commit(...)`, `.get_issue(...)`, `.get_issues(...)`, `.get_pull_request(...)` - `Issue.create_comment(...)`, `.get_comments()` - `PullRequest.get_files()` ### 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 PEG-based `ExtractPaginationLinks` parser. Currently used by `GetRepositoryLabels`. +`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 PEG-based `ExtractPaginationLinks` parser. Used by `GetRepositoryLabels`, `GetOrganizationRepositories`, and `GetRepositoryIssues`. ### Auth @@ -114,7 +115,7 @@ commonly-used categories that a GitHub API library would typically need. | `/repos/{owner}/{repo}` | GET | GetRepository | | `/repos/{owner}/{repo}` | PATCH | **missing** | | `/repos/{owner}/{repo}` | DELETE | **missing** | -| `/orgs/{org}/repos` | GET | **missing** | +| `/orgs/{org}/repos` | GET | GetOrganizationRepositories | | `/orgs/{org}/repos` | POST | **missing** | | `/user/repos` | GET | **missing** | | `/user/repos` | POST | **missing** | @@ -131,7 +132,7 @@ commonly-used categories that a GitHub API library would typically need. | Endpoint | Method | Library | |----------|--------|---------| | `/repos/{owner}/{repo}/issues/{number}` | GET | GetIssue | -| `/repos/{owner}/{repo}/issues` | GET (list) | **missing** | +| `/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** | diff --git a/github_rest_api/issue.pony b/github_rest_api/issue.pony index 0d2c50c..8aee3ec 100644 --- a/github_rest_api/issue.pony +++ b/github_rest_api/issue.pony @@ -16,6 +16,8 @@ class val Issue let state: (String | None) let body: (String | None) + let is_pull_request: Bool + let url: String let respository_url: String let labels_url: String @@ -35,7 +37,8 @@ class val Issue user': User, labels': Array[Label] val, state': (String | None), - body': (String | None)) + body': (String | None), + is_pull_request': Bool = false) => _creds = creds url = url' @@ -50,6 +53,7 @@ class val Issue labels = labels' state = state' body = body' + is_pull_request = is_pull_request' fun create_comment(comment: String): Promise[IssueCommentOrError] => CreateIssueComment.by_url(comments_url, comment, _creds) @@ -93,6 +97,57 @@ primitive GetIssue p +primitive GetRepositoryIssues + fun apply(owner: String, + repo: String, + creds: Credentials, + labels: String = "", + state: String = "open"): Promise[(PaginatedList[Issue] | RequestError)] + => + let u = SimpleURITemplate( + recover val + "https://api.github.com/repos{/owner}{/repo}/issues" + end, + recover val + [ ("owner", owner); ("repo", repo) ] + end) + + match u + | let u': String => + let url = _build_url(u', labels, state) + by_url(url, creds) + | let e: ParseError => + Promise[(PaginatedList[Issue] | RequestError)].>apply( + RequestError(where message' = e.message)) + end + + fun by_url(url: String, + creds: Credentials): Promise[(PaginatedList[Issue] | RequestError)] + => + let ic = IssueJsonConverter + let plc = PaginatedListJsonConverter[Issue](creds, ic) + let p = Promise[(PaginatedList[Issue] | RequestError)] + let r = PaginatedResultReceiver[Issue](creds, p, plc) + + try + PaginatedJsonRequester(creds.auth).apply[Issue](url, r)? + else + let m = "Unable to initiate get_repository_issues request to " + url + p(RequestError(where message' = consume m)) + end + + p + + fun _build_url(base: String, labels: String, state: String): String => + let query = recover iso String end + query.append("?state=") + query.append(state) + if labels.size() > 0 then + query.append("&labels=") + query.append(labels) + end + base + consume query + primitive IssueJsonConverter is JsonConverter[Issue] fun apply(json: JsonType val, creds: Credentials): Issue ? => let obj = JsonExtractor(json).as_object()? @@ -116,6 +171,8 @@ primitive IssueJsonConverter is JsonConverter[Issue] labels.push(l) end + let is_pull_request = obj.contains("pull_request") + Issue(creds, url, respository_url, @@ -128,4 +185,5 @@ primitive IssueJsonConverter is JsonConverter[Issue] user, consume labels, state, - body) + body, + is_pull_request) diff --git a/github_rest_api/repository.pony b/github_rest_api/repository.pony index 3fc92d4..2e58fd6 100644 --- a/github_rest_api/repository.pony +++ b/github_rest_api/repository.pony @@ -316,6 +316,21 @@ class val Repository Promise[IssueOrError].>apply(RequestError(where message' = e.message)) end + fun get_issues(labels: String = "", state: String = "open") + : Promise[(PaginatedList[Issue] | RequestError)] + => + let u = SimpleURITemplate(issues_url, + recover val Array[(String, String)] end) + + match u + | let u': String => + let issues_url' = GetRepositoryIssues._build_url(u', labels, state) + GetRepositoryIssues.by_url(issues_url', _creds) + | let e: ParseError => + Promise[(PaginatedList[Issue] | RequestError)].>apply( + RequestError(where message' = e.message)) + end + fun get_pull_request(number: I64): Promise[PullRequestOrError] => let u = SimpleURITemplate( pulls_url,