Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .release-notes/get-repository-issues.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 7 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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** |
Expand All @@ -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** |
Expand Down
62 changes: 60 additions & 2 deletions github_rest_api/issue.pony
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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()?
Expand All @@ -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,
Expand All @@ -128,4 +185,5 @@ primitive IssueJsonConverter is JsonConverter[Issue]
user,
consume labels,
state,
body)
body,
is_pull_request)
15 changes: 15 additions & 0 deletions github_rest_api/repository.pony
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down