From 9d238d35887b2b7f36df1110343cfb2ed9852744 Mon Sep 17 00:00:00 2001 From: Chidi Williams Date: Tue, 6 Feb 2024 07:07:15 +0000 Subject: [PATCH] Add search code endpoint (#182) * Add search code * Fix query param key * Fix search query key * Fix linting --- OctoKit/Search.swift | 162 +++++++++++++++++++ Tests/OctoKitTests/Fixtures/search_code.json | 82 ++++++++++ Tests/OctoKitTests/SearchTests.swift | 59 +++++++ 3 files changed, 303 insertions(+) create mode 100644 OctoKit/Search.swift create mode 100644 Tests/OctoKitTests/Fixtures/search_code.json create mode 100644 Tests/OctoKitTests/SearchTests.swift diff --git a/OctoKit/Search.swift b/OctoKit/Search.swift new file mode 100644 index 00000000..53f82181 --- /dev/null +++ b/OctoKit/Search.swift @@ -0,0 +1,162 @@ +// +// Search.swift +// +// +// Created by Chidi Williams on 04/02/2024. +// + +import Foundation +import RequestKit +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +open class SearchResponse: Codable { + open var totalCount: Int + open var incompleteResults: Bool + open var items: [T] + + init(totalCount: Int, incompleteResults: Bool, items: [T]) { + self.totalCount = totalCount + self.incompleteResults = incompleteResults + self.items = items + } + + enum CodingKeys: String, CodingKey { + case totalCount = "total_count" + case incompleteResults = "incomplete_results" + case items + } +} + +open class CodeSearchResultItem: Codable { + open var name: String + open var path: String + open var sha: String + open var url: URL + open var gitUrl: URL + open var htmlUrl: URL + open var repository: Repository + open var score: Double + open var fileSize: Int? + open var language: String? + open var lastModifiedAt: Date? + open var lineNumbers: [String]? + + init(name: String, + path: String, + sha: String, + url: URL, + gitUrl: URL, + htmlUrl: URL, + repository: Repository, + score: Double, + fileSize: Int? = nil, + language: String? = nil, + lastModifiedAt: Date? = nil, + lineNumbers: [String]? = nil) { + self.name = name + self.path = path + self.sha = sha + self.url = url + self.gitUrl = gitUrl + self.htmlUrl = htmlUrl + self.repository = repository + self.score = score + self.fileSize = fileSize + self.language = language + self.lastModifiedAt = lastModifiedAt + self.lineNumbers = lineNumbers + } + + enum CodingKeys: String, CodingKey { + case name, path, sha, url + case gitUrl = "git_url" + case htmlUrl = "html_url" + case repository, score + case fileSize = "file_size" + case language + case lastModifiedAt = "last_modified_at" + case lineNumbers = "line_numbers" + } +} + +// MARK: request + +public extension Octokit { + /** + Searches for query terms inside of a file + - parameter query: The query containing one or more search keywords and qualifiers. + - parameter page: Current page for label pagination. `1` by default. + - parameter perPage: Number of labels per page. `100` by default. + - parameter completion: Callback for the outcome of the fetch. + */ + @discardableResult + func searchCode(query: String, + page: String = "1", + perPage: String = "100", + completion: @escaping (_ response: Result, Error>) -> Void) -> URLSessionDataTaskProtocol? { + let router = SearchRouter.searchCode(configuration, query, page, perPage) + return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: SearchResponse.self) { response, error in + if let error = error { + completion(.failure(error)) + } else { + if let response = response { + completion(.success(response)) + } + } + } + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + /** + Searches for query terms inside of a file + - parameter query: The query containing one or more search keywords and qualifiers. + - parameter page: Current page for label pagination. `1` by default. + - parameter perPage: Number of labels per page. `100` by default. + */ + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func searchCode(query: String, page: String = "1", perPage: String = "100") async throws -> SearchResponse { + let router = SearchRouter.searchCode(configuration, query, page, perPage) + return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: SearchResponse.self) + } + #endif +} + +enum SearchRouter: JSONPostRouter { + case searchCode(Configuration, String, String, String) + + var method: HTTPMethod { + switch self { + case .searchCode: + return .GET + } + } + + var encoding: HTTPEncoding { + switch self { + case .searchCode: + return .url + } + } + + var configuration: Configuration { + switch self { + case let .searchCode(config, _, _, _): return config + } + } + + var params: [String: Any] { + switch self { + case let .searchCode(_, query, page, perPage): + return ["q": query, "per_page": perPage, "page": page] + } + } + + var path: String { + switch self { + case .searchCode: + return "search/code" + } + } +} diff --git a/Tests/OctoKitTests/Fixtures/search_code.json b/Tests/OctoKitTests/Fixtures/search_code.json new file mode 100644 index 00000000..1f350495 --- /dev/null +++ b/Tests/OctoKitTests/Fixtures/search_code.json @@ -0,0 +1,82 @@ +{ + "total_count": 1, + "incomplete_results": false, + "items": [ + { + "name": "README", + "path": "README", + "sha": "980a0d5f19a64b4b30a87d4206aade58726b60e3", + "url": "https://api.github.com/repositories/1296269/contents/README?ref=7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", + "git_url": "https://api.github.com/repositories/1296269/git/blobs/980a0d5f19a64b4b30a87d4206aade58726b60e3", + "html_url": "https://github.com/octocat/Hello-World/blob/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d/README", + "repository": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "private": false, + "owner": { + "login": "octocat", + "id": 583231, + "node_id": "MDQ6VXNlcjU4MzIzMQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "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}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/octocat/Hello-World", + "description": "My first repository on GitHub!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments" + }, + "score": 1.0 + } + ] +} diff --git a/Tests/OctoKitTests/SearchTests.swift b/Tests/OctoKitTests/SearchTests.swift new file mode 100644 index 00000000..40bcc535 --- /dev/null +++ b/Tests/OctoKitTests/SearchTests.swift @@ -0,0 +1,59 @@ +// +// SearchTests.swift +// +// +// Created by Chidi Williams on 04/02/2024. +// + +import OctoKit +import XCTest + +final class SearchTests: XCTestCase { + // MARK: Request Tests + + func testSearchCode() { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/search/code?page=1&per_page=100&q=hello%2Brepo%3Aoctocat/hello-world", expectedHTTPMethod: "GET", + jsonFile: "search_code", statusCode: 200) + let task = Octokit(session: session).searchCode(query: "hello+repo:octocat/hello-world") { response in + switch response { + case let .success(result): + XCTAssertEqual(result.totalCount, 1) + XCTAssertEqual(result.items.count, 1) + case let .failure(error): + print(error) + XCTFail("should not get an 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 testSearchCodeAsync() async throws { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/search/code?page=1&per_page=100&q=hello%2Brepo%3Aoctocat/hello-world", + expectedHTTPMethod: "GET", + jsonFile: "search_code", + statusCode: 200) + let response = try await Octokit(session: session).searchCode(query: "hello+repo:octocat/hello-world") + XCTAssertEqual(response.totalCount, 1) + XCTAssertEqual(response.items.count, 1) + XCTAssertTrue(session.wasCalled) + } + #endif + + func testSearchCodeSetsPagination() { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/search/code?page=2&per_page=50&q=hello%2Brepo%3Aoctocat/hello-world", expectedHTTPMethod: "GET", jsonFile: nil, + statusCode: 200) + let task = Octokit(session: session).searchCode(query: "hello+repo:octocat/hello-world", page: "2", perPage: "50") { response in + switch response { + case .success: + XCTAssert(true) + case .failure: + XCTFail("should not get an error") + } + } + XCTAssertNotNil(task) + XCTAssertTrue(session.wasCalled) + } +}