From c4776025b52e95e1c9af49dc7024028a2f238273 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Mon, 10 Feb 2025 22:23:33 -0900 Subject: [PATCH] Add gist `fork_of` decoding and requests to fetch user's starred gists (#201) * add title,forkOf fields to gist, and starred gists request * actually, dont need the title, using filenames * fix tests * fix lint errors --------- Co-authored-by: Andrew McKnight --- OctoKit/Gist.swift | 59 ++++++++++++++++--- Tests/OctoKitTests/Fixtures/gist.json | 43 +++++++++++++- .../OctoKitTests/Fixtures/starred_gists.json | 49 +++++++++++++++ Tests/OctoKitTests/GistTests.swift | 45 ++++++++++++++ 4 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 Tests/OctoKitTests/Fixtures/starred_gists.json diff --git a/OctoKit/Gist.swift b/OctoKit/Gist.swift index 87fe728..ddea34c 100644 --- a/OctoKit/Gist.swift +++ b/OctoKit/Gist.swift @@ -23,6 +23,7 @@ open class Gist: Codable { open var comments: Int? open var user: User? open var owner: User? + open var forkOf: Gist? public init(id: String? = nil, url: URL? = nil, @@ -39,7 +40,8 @@ open class Gist: Codable { description: String? = nil, comments: Int? = nil, user: User? = nil, - owner: User? = nil) { + owner: User? = nil, + forkOf: Gist? = nil) { self.id = id self.url = url self.forksURL = forksURL @@ -56,6 +58,7 @@ open class Gist: Codable { self.comments = comments self.user = user self.owner = owner + self.forkOf = forkOf } enum CodingKeys: String, CodingKey { @@ -75,6 +78,7 @@ open class Gist: Codable { case comments case user case owner + case forkOf = "fork_of" } } @@ -116,6 +120,41 @@ public extension Octokit { } #endif + /** + Fetches the starred gists of the authenticated user + - parameter page: Current page for gist pagination. `1` by default. + - parameter perPage: Number of gists per page. `100` by default. + - parameter completion: Callback for the outcome of the fetch. + */ + @discardableResult + func myStarredGists(page: String = "1", + perPage: String = "100", + completion: @escaping (_ response: Result<[Gist], Error>) -> Void) -> URLSessionDataTaskProtocol? { + let router = GistRouter.readAuthenticatedStarredGists(configuration, page, perPage) + return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [Gist].self) { gists, error in + if let error = error { + completion(.failure(error)) + } else { + if let gists = gists { + completion(.success(gists)) + } + } + } + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + /** + Fetches the starred gists of the authenticated user + - parameter page: Current page for gist pagination. `1` by default. + - parameter perPage: Number of gists per page. `100` by default. + */ + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func myStarredGists(page: String = "1", perPage: String = "100") async throws -> [Gist] { + let router = GistRouter.readAuthenticatedStarredGists(configuration, page, perPage) + return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [Gist].self) + } + #endif + /** Fetches the gists of the specified user - parameter owner: The username who owns the gists. @@ -155,7 +194,7 @@ public extension Octokit { #endif /** - Fetches an gist + Fetches a gist - parameter id: The id of the gist. - parameter completion: Callback for the outcome of the fetch. */ @@ -175,7 +214,7 @@ public extension Octokit { #if compiler(>=5.5.2) && canImport(_Concurrency) /** - Fetches an gist + Fetches a gist - parameter id: The id of the gist. */ @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) @@ -186,7 +225,7 @@ public extension Octokit { #endif /** - Creates an gist with a single file. + Creates a gist with a single file. - parameter description: The description of the gist. - parameter filename: The name of the file in the gist. - parameter fileContent: The content of the file in the gist. @@ -215,7 +254,7 @@ public extension Octokit { #if compiler(>=5.5.2) && canImport(_Concurrency) /** - Creates an gist with a single file. + Creates a gist with a single file. - parameter description: The description of the gist. - parameter filename: The name of the file in the gist. - parameter fileContent: The content of the file in the gist. @@ -231,7 +270,7 @@ public extension Octokit { #endif /** - Edits an gist with a single file. + Edits a gist with a single file. - parameter id: The of the gist to update. - parameter description: The description of the gist. - parameter filename: The name of the file in the gist. @@ -260,7 +299,7 @@ public extension Octokit { #if compiler(>=5.5.2) && canImport(_Concurrency) /** - Edits an gist with a single file. + Edits a gist with a single file. - parameter id: The of the gist to update. - parameter description: The description of the gist. - parameter filename: The name of the file in the gist. @@ -280,6 +319,7 @@ public extension Octokit { enum GistRouter: JSONPostRouter { case readAuthenticatedGists(Configuration, String, String) + case readAuthenticatedStarredGists(Configuration, String, String) case readGists(Configuration, String, String, String) case readGist(Configuration, String) case postGistFile(Configuration, String, String, String, Bool) @@ -306,6 +346,7 @@ enum GistRouter: JSONPostRouter { var configuration: Configuration { switch self { case let .readAuthenticatedGists(config, _, _): return config + case let .readAuthenticatedStarredGists(config, _, _): return config case let .readGists(config, _, _, _): return config case let .readGist(config, _): return config case let .postGistFile(config, _, _, _, _): return config @@ -317,6 +358,8 @@ enum GistRouter: JSONPostRouter { switch self { case let .readAuthenticatedGists(_, page, perPage): return ["per_page": perPage, "page": page] + case let .readAuthenticatedStarredGists(_, page, perPage): + return ["per_page": perPage, "page": page] case let .readGists(_, _, page, perPage): return ["per_page": perPage, "page": page] case .readGist: @@ -347,6 +390,8 @@ enum GistRouter: JSONPostRouter { switch self { case .readAuthenticatedGists: return "gists" + case .readAuthenticatedStarredGists: + return "gists/starred" case let .readGists(_, owner, _, _): return "users/\(owner)/gists" case let .readGist(_, id): diff --git a/Tests/OctoKitTests/Fixtures/gist.json b/Tests/OctoKitTests/Fixtures/gist.json index c545125..603194e 100644 --- a/Tests/OctoKitTests/Fixtures/gist.json +++ b/Tests/OctoKitTests/Fixtures/gist.json @@ -42,6 +42,47 @@ "type" : "application\/x-ruby" } }, + "fork_of": { + "url": "https://api.github.com/gists/f87cbd23c4bc9ad7b04a9f80cf532fc3", + "forks_url": "https://api.github.com/gists/f87cbd23c4bc9ad7b04a9f80cf532fc3/forks", + "commits_url": "https://api.github.com/gists/f87cbd23c4bc9ad7b04a9f80cf532fc3/commits", + "id": "f87cbd23c4bc9ad7b04a9f80cf532fc3", + "node_id": "G_kwDOADF1_doAIGY4N2NiZDIzYzRiYzlhZDdiMDRhOWY4MGNmNTMyZmMz", + "git_pull_url": "https://gist.github.com/f87cbd23c4bc9ad7b04a9f80cf532fc3.git", + "git_push_url": "https://gist.github.com/f87cbd23c4bc9ad7b04a9f80cf532fc3.git", + "html_url": "https://gist.github.com/armcknight/f87cbd23c4bc9ad7b04a9f80cf532fc3", + "files": { + + }, + "public": true, + "created_at": "2023-07-24T19:09:01Z", + "updated_at": "2023-07-24T19:09:02Z", + "description": "Hello World Examples", + "comments": 0, + "user": null, + "comments_url": "https://api.github.com/gists/f87cbd23c4bc9ad7b04a9f80cf532fc3/comments", + "owner": { + "login": "armcknight", + "id": 3241469, + "node_id": "MDQ6VXNlcjMyNDE0Njk=", + "avatar_url": "https://avatars.githubusercontent.com/u/3241469?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/armcknight", + "html_url": "https://github.com/armcknight", + "followers_url": "https://api.github.com/users/armcknight/followers", + "following_url": "https://api.github.com/users/armcknight/following{/other_user}", + "gists_url": "https://api.github.com/users/armcknight/gists{/gist_id}", + "starred_url": "https://api.github.com/users/armcknight/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/armcknight/subscriptions", + "organizations_url": "https://api.github.com/users/armcknight/orgs", + "repos_url": "https://api.github.com/users/armcknight/repos", + "events_url": "https://api.github.com/users/armcknight/events{/privacy}", + "received_events_url": "https://api.github.com/users/armcknight/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } + }, "forks" : [ { "created_at" : "2011-04-14T16:00:49Z", @@ -133,4 +174,4 @@ "updated_at" : "2011-06-20T11:34:15Z", "url" : "https:\/\/api.github.com\/gists\/aa5a315d61ae9438b18d", "user" : null -} \ No newline at end of file +} diff --git a/Tests/OctoKitTests/Fixtures/starred_gists.json b/Tests/OctoKitTests/Fixtures/starred_gists.json new file mode 100644 index 0000000..a47097b --- /dev/null +++ b/Tests/OctoKitTests/Fixtures/starred_gists.json @@ -0,0 +1,49 @@ +[ + { + "comments" : 0, + "comments_url" : "https:\/\/api.github.com\/gists\/aa5a315d61ae9438b18d\/comments\/", + "commits_url" : "https:\/\/api.github.com\/gists\/aa5a315d61ae9438b18d\/commits", + "created_at" : "2010-04-14T02:15:15Z", + "description" : "Hello World Starred Examples", + "files" : { + "hello_world.rb" : { + "filename" : "hello_world.rb", + "language" : "Ruby", + "raw_url" : "https:\/\/gist.githubusercontent.com\/octocat\/6cad326836d38bd3a7ae\/raw\/db9c55113504e46fa076e7df3a04ce592e2e86d8\/hello_world.rb", + "size" : 167, + "type" : "application\/x-ruby" + } + }, + "forks_url" : "https:\/\/api.github.com\/gists\/aa5a315d61ae9438b18d\/forks", + "git_pull_url" : "https:\/\/gist.github.com\/aa5a315d61ae9438b18d.git", + "git_push_url" : "https:\/\/gist.github.com\/aa5a315d61ae9438b18d.git", + "html_url" : "https:\/\/gist.github.com\/aa5a315d61ae9438b18d", + "id" : "aa5a315d61ae9438b18d", + "node_id" : "MDQ6R2lzdGFhNWEzMTVkNjFhZTk0MzhiMThk", + "owner" : { + "avatar_url" : "https:\/\/github.com\/images\/error\/octocat_happy.gif", + "events_url" : "https:\/\/api.github.com\/users\/octocat\/events{\/privacy}", + "followers_url" : "https:\/\/api.github.com\/users\/octocat\/followers", + "following_url" : "https:\/\/api.github.com\/users\/octocat\/following{\/other_user}", + "gists_url" : "https:\/\/api.github.com\/users\/octocat\/gists{\/gist_id}", + "gravatar_id" : "", + "html_url" : "https:\/\/github.com\/octocat", + "id" : 1, + "login" : "octocat", + "node_id" : "MDQ6VXNlcjE=", + "organizations_url" : "https:\/\/api.github.com\/users\/octocat\/orgs", + "received_events_url" : "https:\/\/api.github.com\/users\/octocat\/received_events", + "repos_url" : "https:\/\/api.github.com\/users\/octocat\/repos", + "site_admin" : false, + "starred_url" : "https:\/\/api.github.com\/users\/octocat\/starred{\/owner}{\/repo}", + "subscriptions_url" : "https:\/\/api.github.com\/users\/octocat\/subscriptions", + "type" : "User", + "url" : "https:\/\/api.github.com\/users\/octocat" + }, + "public" : true, + "truncated" : false, + "updated_at" : "2011-06-20T11:34:15Z", + "url" : "https:\/\/api.github.com\/gists\/aa5a315d61ae9438b18d", + "user" : null + } +] \ No newline at end of file diff --git a/Tests/OctoKitTests/GistTests.swift b/Tests/OctoKitTests/GistTests.swift index 371bd42..3570c43 100644 --- a/Tests/OctoKitTests/GistTests.swift +++ b/Tests/OctoKitTests/GistTests.swift @@ -44,6 +44,46 @@ class GistTests: XCTestCase { } #endif + func testGetMyStarredGists() { + let config = TokenConfiguration("user:12345") + let headers = Helper.makeAuthHeader(username: "user", password: "12345") + + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/gists/starred?page=1&per_page=100", + expectedHTTPMethod: "GET", + expectedHTTPHeaders: headers, + jsonFile: "starred_gists", + statusCode: 200) + let task = Octokit(config, session: session).myStarredGists { response in + switch response { + case let .success(gists): + XCTAssertEqual(gists.count, 1) + XCTAssertEqual(gists[0].description, "Hello World Starred Examples") + case let .failure(error): + XCTAssertNil(error) + } + } + XCTAssertNotNil(task) + XCTAssertTrue(session.wasCalled) + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func testGetMyStarredGistsAsync() async throws { + let config = TokenConfiguration("user:12345") + let headers = Helper.makeAuthHeader(username: "user", password: "12345") + + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/gists/starred?page=1&per_page=100", + expectedHTTPMethod: "GET", + expectedHTTPHeaders: headers, + jsonFile: "starred_gists", + statusCode: 200) + let gists = try await Octokit(config, session: session).myStarredGists() + XCTAssertEqual(gists.count, 1) + XCTAssertEqual(gists[0].description, "Hello World Starred Examples") + XCTAssertTrue(session.wasCalled) + } + #endif + func testGetGists() { let config = TokenConfiguration("user:12345") let headers = Helper.makeAuthHeader(username: "user", password: "12345") @@ -88,6 +128,11 @@ class GistTests: XCTestCase { switch response { case let .success(gist): XCTAssertEqual(gist.id, "aa5a315d61ae9438b18d") + guard let forkID = gist.forkOf?.id else { + XCTFail("Didn't properly decode fork parent gist") + return + } + XCTAssertEqual(forkID, "f87cbd23c4bc9ad7b04a9f80cf532fc3") case .failure: XCTFail("should not get an error") }