├── .github ├── CODEOWNERS └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── Documentation └── Reference │ ├── README.md │ ├── enums │ └── GitHubError.md │ ├── extensions │ └── GitHubError.md │ └── structs │ ├── GitHub.md │ ├── PullRequest.md │ ├── PullRequestConnection.md │ ├── QueryResult.md │ ├── Reference.md │ ├── Release.md │ ├── ReleaseConnection.md │ ├── Repository.md │ ├── Request.md │ └── Response.md ├── Package.swift ├── README.md ├── Sources └── GitHub │ ├── Client.swift │ ├── GitHub.swift │ ├── GitHubError.swift │ └── Models.swift ├── Tests ├── GitHubTests │ └── GitHubTests.swift └── LinuxMain.swift └── codecov.yml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @eneko 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: eneko 4 | patreon: eneko 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | .swiftpm/ 42 | *.xcodeproj 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | matrix: 4 | include: 5 | - os: linux 6 | dist: trusty 7 | sudo: required 8 | - os: osx 9 | osx_image: xcode10.3 10 | 11 | install: 12 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 13 | eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; 14 | swiftenv install 5.0; 15 | swiftenv local 5.0; 16 | fi 17 | - swift --version 18 | 19 | script: 20 | - swift test --parallel 21 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 22 | swift package generate-xcodeproj --enable-code-coverage; 23 | xcodebuild -scheme GitHub-Package test; 24 | fi 25 | 26 | after_success: 27 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 28 | bash <(curl -s https://codecov.io/bash); 29 | fi 30 | -------------------------------------------------------------------------------- /Documentation/Reference/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | This Reference Documentation has been generated with 3 | [SourceDocs](https://github.com/eneko/SourceDocs). 4 | 5 | ## Structs 6 | 7 | - [GitHub](structs/GitHub.md) 8 | - [PullRequest](structs/PullRequest.md) 9 | - [PullRequestConnection](structs/PullRequestConnection.md) 10 | - [QueryResult](structs/QueryResult.md) 11 | - [Reference](structs/Reference.md) 12 | - [Release](structs/Release.md) 13 | - [ReleaseConnection](structs/ReleaseConnection.md) 14 | - [Repository](structs/Repository.md) 15 | - [Request](structs/Request.md) 16 | - [Response](structs/Response.md) 17 | 18 | ## Enums 19 | 20 | - [GitHubError](enums/GitHubError.md) 21 | 22 | ## Extensions 23 | 24 | - [GitHubError](extensions/GitHubError.md) 25 | -------------------------------------------------------------------------------- /Documentation/Reference/enums/GitHubError.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `GitHubError` 4 | 5 | ```swift 6 | public enum GitHubError: Error 7 | ``` 8 | 9 | ## Cases 10 | ### `requestTimedOut` 11 | 12 | ```swift 13 | case requestTimedOut 14 | ``` 15 | 16 | ### `invalidResponse` 17 | 18 | ```swift 19 | case invalidResponse 20 | ``` 21 | 22 | ### `repositoryNotFound` 23 | 24 | ```swift 25 | case repositoryNotFound(name: String) 26 | ``` 27 | -------------------------------------------------------------------------------- /Documentation/Reference/extensions/GitHubError.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `GitHubError` 4 | 5 | ## Properties 6 | ### `localizedDescription` 7 | 8 | ```swift 9 | public var localizedDescription: String 10 | ``` 11 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/GitHub.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `GitHub` 4 | 5 | ```swift 6 | public struct GitHub 7 | ``` 8 | 9 | ## Methods 10 | ### `init(token:)` 11 | 12 | ```swift 13 | public init(token: String) 14 | ``` 15 | 16 | ### `submit(query:)` 17 | 18 | ```swift 19 | public func submit(query: String) throws -> Response 20 | ``` 21 | 22 | ### `latestRelease(owner:project:)` 23 | 24 | ```swift 25 | public func latestRelease(owner: String, project: String) throws -> String? 26 | ``` 27 | 28 | ### `openPullRequests(owner:project:limit:)` 29 | 30 | ```swift 31 | public func openPullRequests(owner: String, project: String, limit: Int = 100) throws -> [PullRequest] 32 | ``` 33 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/PullRequest.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `PullRequest` 4 | 5 | ```swift 6 | public struct PullRequest: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/PullRequestConnection.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `PullRequestConnection` 4 | 5 | ```swift 6 | public struct PullRequestConnection: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/QueryResult.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `QueryResult` 4 | 5 | ```swift 6 | public struct QueryResult: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/Reference.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Reference` 4 | 5 | ```swift 6 | public struct Reference: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/Release.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Release` 4 | 5 | ```swift 6 | public struct Release: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/ReleaseConnection.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `ReleaseConnection` 4 | 5 | ```swift 6 | public struct ReleaseConnection: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/Repository.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Repository` 4 | 5 | ```swift 6 | public struct Repository: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/Request.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Request` 4 | 5 | ```swift 6 | public struct Request: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/structs/Response.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Response` 4 | 5 | ```swift 6 | public struct Response: Codable 7 | ``` 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "GitHub", 6 | products: [ 7 | .library(name: "GitHub", targets: ["GitHub"]), 8 | ], 9 | dependencies: [ 10 | ], 11 | targets: [ 12 | .target(name: "GitHub", dependencies: []), 13 | .testTarget(name: "GitHubTests", dependencies: ["GitHub"]), 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Release](https://img.shields.io/github/release/eneko/github.svg) 2 | ![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg) 3 | [![Build Status](https://travis-ci.org/eneko/GitHub.svg?branch=master)](https://travis-ci.org/eneko/GitHub) 4 | [![codecov](https://codecov.io/gh/eneko/GitHub/branch/master/graph/badge.svg)](https://codecov.io/gh/eneko/GitHub) 5 | [![Swift Package Manager Compatible](https://img.shields.io/badge/spm-compatible-brightgreen.svg)](https://swift.org/package-manager) 6 | ![Linux Compatible](https://img.shields.io/badge/linux-compatible%20🐧-brightgreen.svg) 7 | 8 | # GitHub GraphQL API V4 client 9 | 10 | This is a client for the [GitHub GraphQL API V4](https://developer.github.com/v4/). 11 | 12 | Note: This client is in very early stages and currently has extremely limited functionality: 13 | - Retrieve version tag for lastest release from a given repository. 14 | - Retrieve list of open pull requests from a given repository. 15 | 16 | ## Usage 17 | 18 | Initialize a client passing in a valid GitHub access token: 19 | ```swift 20 | let token = "your_token" 21 | let github = GitHub(token: token) 22 | ``` 23 | 24 | Retrieve latest release version of a given project: 25 | ```swift 26 | let version = try github.latestRelease(owner: "eneko", project: "SourceDocs") 27 | print(version) // 0.5.0 28 | ``` 29 | 30 | Retrieve list of open pull requests on a given project: 31 | ```swift 32 | let pullRequests = try github.openPullRequests(owner: "eneko", project: "SourceDocs") 33 | print(pullRequests.count) // 0 34 | ``` 35 | -------------------------------------------------------------------------------- /Sources/GitHub/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // GitHub 4 | // 5 | // Created by Eneko Alonso on 12/20/17. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct Client { 12 | 13 | let token: String 14 | 15 | typealias CompletionBlock = (_ response: Response?, _ error: Error?) -> Void 16 | 17 | func submit(request: Request) throws -> Response { 18 | let urlRequest = try makeURLRequest(with: request) 19 | return try submit(urlRequest: urlRequest) 20 | } 21 | 22 | func makeURLRequest(with request: Request) throws -> URLRequest { 23 | guard let url = URL(string: "https://api.github.com/graphql") else { 24 | fatalError("Failed to make url") 25 | } 26 | 27 | var urlRequest = URLRequest(url: url) 28 | urlRequest.httpMethod = "POST" 29 | urlRequest.setValue("bearer \(token)", forHTTPHeaderField: "Authorization") 30 | 31 | let encoder = JSONEncoder() 32 | urlRequest.httpBody = try encoder.encode(request) 33 | 34 | return urlRequest 35 | } 36 | 37 | func submit(urlRequest: URLRequest) throws -> Response { 38 | var responseData: Response? 39 | let semaphore = DispatchSemaphore(value: 0) 40 | submit(urlRequest: urlRequest) { (response, error) in 41 | responseData = response 42 | semaphore.signal() 43 | } 44 | if semaphore.wait(timeout: DispatchTime.now() + 30) == .timedOut { 45 | throw GitHubError.requestTimedOut 46 | } 47 | guard let response = responseData else { 48 | throw GitHubError.invalidResponse 49 | } 50 | return response 51 | } 52 | 53 | func submit(urlRequest: URLRequest, completion: @escaping CompletionBlock) { 54 | let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: OperationQueue()) 55 | let task = session.dataTask(with: urlRequest) { (data, response, error) in 56 | guard let data = data else { 57 | completion(nil, error) 58 | return 59 | } 60 | let decoder = JSONDecoder() 61 | guard let response = try? decoder.decode(Response.self, from: data) else { 62 | completion(nil, GitHubError.invalidResponse) 63 | return 64 | } 65 | completion(response, nil) 66 | } 67 | task.resume() 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/GitHub/GitHub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHub.swift 3 | // GitHub 4 | // 5 | // Created by Eneko Alonso on 12/19/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GitHub { 11 | let client: Client 12 | 13 | public init(token: String) { 14 | client = Client(token: token) 15 | } 16 | 17 | public func submit(query: String) throws -> Response { 18 | return try client.submit(request: Request(query: query)) 19 | } 20 | 21 | public func latestRelease(owner: String, project: String) throws -> String? { 22 | let query = """ 23 | query { 24 | repository(owner: "\(owner)", name: "\(project)") { 25 | releases(last: 1) { 26 | nodes { 27 | name 28 | tag { 29 | id 30 | name 31 | } 32 | } 33 | } 34 | } 35 | } 36 | """ 37 | 38 | let response = try submit(query: query) 39 | guard let repository = response.data.repository else { 40 | throw GitHubError.repositoryNotFound(name: project) 41 | } 42 | return repository.releases?.nodes?.first?.tag?.name 43 | } 44 | 45 | public func openPullRequests(owner: String, project: String, limit: Int = 100) throws -> [PullRequest] { 46 | let query = """ 47 | query { 48 | repository(owner: "\(owner)", name: "\(project)") { 49 | pullRequests(states: [OPEN], first: \(limit)) { 50 | nodes { 51 | createdAt 52 | number 53 | title 54 | } 55 | } 56 | } 57 | } 58 | """ 59 | 60 | let response = try submit(query: query) 61 | guard let repository = response.data.repository else { 62 | throw GitHubError.repositoryNotFound(name: project) 63 | } 64 | return repository.pullRequests?.nodes ?? [] 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/GitHub/GitHubError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHubError.swift 3 | // GitHubPackageDescription 4 | // 5 | // Created by Eneko Alonso on 12/20/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum GitHubError: Error { 11 | case requestTimedOut 12 | case invalidResponse 13 | case repositoryNotFound(name: String) 14 | } 15 | 16 | extension GitHubError: LocalizedError { 17 | public var localizedDescription: String { 18 | switch self { 19 | case .requestTimedOut: 20 | return "GitHub API request timed out." 21 | case .invalidResponse: 22 | return "Invalid response or no response received from GitHub API" 23 | case .repositoryNotFound(let name): 24 | return "Repository not found: \(name)" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/GitHub/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // GitHub 4 | // 5 | // Created by Eneko Alonso on 12/19/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Request: Codable { 11 | let query: String 12 | } 13 | 14 | public struct Response: Codable { 15 | let data: QueryResult 16 | } 17 | 18 | public struct QueryResult: Codable { 19 | let repository: Repository? 20 | } 21 | 22 | public struct Repository: Codable { 23 | let pullRequests: PullRequestConnection? 24 | let releases: ReleaseConnection? 25 | } 26 | 27 | public struct ReleaseConnection: Codable { 28 | let nodes: [Release]? 29 | } 30 | 31 | public struct Release: Codable { 32 | let name: String? 33 | let tag: Reference? 34 | } 35 | 36 | public struct PullRequestConnection: Codable { 37 | let nodes: [PullRequest]? 38 | } 39 | 40 | public struct PullRequest: Codable { 41 | let body: String? 42 | let closed: Bool? 43 | let merged: Bool? 44 | let number: Int? 45 | let title: String? 46 | } 47 | 48 | public struct Reference: Codable { 49 | let id: String 50 | let name: String 51 | } 52 | -------------------------------------------------------------------------------- /Tests/GitHubTests/GitHubTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import GitHub 3 | 4 | class GitHubTests: XCTestCase { 5 | 6 | func testClientToken() { 7 | XCTAssertEqual(GitHub(token: "foo").client.token, "foo") 8 | } 9 | 10 | func testLatestRelease() throws { 11 | guard let token = ProcessInfo.processInfo.environment["GITHUB_ACCESS_TOKEN"] else { 12 | XCTFail("No token") 13 | return 14 | } 15 | let release = try GitHub(token: token).latestRelease(owner: "eneko", project: "GitHubTest") 16 | XCTAssertEqual(release, "0.0.0") 17 | } 18 | 19 | func testPullRequests() throws { 20 | guard let token = ProcessInfo.processInfo.environment["GITHUB_ACCESS_TOKEN"] else { 21 | XCTFail("No token") 22 | return 23 | } 24 | let pullRequests = try GitHub(token: token).openPullRequests(owner: "eneko", project: "GitHubTest") 25 | XCTAssertEqual(pullRequests.first?.title, "Update README.md") 26 | } 27 | 28 | static var allTests = [ 29 | ("testClientToken", testClientToken), 30 | ("testLatestRelease", testLatestRelease) 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import GitHubTests 3 | 4 | XCTMain([ 5 | testCase(GitHubTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false # if true: only post the comment if coverage changes 5 | require_base: no # [yes :: must have a base report to post] 6 | require_head: yes # [yes :: must have a head report to post] 7 | branches: null 8 | 9 | --------------------------------------------------------------------------------