Skip to content

Commit

Permalink
Add search code endpoint (#182)
Browse files Browse the repository at this point in the history
* Add search code

* Fix query param key

* Fix search query key

* Fix linting
  • Loading branch information
chidiwilliams authored Feb 6, 2024
1 parent 30f8645 commit 9d238d3
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 0 deletions.
162 changes: 162 additions & 0 deletions OctoKit/Search.swift
Original file line number Diff line number Diff line change
@@ -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<T: Codable>: 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<SearchResponse<CodeSearchResultItem>, Error>) -> Void) -> URLSessionDataTaskProtocol? {
let router = SearchRouter.searchCode(configuration, query, page, perPage)
return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: SearchResponse<CodeSearchResultItem>.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<CodeSearchResultItem> {
let router = SearchRouter.searchCode(configuration, query, page, perPage)
return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: SearchResponse<CodeSearchResultItem>.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"
}
}
}
82 changes: 82 additions & 0 deletions Tests/OctoKitTests/Fixtures/search_code.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
59 changes: 59 additions & 0 deletions Tests/OctoKitTests/SearchTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 9d238d3

Please sign in to comment.