Skip to content

Commit 036a8d0

Browse files
authored
Add full Gist API support (#72)
Implements complete coverage of the GitHub Gist API (20 endpoints) requested in #70 for an upcoming ponylang org project. New models: Gist, GistFile, GistFileEdit, GistFileRename, GistFileDelete, GistCommit, GistChangeStatus, GistComment. 15 gist operations: GetGist, CreateGist, UpdateGist, DeleteGist, GetUserGists, GetPublicGists, GetStarredGists, GetUsernameGists, GetGistRevision, ForkGist, GetGistForks, GetGistCommits, StarGist, UnstarGist, CheckGistStar. 5 gist comment operations: GetGistComment, GetGistComments, CreateGistComment, UpdateGistComment, DeleteGistComment. 3 new HTTP infrastructure classes: HTTPPatch (PATCH expecting 200), HTTPPut (PUT expecting 204), HTTPCheck (GET returning Bool via 204/404 status codes). These close the documented PATCH/PUT infrastructure gaps. Closes #70
1 parent 3d50130 commit 036a8d0

32 files changed

Lines changed: 2718 additions & 5 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## Add full Gist API support
2+
3+
Complete coverage of the GitHub Gist API: 15 gist operations and 5 gist comment operations covering CRUD, listing, forking, commit history, and starring.
4+
5+
New models: `Gist`, `GistFile`, `GistFileEdit`, `GistFileRename`, `GistFileDelete`, `GistCommit`, `GistChangeStatus`, and `GistComment`.
6+
7+
```pony
8+
// Fetch a gist
9+
GitHub(creds).get_gist("abc123")
10+
.next[None]({(r: GistOrError) =>
11+
match r
12+
| let gist: Gist =>
13+
for (name, file) in gist.files.values() do
14+
env.out.print(name)
15+
end
16+
// Chain to further operations
17+
gist.get_comments()
18+
gist.star()
19+
gist.fork()
20+
end
21+
})
22+
23+
// Create a gist
24+
let files = recover val [("hello.py", "print('hello')")] end
25+
CreateGist(files, creds where description = "My gist", is_public = true)
26+
27+
// Update a gist's files
28+
let updates = recover val
29+
[("old.py", GistFileEdit("new content"))
30+
("rename.py", GistFileRename("renamed.py"))
31+
("delete-me.py", GistFileDelete)]
32+
end
33+
gist.update_gist(updates)
34+
```
35+
36+
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).

CLAUDE.md

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Uses `corral` for dependency management. `make` automatically runs `corral fetch
2828

2929
```
3030
github_rest_api/
31-
github.pony -- GitHub class (entry point, has get_repo and get_org_repos)
31+
github.pony -- GitHub class (entry point, has get_repo, get_org_repos, gist operations)
3232
repository.pony -- Repository model + GetRepository, GetRepositoryLabels
3333
issue.pony -- Issue model + GetIssue, GetRepositoryIssues
3434
issue_pull_request.pony -- IssuePullRequest model (PR metadata on issues)
@@ -43,6 +43,11 @@ github_rest_api/
4343
asset.pony -- Asset model (release assets)
4444
label.pony -- Label model + CreateLabel, DeleteLabel
4545
issue_comment.pony -- IssueComment model + CreateIssueComment, GetIssueComments
46+
gist.pony -- Gist model + 15 gist operations (CRUD, lists, forks, commits, star)
47+
gist_file.pony -- GistFile model (file within a gist)
48+
gist_file_update.pony -- GistFileEdit, GistFileRename, GistFileDelete for update operations
49+
gist_commit.pony -- GistCommit + GistChangeStatus models
50+
gist_comment.pony -- GistComment model + 5 comment operations (CRUD + list)
4651
search.pony -- SearchIssues + SearchResults generic
4752
user.pony -- User model
4853
license.pony -- License model
@@ -53,7 +58,10 @@ github_rest_api/
5358
http.pony -- Credentials, ResultReceiver, RequestFactory
5459
http_get.pony -- JsonRequester (GET with JSON response)
5560
http_post.pony -- HTTPPost (POST with JSON response)
61+
http_patch.pony -- HTTPPatch (PATCH with JSON response, expects 200)
5662
http_delete.pony -- HTTPDelete (DELETE, expects 204)
63+
http_put.pony -- HTTPPut (PUT with no body, expects 204)
64+
http_check.pony -- HTTPCheck (GET returning Bool: 204=true, 404=false)
5765
request_error.pony -- RequestError (status, response_body, message)
5866
json.pony -- JsonConverter interface, JsonTypeString utility
5967
query_params.pony -- QueryParams (URL query string builder with percent-encoding)
@@ -70,7 +78,7 @@ All API operations return `Promise[(T | RequestError)]`. The flow is:
7078
1. Operation primitive (e.g., `GetRepository`) creates a `Promise`
7179
2. Creates a `ResultReceiver[T]` actor with the promise and a `JsonConverter[T]`
7280
3. Builds URL using `ponylang/uri` RFC 6570 template expansion for path parameters
73-
4. Issues HTTP request via `JsonRequester` / `HTTPPost` / `HTTPDelete`
81+
4. Issues HTTP request via `JsonRequester` / `HTTPPost` / `HTTPPatch` / `HTTPDelete` / `HTTPPut` / `HTTPCheck`
7482
5. On success, JSON is parsed and converted to model via `JsonConverter`
7583
6. Promise is fulfilled with either the model or a `RequestError`
7684

@@ -79,13 +87,18 @@ All API operations return `Promise[(T | RequestError)]`. The flow is:
7987
Models have methods that chain to further API calls:
8088
- `GitHub.get_repo(owner, repo)` -> `Repository`
8189
- `GitHub.get_org_repos(org)` -> `PaginatedList[Repository]`
90+
- `GitHub.get_gist(gist_id)` -> `Gist`
91+
- `GitHub.create_gist(files, description, is_public)` -> `Gist`
92+
- `GitHub.get_user_gists()`, `.get_public_gists()`, `.get_starred_gists()`, `.get_username_gists(username)` -> `PaginatedList[Gist]`
8293
- `Repository.create_label(...)`, `.create_release(...)`, `.delete_label(...)`, `.get_commit(...)`, `.get_issue(...)`, `.get_issues(...)`, `.get_pull_request(...)`
8394
- `Issue.create_comment(...)`, `.get_comments()`
8495
- `PullRequest.get_files()`
96+
- `Gist.update_gist(files, description)`, `.delete_gist()`, `.get_revision(sha)`, `.fork()`, `.get_forks()`, `.get_commits()`, `.star()`, `.unstar()`, `.is_starred()`, `.create_comment(body)`, `.get_comments()`
97+
- `GistComment.update(new_body)`, `.delete()`
8598

8699
### Pagination
87100

88-
`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`, and `SearchIssues`.
101+
`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`.
89102

90103
### Auth
91104

@@ -264,6 +277,36 @@ commonly-used categories that a GitHub API library would typically need.
264277
| `/repos/{owner}/{repo}/releases/assets/{id}` | PATCH | **missing** |
265278
| `/repos/{owner}/{repo}/releases/assets/{id}` | DELETE | **missing** |
266279

280+
### Gists
281+
282+
| Endpoint | Method | Library |
283+
|----------|--------|---------|
284+
| `/gists/{gist_id}` | GET | GetGist |
285+
| `/gists` | POST | CreateGist |
286+
| `/gists/{gist_id}` | PATCH | UpdateGist |
287+
| `/gists/{gist_id}` | DELETE | DeleteGist |
288+
| `/gists` | GET (list) | GetUserGists (paginated) |
289+
| `/gists/public` | GET (list) | GetPublicGists (paginated) |
290+
| `/gists/starred` | GET (list) | GetStarredGists (paginated) |
291+
| `/users/{username}/gists` | GET (list) | GetUsernameGists (paginated) |
292+
| `/gists/{gist_id}/{sha}` | GET | GetGistRevision |
293+
| `/gists/{gist_id}/forks` | POST | ForkGist |
294+
| `/gists/{gist_id}/forks` | GET (list) | GetGistForks (paginated) |
295+
| `/gists/{gist_id}/commits` | GET (list) | GetGistCommits (paginated) |
296+
| `/gists/{gist_id}/star` | PUT | StarGist |
297+
| `/gists/{gist_id}/star` | DELETE | UnstarGist |
298+
| `/gists/{gist_id}/star` | GET | CheckGistStar |
299+
300+
### Gist Comments
301+
302+
| Endpoint | Method | Library |
303+
|----------|--------|---------|
304+
| `/gists/{gist_id}/comments/{comment_id}` | GET | GetGistComment |
305+
| `/gists/{gist_id}/comments` | GET (list) | GetGistComments (paginated) |
306+
| `/gists/{gist_id}/comments` | POST | CreateGistComment |
307+
| `/gists/{gist_id}/comments/{comment_id}` | PATCH | UpdateGistComment |
308+
| `/gists/{gist_id}/comments/{comment_id}` | DELETE | DeleteGistComment |
309+
267310
### Search
268311

269312
| Endpoint | Method | Library |
@@ -373,7 +416,6 @@ These API categories have zero coverage in the library:
373416
- Code scanning
374417
- Codespaces
375418
- Deployments
376-
- Gists
377419
- Git database (blobs, trees, tags beyond refs)
378420
- GitHub Pages
379421
- Packages
@@ -385,7 +427,6 @@ These API categories have zero coverage in the library:
385427

386428
| Gap | Notes |
387429
|-----|-------|
388-
| No HTTP PUT/PATCH support | Can't update any resources. Need `HTTPPut` and `HTTPPatch` classes |
389430
| List operations | Most resources only have "get one", not "list many" |
390431
| GetPullRequestFiles not paginated | GitHub paginates this but library returns plain Array |
391432
| PullRequestFile sparse | Only has `filename`; GitHub returns sha, status, additions, deletions, changes, patch, etc. |

examples/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Examples
2+
3+
Examples come in two styles: **functional** (calling operation primitives directly) and **OO** (chaining through `GitHub` and model convenience methods). Both styles produce the same results; use whichever fits your application's structure.
4+
5+
Each example has its own `Makefile`. Build with `make ssl=3.0.x` (or the SSL version matching your system).
6+
7+
## Gists
8+
9+
| Example | Style | Description |
10+
|---------|-------|-------------|
11+
| `get-gist` | Functional | Fetch a gist by ID and print its files |
12+
| `get-gist-oo` | OO | Same as `get-gist`, using `GitHub.get_gist()` |
13+
| `create-gist` | Functional | Create a new gist with a single file |
14+
| `create-gist-oo` | OO | Same as `create-gist`, using `GitHub.create_gist()` |
15+
| `list-gists` | Functional | List the authenticated user's gists with pagination |
16+
| `list-gists-oo` | OO | Same as `list-gists`, using `GitHub.get_user_gists()` |
17+
| `gist-comments` | Functional | List comments on a gist with pagination |
18+
| `gist-comments-oo` | OO | Same as `gist-comments`, chaining through `Gist.get_comments()` |
19+
| `star-gist` | Functional | Star a gist and verify it is starred |
20+
| `star-gist-oo` | OO | Same as `star-gist`, chaining through `Gist.star()` |
21+
22+
## Repositories
23+
24+
| Example | Style | Description |
25+
|---------|-------|-------------|
26+
| `get-repository` | Functional | Fetch a repository by owner and name |
27+
| `get-repository-oo` | OO | Same as `get-repository`, using `GitHub.get_repo()` |
28+
| `get-repository-labels` | Functional | List labels for a repository with pagination |
29+
30+
## Issues
31+
32+
| Example | Style | Description |
33+
|---------|-------|-------------|
34+
| `get-issue` | Functional | Fetch an issue by number |
35+
| `get-issue-oo` | OO | Same as `get-issue`, chaining through `Repository.get_issue()` |
36+
| `get-issue-comments` | Functional | List comments on an issue |
37+
| `get-issue-comments-oo` | OO | Same as `get-issue-comments`, chaining through `Issue.get_comments()` |
38+
| `create-issue-comment` | Functional | Create a comment on an issue |
39+
| `create-issue-comment-oo` | OO | Same as `create-issue-comment`, chaining through `Issue.create_comment()` |
40+
| `search-issues` | Functional | Search issues across repositories with pagination |
41+
42+
## Pull Requests
43+
44+
| Example | Style | Description |
45+
|---------|-------|-------------|
46+
| `get-pull-request` | Functional | Fetch a pull request by number |
47+
| `get-pull-request-oo` | OO | Same as `get-pull-request`, chaining through `Repository.get_pull_request()` |
48+
| `get-pull-request-files` | Functional | List files changed in a pull request |
49+
| `get-pull-request-files-oo` | OO | Same as `get-pull-request-files`, chaining through `PullRequest.get_files()` |
50+
51+
## Commits
52+
53+
| Example | Style | Description |
54+
|---------|-------|-------------|
55+
| `get-commit` | Functional | Fetch a commit by SHA |
56+
| `get-commit-oo` | OO | Same as `get-commit`, chaining through `Repository.get_commit()` |
57+
58+
## Labels
59+
60+
| Example | Style | Description |
61+
|---------|-------|-------------|
62+
| `create-label` | Functional | Create a label on a repository |
63+
| `create-label-oo` | OO | Same as `create-label`, chaining through `Repository.create_label()` |
64+
| `delete-label` | Functional | Delete a label from a repository |
65+
| `delete-label-oo` | OO | Same as `delete-label`, chaining through `Repository.delete_label()` |
66+
| `standard-pony-labels` | Functional | Creates the standard set of labels used by ponylang projects |
67+
68+
## Releases
69+
70+
| Example | Style | Description |
71+
|---------|-------|-------------|
72+
| `create-release` | Functional | Create a release on a repository |
73+
| `create-release-oo` | OO | Same as `create-release`, chaining through `Repository.create_release()` |

examples/create-gist-oo/Makefile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
GET_DEPENDENCIES_WITH := corral fetch
2+
COMPILE_WITH := corral run -- ponyc
3+
4+
BUILD_DIR ?= build
5+
SRC_DIR := .
6+
BINARY_NAME := create-gist-oo
7+
LIB_SRC := ../../github_rest_api/
8+
9+
PONYC = $(COMPILE_WITH)
10+
11+
ifeq ($(ssl), 3.0.x)
12+
SSL = -Dopenssl_3.0.x
13+
else ifeq ($(ssl), 1.1.x)
14+
SSL = -Dopenssl_1.1.x
15+
else ifeq ($(ssl), 0.9.0)
16+
SSL = -Dopenssl_0.9.0
17+
else
18+
$(error Unknown SSL version "$(ssl)". Must set using 'ssl=FOO')
19+
endif
20+
21+
PONYC := $(PONYC) $(SSL)
22+
23+
SOURCE_FILES := $(shell find $(LIB_SRC) -name "*.pony")
24+
25+
$(BUILD_DIR)/$(BINARY_NAME): $(SOURCE_FILES) main.pony | $(BUILD_DIR)
26+
$(GET_DEPENDENCIES_WITH)
27+
$(PONYC) -o $(BUILD_DIR) $(SRC_DIR)
28+
29+
clean:
30+
rm -rf $(BUILD_DIR)
31+
32+
$(BUILD_DIR):
33+
mkdir -p $(BUILD_DIR)
34+
35+
.PHONY: clean

examples/create-gist-oo/main.pony

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use "../../github_rest_api"
2+
use "../../github_rest_api/request"
3+
use "cli"
4+
use "net"
5+
6+
actor Main
7+
new create(env: Env) =>
8+
try
9+
// ----- CLI setup
10+
let cs =
11+
CommandSpec.leaf("create-gist-oo",
12+
"Create a new gist with a single file",
13+
[
14+
OptionSpec.string("filename", "Name of the file to create")
15+
OptionSpec.string("content", "Content of the file")
16+
OptionSpec.string("description",
17+
"Description of the gist"
18+
where default' = "")
19+
OptionSpec.bool("public",
20+
"Whether the gist should be public"
21+
where default' = false)
22+
OptionSpec.string("token", "GitHub personal access token")
23+
]
24+
)? .> add_help()?
25+
26+
let cmd = match CommandParser(cs).parse(env.args, env.vars)
27+
| let c: Command =>
28+
c
29+
| let ch: CommandHelp =>
30+
ch.print_help(env.out)
31+
return
32+
| let se: SyntaxError =>
33+
env.err.print(se.string())
34+
env.exitcode(1)
35+
return
36+
end
37+
38+
let filename = cmd.option("filename").string()
39+
let content = cmd.option("content").string()
40+
let description = cmd.option("description").string()
41+
let is_public = cmd.option("public").bool()
42+
let token = cmd.option("token").string()
43+
44+
// ----- Create gist
45+
let auth = TCPConnectAuth(env.root)
46+
let creds = Credentials(auth, token)
47+
48+
let files = recover val
49+
let f = Array[(String, String)]
50+
f.push((filename, content))
51+
f
52+
end
53+
54+
let desc: (String | None) =
55+
if description.size() > 0 then description else None end
56+
57+
GitHub(creds).create_gist(files, desc, is_public)
58+
.next[None](PrintGist~apply(env.out))
59+
else
60+
env.out.print("Something went wrong")
61+
end
62+
63+
primitive PrintGist
64+
fun apply(out: OutStream, g: GistOrError) =>
65+
match g
66+
| let gist: Gist =>
67+
out.print("Gist created: " + gist.id)
68+
out.print(gist.html_url)
69+
| let e: RequestError =>
70+
out.print("Unable to create gist")
71+
out.print(e.status.string())
72+
out.print(e.response_body)
73+
out.print(e.message)
74+
end

examples/create-gist/Makefile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
GET_DEPENDENCIES_WITH := corral fetch
2+
COMPILE_WITH := corral run -- ponyc
3+
4+
BUILD_DIR ?= build
5+
SRC_DIR := .
6+
BINARY_NAME := create-gist
7+
LIB_SRC := ../../github_rest_api/
8+
9+
PONYC = $(COMPILE_WITH)
10+
11+
ifeq ($(ssl), 3.0.x)
12+
SSL = -Dopenssl_3.0.x
13+
else ifeq ($(ssl), 1.1.x)
14+
SSL = -Dopenssl_1.1.x
15+
else ifeq ($(ssl), 0.9.0)
16+
SSL = -Dopenssl_0.9.0
17+
else
18+
$(error Unknown SSL version "$(ssl)". Must set using 'ssl=FOO')
19+
endif
20+
21+
PONYC := $(PONYC) $(SSL)
22+
23+
SOURCE_FILES := $(shell find $(LIB_SRC) -name "*.pony")
24+
25+
$(BUILD_DIR)/$(BINARY_NAME): $(SOURCE_FILES) main.pony | $(BUILD_DIR)
26+
$(GET_DEPENDENCIES_WITH)
27+
$(PONYC) -o $(BUILD_DIR) $(SRC_DIR)
28+
29+
clean:
30+
rm -rf $(BUILD_DIR)
31+
32+
$(BUILD_DIR):
33+
mkdir -p $(BUILD_DIR)
34+
35+
.PHONY: clean

0 commit comments

Comments
 (0)