diff --git a/.gitmodules b/.gitmodules index d69abd3..55a69dd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,6 @@ [submodule "Carthage/Checkouts/Result"] path = Carthage/Checkouts/Result url = https://github.com/antitypical/Result.git -[submodule "Carthage/Checkouts/Argo"] - path = Carthage/Checkouts/Argo - url = https://github.com/thoughtbot/Argo.git -[submodule "Carthage/Checkouts/Curry"] - path = Carthage/Checkouts/Curry - url = https://github.com/thoughtbot/Curry.git [submodule "Carthage/Checkouts/ReactiveSwift"] path = Carthage/Checkouts/ReactiveSwift url = https://github.com/ReactiveCocoa/ReactiveSwift.git -[submodule "Carthage/Checkouts/Runes"] - path = Carthage/Checkouts/Runes - url = https://github.com/thoughtbot/Runes.git diff --git a/.swift-version b/.swift-version index 8c50098..5186d07 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.1 +4.0 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..f85f232 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,4 @@ +included: + - Sources +whitelist_rules: + - trailing_closure diff --git a/.travis.yml b/.travis.yml index 866869c..ae2b8c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -osx_image: xcode8.3 +osx_image: xcode9 branches: only: - master @@ -9,33 +9,13 @@ script: script/cibuild "$TRAVIS_XCODE_WORKSPACE" "$TRAVIS_XCODE_SCHEME" "$XCODE_ xcode_workspace: Tentacle.xcworkspace matrix: include: - - osx_image: xcode8.3 - xcode_scheme: Tentacle-OSX + - xcode_scheme: Tentacle-OSX env: XCODE_ACTION="build-for-testing test-without-building" - - osx_image: xcode8.3 - xcode_scheme: Tentacle-iOS + - xcode_scheme: Tentacle-iOS env: XCODE_ACTION="build-for-testing test-without-building" - - osx_image: xcode8.3 - xcode_scheme: update-test-fixtures + - xcode_scheme: update-test-fixtures env: XCODE_ACTION=build - - osx_image: xcode8.3 - before_script: true - script: - - swift build - - swift test - env: - - JOB=SWIFTPM_DARWIN - - osx_image: xcode9 - xcode_scheme: Tentacle-OSX - env: XCODE_ACTION="build-for-testing test-without-building" - - osx_image: xcode9 - xcode_scheme: Tentacle-iOS - env: XCODE_ACTION="build-for-testing test-without-building" - - osx_image: xcode9 - xcode_scheme: update-test-fixtures - env: XCODE_ACTION=build - - osx_image: xcode9 - before_script: true + - before_script: true script: - swift build - swift test diff --git a/Cartfile b/Cartfile index 712a25f..229fab6 100644 --- a/Cartfile +++ b/Cartfile @@ -1,3 +1 @@ github "ReactiveCocoa/ReactiveSwift" ~> 2.0 -github "thoughtbot/Argo" ~> 4.1.2 -github "thoughtbot/Curry" ~> 3.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index a8a88b6..7731d9d 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,5 +1,2 @@ -github "ReactiveCocoa/ReactiveSwift" "2.0.0" +github "ReactiveCocoa/ReactiveSwift" "2.0.1" github "antitypical/Result" "3.2.3" -github "thoughtbot/Argo" "v4.1.2" -github "thoughtbot/Curry" "v3.0.0" -github "thoughtbot/Runes" "v4.0.1" diff --git a/Carthage/Checkouts/Argo b/Carthage/Checkouts/Argo deleted file mode 160000 index 203daf8..0000000 --- a/Carthage/Checkouts/Argo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 203daf8fbebff84fb8d560acd3cad32a067b6189 diff --git a/Carthage/Checkouts/Curry b/Carthage/Checkouts/Curry deleted file mode 160000 index c9f1a37..0000000 --- a/Carthage/Checkouts/Curry +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c9f1a37e0e12ad38af7933b9386bdc5f9e6222dc diff --git a/Carthage/Checkouts/ReactiveSwift b/Carthage/Checkouts/ReactiveSwift index 6147571..b9d5b35 160000 --- a/Carthage/Checkouts/ReactiveSwift +++ b/Carthage/Checkouts/ReactiveSwift @@ -1 +1 @@ -Subproject commit 614757120eb91a59a7421ad11c48614ca9ac04c0 +Subproject commit b9d5b350a446b85704396ce332a1f9e4960cfc6b diff --git a/Carthage/Checkouts/Runes b/Carthage/Checkouts/Runes deleted file mode 160000 index 727fcfe..0000000 --- a/Carthage/Checkouts/Runes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 727fcfe8d79ec92ce989a96329147190d20f7cd2 diff --git a/Package.pins b/Package.pins deleted file mode 100644 index a5d1aeb..0000000 --- a/Package.pins +++ /dev/null @@ -1,36 +0,0 @@ -{ - "autoPin": true, - "pins": [ - { - "package": "Argo", - "reason": null, - "repositoryURL": "https://github.com/thoughtbot/Argo.git", - "version": "4.1.2" - }, - { - "package": "Curry", - "reason": null, - "repositoryURL": "https://github.com/thoughtbot/Curry.git", - "version": "3.0.0" - }, - { - "package": "ReactiveSwift", - "reason": null, - "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", - "version": "2.0.0" - }, - { - "package": "Result", - "reason": null, - "repositoryURL": "https://github.com/antitypical/Result.git", - "version": "3.2.3" - }, - { - "package": "Runes", - "reason": null, - "repositoryURL": "https://github.com/thoughtbot/Runes.git", - "version": "4.1.0" - } - ], - "version": 1 -} \ No newline at end of file diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..aac20b3 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "ReactiveSwift", + "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", + "state": { + "branch": null, + "revision": "b9d5b350a446b85704396ce332a1f9e4960cfc6b", + "version": "2.0.1" + } + }, + { + "package": "Result", + "repositoryURL": "https://github.com/antitypical/Result.git", + "state": { + "branch": null, + "revision": "7477584259bfce2560a19e06ad9f71db441fff11", + "version": "3.2.4" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index aa2c61e..dd2bfb2 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,17 @@ +// swift-tools-version:4.0 import PackageDescription let package = Package( name: "Tentacle", + products: [ + .library(name: "Tentacle", targets: ["Tentacle"]), + ], dependencies: [ - .Package(url: "https://github.com/thoughtbot/Argo.git", versions: Version(4, 1, 2).. JSON -} - -extension JSON { - func JSONObject() -> Any { - switch self { - case .null: - return NSNull() - case .string(let value): - return value - case .number(let value): - return value - case .array(let array): - return array.map { $0.JSONObject() } - case .bool(let value): - return value - case .object(let object): - var dict: [String : Any] = [:] - for (key, value) in object { - dict[key] = value.JSONObject() - } - return dict as Any - } - } -} - -internal func decode(_ json: JSON) -> Result where T == T.DecodedType { - switch T.decode(json) { - case let .success(object): - return .success(object) - case let .failure(error): - return .failure(error) - } -} - -internal func decode(_ json: JSON) -> Result<[T], DecodeError> where T == T.DecodedType { - switch [T].decode(json) { - case let .success(object): - return .success(object) - case let .failure(error): - return .failure(error) - } -} - -internal func toString(_ number: Int) -> Decoded { - return .success(number.description) -} - -internal func toIdentifier(_ number: Int) -> Decoded> { - return .success(ID(rawValue: number)) -} - -internal func toInt(_ string: String) -> Decoded { - if let int = Int(string) { - return .success(int) - } else { - return .failure(.custom("String is not a valid number")) - } -} - -internal func toIssueState(_ string: String) -> Decoded { - if let state = Issue.State(rawValue: string) { - return .success(state) - } else { - return .failure(.custom("String \(string) does not represent a valid issue state")) - } -} - -internal func toDate(_ string: String) -> Decoded { - if let date = DateFormatter.iso8601.date(from: string) { - return .success(date) - } else { - return .failure(.custom("Date is not ISO8601 formatted")) - } -} - -internal func toOptionalDate(_ string: String?) -> Decoded { - guard let string = string else { return .success(nil) } - if let date = DateFormatter.iso8601.date(from: string) { - return .success(date) - } else { - return .failure(.custom("Date is not ISO8601 formatted")) - } -} - -internal func toURL(_ string: String) -> Decoded { - if let url = URL(string: string) { - return .success(url) - } else { - return .failure(.custom("URL \(string) is not properly formatted")) - } -} - -internal func toOptionalURL(_ string: String?) -> Decoded { - guard let string = string else { return .success(nil) } - - return .success(URL(string: string)) -} - -internal func toColor(_ string: String) -> Decoded { - return .success(Color(hex: string)) -} - -internal func toUserType(_ string: String) -> Decoded { - if let type = UserInfo.UserType(rawValue: string) { - return .success(type) - } else { - return .failure(.custom("String \(string) does not represent a valid user type")) - } -} - -internal func toSHA(_ string: String) -> Decoded { - return .success(SHA(hash: string)) -} diff --git a/Sources/Tentacle/Author.swift b/Sources/Tentacle/Author.swift index 62db82f..52c8414 100644 --- a/Sources/Tentacle/Author.swift +++ b/Sources/Tentacle/Author.swift @@ -7,9 +7,8 @@ // import Foundation -import Argo -public struct Author { +public struct Author: ResourceType, Encodable { /// Name of the Author let name: String /// Email of the Author @@ -21,15 +20,6 @@ public struct Author { } } -extension Author: Encodable { - public func encode() -> JSON { - return JSON.object([ - "name": .string(name), - "email": .string(email) - ]) - } -} - extension Author: Hashable, Equatable { public var hashValue: Int { return name.hashValue ^ email.hashValue diff --git a/Sources/Tentacle/Branch.swift b/Sources/Tentacle/Branch.swift index e0d22fd..ee8592f 100644 --- a/Sources/Tentacle/Branch.swift +++ b/Sources/Tentacle/Branch.swift @@ -7,9 +7,6 @@ // import Foundation -import Argo -import Runes -import Curry extension Repository { /// A request for the branches in the repository. @@ -20,36 +17,43 @@ extension Repository { } } -public struct Branch { +public struct Branch: ResourceType { + + public struct Commit: Decodable { + public let sha: SHA + } /// Name of the branch public let name: String - /// Sha of the commit the branch points to - public let sha: SHA + /// The commit the branch points to + public let commit: Commit - public init(name: String, sha: SHA) { + public init(name: String, commit: Commit) { self.name = name - self.sha = sha + self.commit = commit } + } extension Branch: Hashable { public static func ==(lhs: Branch, rhs: Branch) -> Bool { - return lhs.name == rhs.name && lhs.sha == rhs.sha + return lhs.name == rhs.name && lhs.commit == rhs.commit } public var hashValue: Int { - return name.hashValue ^ sha.hashValue + return name.hashValue ^ commit.hashValue } } -extension Branch: ResourceType { - public static func decode(_ j: JSON) -> Decoded { - let f = curry(Branch.init) +extension Branch.Commit: Hashable { + public var hashValue: Int { + return sha.hashValue + } +} - return f - <^> j <| "name" - <*> j <| "commit" +extension Branch.Commit: Equatable { + public static func ==(lhs: Branch.Commit, rhs: Branch.Commit) -> Bool { + return lhs.sha == rhs.sha } } diff --git a/Sources/Tentacle/Client.swift b/Sources/Tentacle/Client.swift index 9d4e0d9..b32b595 100644 --- a/Sources/Tentacle/Client.swift +++ b/Sources/Tentacle/Client.swift @@ -6,17 +6,10 @@ // Copyright © 2016 Matt Diephouse. All rights reserved. // -import Argo import Foundation import ReactiveSwift import Result -extension JSONSerialization { - internal static func deserializeJSON(_ data: Data) -> Result { - return materialize(JSON(try JSONSerialization.jsonObject(with: data))) - } -} - extension URL { internal func url(with queryItems: [URLQueryItem]) -> URL { var components = URLComponents(url: self, resolvingAgainstBaseURL: true)! @@ -87,7 +80,7 @@ public final class Client { case jsonDeserializationError(Swift.Error) /// An error occurred while decoding JSON. - case jsonDecodingError(DecodeError) + case jsonDecodingError(DecodingError) /// A status code, response, and error that was returned from the API. case apiError(Int, Response, GitHubError) @@ -188,55 +181,45 @@ public final class Client { } /// Fetch a request from the API. - private func execute(_ request: Request, page: UInt?, perPage: UInt?) -> SignalProducer<(Response, JSON), Error> { + private func execute(_ request: Request, page: UInt?, perPage: UInt?) -> SignalProducer<(Response, Data), Error> { return urlSession .reactive .data(with: urlRequest(for: request, page: page, perPage: perPage)) .mapError { Error.networkError($0.error) } - .flatMap(.concat) { data, response -> SignalProducer<(Response, JSON), Error> in + .flatMap(.concat) { data, response -> SignalProducer<(Response, Data), Error> in let response = response as! HTTPURLResponse let headers = response.allHeaderFields as! [String:String] - // The explicitness is required to pick up - // `init(_ action: @escaping () -> Result)` - // over `init(_ action: @escaping () -> Value)`. - let producer: SignalProducer = SignalProducer { () -> Result in - return JSONSerialization.deserializeJSON(data).mapError { Error.jsonDeserializationError($0.error) } - } - return producer - .attemptMap { json in - if response.statusCode == 404 { - return .failure(.doesNotExist) - } - if response.statusCode >= 400 && response.statusCode < 600 { - return decode(json) - .mapError(Error.jsonDecodingError) - .flatMap { error in - .failure(Error.apiError(response.statusCode, Response(headerFields: headers), error)) - } - } - return .success(json) + return SignalProducer<(Response, Data), Error> { () -> Result<(Response, Data), Error> in + guard response.statusCode != 404 else { + return .failure(.doesNotExist) } - .map { json in - return (Response(headerFields: headers), json) + + if response.statusCode >= 400 && response.statusCode < 600 { + return decode(data) + .mapError(Error.jsonDecodingError) + .flatMap { error in + .failure(Error.apiError(response.statusCode, Response(headerFields: headers), error)) + } } + + return .success((Response(headerFields: headers), data)) + } } } - + /// Fetch an object from the API. public func execute( _ request: Request - ) -> SignalProducer<(Response, Resource), Error> where Resource.DecodedType == Resource { + ) -> SignalProducer<(Response, Resource), Error> { return execute(request, page: nil, perPage: nil) - .attemptMap { response, json in - return decode(json) - .map { resource in - (response, resource) - } + .attemptMap { response, data -> Result<(Response, Resource), Client.Error> in + return decode(data) + .map { (response, $0) } .mapError(Error.jsonDecodingError) } } - + /// Fetch a list of objects from the API. /// /// This method will automatically fetch all pages. Each value in the returned signal producer @@ -245,19 +228,22 @@ public final class Client { _ request: Request<[Resource]>, page: UInt? = 1, perPage: UInt? = 30 - ) -> SignalProducer<(Response, [Resource]), Error> where Resource.DecodedType == Resource { + ) -> SignalProducer<(Response, [Resource]), Error> { let nextPage = (page ?? 1) + 1 + return execute(request, page: page, perPage: perPage) - .attemptMap { (response: Response, json: JSON) in - return decode(json) - .map { resource in - (response, resource) - } + .attemptMap { response, data -> Result<(Response, [Resource]), Client.Error> in + return decodeList(data) + .map { (response, $0) } .mapError(Error.jsonDecodingError) } - .flatMap(.concat) { response, json -> SignalProducer<(Response, [Resource]), Error> in - return SignalProducer(value: (response, json)) - .concat(response.links["next"] == nil ? SignalProducer.empty : self.execute(request, page: nextPage, perPage: perPage)) + .flatMap(.concat) { response, data -> SignalProducer<(Response, [Resource]), Error> in + let current = SignalProducer<(Response, [Resource]), Error>(value: (response, data)) + guard let _ = response.links["next"] else { + return current + } + + return current.concat(self.execute(request, page: nextPage, perPage: perPage)) } } } @@ -294,7 +280,7 @@ extension Client.Error: Hashable { return (error as NSError).hashValue case let .jsonDecodingError(error): - return error.hashValue + return (error as NSError).hashValue case let .apiError(statusCode, response, error): return statusCode.hashValue ^ response.hashValue ^ error.hashValue diff --git a/Sources/Tentacle/Comment.swift b/Sources/Tentacle/Comment.swift index 03ece8c..d8cd510 100644 --- a/Sources/Tentacle/Comment.swift +++ b/Sources/Tentacle/Comment.swift @@ -7,9 +7,6 @@ // import Foundation -import Curry -import Argo -import Runes extension Repository { /// A request for the comments on the given issue. @@ -20,7 +17,7 @@ extension Repository { } } -public struct Comment: CustomStringConvertible, Identifiable { +public struct Comment: CustomStringConvertible, ResourceType, Identifiable { /// The id of the issue public let id: ID @@ -38,6 +35,17 @@ public struct Comment: CustomStringConvertible, Identifiable { public var description: String { return body } + + private enum CodingKeys: String, CodingKey { + case id + case url = "html_url" + case createdAt = "created_at" + case updatedAt = "updated_at" + case body + case author = "user" + + + } } extension Comment: Hashable { @@ -52,16 +60,3 @@ extension Comment: Hashable { } } -extension Comment: ResourceType { - public static func decode(_ j: JSON) -> Decoded { - let f = curry(Comment.init) - - return f - <^> (j <| "id" >>- toIdentifier) - <*> (j <| "html_url" >>- toURL) - <*> (j <| "created_at" >>- toDate) - <*> (j <| "updated_at" >>- toDate) - <*> j <| "body" - <*> j <| "user" - } -} diff --git a/Sources/Tentacle/Commit.swift b/Sources/Tentacle/Commit.swift index 5919d64..7cd6559 100644 --- a/Sources/Tentacle/Commit.swift +++ b/Sources/Tentacle/Commit.swift @@ -7,11 +7,8 @@ // import Foundation -import Argo -import Curry -import Runes -public struct Commit { +public struct Commit: ResourceType { /// SHA of the commit public let sha: SHA @@ -30,7 +27,7 @@ public struct Commit { /// Parents commits public let parents: [Parent] - public struct Parent { + public struct Parent: ResourceType { /// URL to see the parent commit in a browser public let url: URL @@ -38,7 +35,7 @@ public struct Commit { public let sha: SHA } - public struct Author { + public struct Author: ResourceType { /// Date the author made the commit public let date: Date /// Name of the author @@ -48,7 +45,7 @@ public struct Commit { } } -extension Commit: ResourceType { +extension Commit { public var hashValue: Int { return sha.hashValue } @@ -56,26 +53,9 @@ extension Commit: ResourceType { public static func ==(lhs: Commit, rhs: Commit) -> Bool { return lhs.sha == rhs.sha } - - public static func decode(_ j: JSON) -> Decoded { - return curry(Commit.init) - <^> (j <| "sha" >>- toSHA) - <*> j <| "author" - <*> j <| "committer" - <*> j <| "message" - <*> (j <| "url" >>- toURL) - <*> j <|| "parents" - } } -extension Commit.Author: ResourceType { - public static func decode(_ j: JSON) -> Decoded { - return curry(Commit.Author.init) - <^> (j <| "date" >>- toDate) - <*> j <| "name" - <*> j <| "email" - } - +extension Commit.Author { public var hashValue: Int { return date.hashValue ^ name.hashValue ^ email.hashValue } @@ -87,13 +67,7 @@ extension Commit.Author: ResourceType { } } -extension Commit.Parent: ResourceType { - public static func decode(_ j: JSON) -> Decoded { - return curry(Commit.Parent.init) - <^> (j <| "url" >>- toURL) - <*> (j <| "sha" >>- toSHA) - } - +extension Commit.Parent { public var hashValue: Int { return sha.hashValue ^ url.hashValue } @@ -103,4 +77,3 @@ extension Commit.Parent: ResourceType { && lhs.url == rhs.url } } - diff --git a/Sources/Tentacle/Content.swift b/Sources/Tentacle/Content.swift index 593281f..c1d6f5e 100644 --- a/Sources/Tentacle/Content.swift +++ b/Sources/Tentacle/Content.swift @@ -7,9 +7,6 @@ // import Foundation -import Argo -import Curry -import Runes extension Repository { /// A request for the content at a given path in the repository. @@ -31,9 +28,16 @@ extension Repository { /// /// - file: a file when queried directly in a repository /// - directory: a directory when queried directly in a repository (may contain multiple files) -public enum Content { +public enum Content: ResourceType { /// A file in a repository - public struct File: CustomStringConvertible { + public struct File: CustomStringConvertible, Decodable { + + public enum ContentTypeName: String, Decodable { + case file + case directory = "dir" + case symlink + case submodule + } /// Type of content in a repository /// @@ -41,7 +45,7 @@ public enum Content { /// - directory: a directory in a repository /// - symlink: a symlink in a repository not targeting a file inside the same repository /// - submodule: a submodule in a repository - public enum ContentType { + public enum ContentType: Decodable { /// A file a in a repository case file(size: Int, downloadURL: URL?) @@ -57,6 +61,37 @@ public enum Content { /// when they are the result of a query for a directory /// See https://developer.github.com/v3/repos/contents/ case submodule(url: String?) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(ContentTypeName.self, forKey: .type) + switch type { + case .file: + let size = try container.decode(Int.self, forKey: .size) + if let url = try container.decodeIfPresent(URL.self, forKey: .downloadURL) { + self = .file(size: size, downloadURL: url) + } else { + self = .submodule(url: nil) + } + case .directory: + self = .directory + case .submodule: + let url = try container.decodeIfPresent(String.self, forKey: .submoduleURL) + self = .submodule(url: url) + case .symlink: + let target = try container.decodeIfPresent(String.self, forKey: .target) + let url = try container.decodeIfPresent(URL.self, forKey: .downloadURL) + self = .symlink(target: target, downloadURL: url) + } + } + + private enum CodingKeys: String, CodingKey { + case type + case size + case target + case downloadURL = "download_url" + case submoduleURL = "submodule_git_url" + } } /// The type of content @@ -78,54 +113,50 @@ public enum Content { return name } - } - - case file(File) - case directory([File]) -} + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) -func decodeFile(_ j: JSON) -> Decoded { - return curry(Content.File.ContentType.file) - <^> j <| "size" - <*> (j <|? "download_url" >>- toOptionalURL) -} + self.name = try container.decode(String.self, forKey: .name) + self.path = try container.decode(String.self, forKey: .path) + self.sha = try container.decode(String.self, forKey: .sha) + self.url = try container.decode(URL.self, forKey: .url) + self.content = try ContentType(from: decoder) + } -func decodeSymlink(_ j: JSON) -> Decoded { - return curry(Content.File.ContentType.symlink) - <^> j <|? "target" - <*> (j <|? "download_url" >>- toOptionalURL) -} + public init(content: ContentType, name: String, path: String, sha: String, url: URL) { + self.name = name + self.path = path + self.sha = sha + self.url = url + self.content = content + } -func decodeSubmodule(_ j: JSON) -> Decoded { - return curry(Content.File.ContentType.submodule) - <^> j <|? "submodule_git_url" -} + private enum CodingKeys: String, CodingKey { + case name + case path + case sha + case url = "html_url" + case content + } + } -extension Content.File.ContentType: Argo.Decodable { - public static func decode(_ json: JSON) -> Decoded { - guard case let .object(payload) = json else { - return .failure(.typeMismatch(expected: "object", actual: "\(json)")) - } + case file(File) + case directory([File]) - guard let type = payload["type"], case let .string(value) = type else { - return .failure(.custom("Content type is invalid")) - } + public init(from decoder: Decoder) throws { - switch value { - case "file": - if payload["download_url"] == .null { - return decodeSubmodule(json) + do { + let file = try File(from: decoder) + self = .file(file) + } catch { + var container = try decoder.unkeyedContainer() + var files = [File]() + while !container.isAtEnd { + files.append(try container.decode(File.self)) } - return decodeFile(json) - case "dir": - return .success(Content.File.ContentType.directory) - case "submodule": - return decodeSubmodule(json) - case "symlink": - return decodeSymlink(json) - default: - return .failure(.custom("Content type \(value) is invalid")) + + self = .directory(files) } } } @@ -169,20 +200,6 @@ extension Content.File.ContentType: Equatable { } } -extension Content: ResourceType { - public static func decode(_ j: JSON) -> Decoded { - - switch j { - case .array(_): - return Array.decode(j).map(Content.directory) - case .object(_): - return Content.File.decode(j).map(Content.file) - default: - return .failure(.typeMismatch(expected: "Array or Object", actual: "\(j)")) - } - } -} - extension Content.File: Hashable { public static func ==(lhs: Content.File, rhs: Content.File) -> Bool { return lhs.name == rhs.name @@ -196,15 +213,4 @@ extension Content.File: Hashable { } } -extension Content.File: ResourceType { - public static func decode(_ j: JSON) -> Decoded { - let f = curry(Content.File.init) - return f - <^> Content.File.ContentType.decode(j) - <*> j <| "name" - <*> j <| "path" - <*> j <| "sha" - <*> (j <| "html_url" >>- toURL) - } -} diff --git a/Sources/Tentacle/Decodable.swift b/Sources/Tentacle/Decodable.swift deleted file mode 100644 index d3457cd..0000000 --- a/Sources/Tentacle/Decodable.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Decodable.swift -// Tentacle -// -// Created by Matt Diephouse on 3/3/16. -// Copyright © 2016 Matt Diephouse. All rights reserved. -// - -import Argo -import Foundation - -extension URL: Argo.Decodable { - public static func decode(_ json: JSON) -> Decoded { - return String.decode(json).flatMap { URLString in - return .fromOptional(self.init(string: URLString)) - } - } -} diff --git a/Sources/Tentacle/File.swift b/Sources/Tentacle/File.swift index 0afb6e4..d0e975c 100644 --- a/Sources/Tentacle/File.swift +++ b/Sources/Tentacle/File.swift @@ -7,9 +7,6 @@ // import Foundation -import Argo -import Runes -import Curry extension Repository { /// A request to create a file at a given path in the repository. @@ -30,7 +27,7 @@ extension Repository { } } -public struct File { +public struct File: ResourceType, Encodable { /// Commit message public let message: String /// The committer of the commit @@ -49,33 +46,21 @@ public struct File { self.content = content self.branch = branch } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(message, forKey: .message) + try container.encode(committer, forKey: .committer) + try container.encode(author, forKey: .author) + try container.encode(content.base64EncodedString(), forKey: .content) + try container.encode(branch, forKey: .branch) + } } -extension File: Encodable, Hashable { +extension File: Hashable { public var hashValue: Int { return message.hashValue } - - public func encode() -> JSON { - var payload: [String: JSON] = [ - "message": .string(message), - "content": .string(content.base64EncodedString()) - ] - - if let author = author { - payload["author"] = author.encode() - } - - if let committer = committer { - payload["committer"] = committer.encode() - } - - if let branch = branch { - payload["branch"] = .string(branch) - } - - return JSON.object(payload) - } public static func ==(lhs: File, rhs: File) -> Bool { return lhs.message == rhs.message diff --git a/Sources/Tentacle/FileResponse.swift b/Sources/Tentacle/FileResponse.swift index abda962..76584e9 100644 --- a/Sources/Tentacle/FileResponse.swift +++ b/Sources/Tentacle/FileResponse.swift @@ -7,11 +7,8 @@ // import Foundation -import Argo -import Curry -import Runes -public struct FileResponse { +public struct FileResponse: ResourceType { /// Created file public let content: Content @@ -19,13 +16,7 @@ public struct FileResponse { public let commit: Commit } -extension FileResponse: ResourceType { - static public func decode(_ j: JSON) -> Decoded { - return curry(FileResponse.init) - <^> j <| "content" - <*> j <| "commit" - } - +extension FileResponse { public var hashValue: Int { return content.hashValue ^ commit.hashValue } diff --git a/Sources/Tentacle/GitHubError.swift b/Sources/Tentacle/GitHubError.swift index dcaba81..f4f0807 100644 --- a/Sources/Tentacle/GitHubError.swift +++ b/Sources/Tentacle/GitHubError.swift @@ -6,14 +6,10 @@ // Copyright © 2016 Matt Diephouse. All rights reserved. // -import Argo -import Curry -import Runes import Foundation - /// An error from the GitHub API. -public struct GitHubError: CustomStringConvertible, Error { +public struct GitHubError: CustomStringConvertible, Error, Decodable { /// The error message from the API. public let message: String @@ -35,9 +31,3 @@ extension GitHubError: Hashable { return message.hashValue } } - -extension GitHubError: Argo.Decodable { - public static func decode(_ j: JSON) -> Decoded { - return curry(self.init) <^> j <| "message" - } -} diff --git a/Sources/Tentacle/Identifiable.swift b/Sources/Tentacle/Identifiable.swift index 8eb1f49..d76fae2 100644 --- a/Sources/Tentacle/Identifiable.swift +++ b/Sources/Tentacle/Identifiable.swift @@ -12,8 +12,18 @@ public protocol Identifiable { var id: ID { get } } -public struct ID { - let rawValue: Int +public struct ID: Decodable { + var rawValue: Int + + public var string: String { + return "\(rawValue)" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + rawValue = try container.decode(Int.self) + } + } extension ID: ExpressibleByIntegerLiteral { @@ -35,5 +45,5 @@ extension ID: Equatable { static public func == (lhs: ID, rhs: ID) -> Bool { return lhs.rawValue == rhs.rawValue } - + } diff --git a/Sources/Tentacle/Issue.swift b/Sources/Tentacle/Issue.swift index ecf976c..e56744d 100644 --- a/Sources/Tentacle/Issue.swift +++ b/Sources/Tentacle/Issue.swift @@ -7,9 +7,6 @@ // import Foundation -import Argo -import Curry -import Runes extension Repository { /// A request for issues in the repository. @@ -21,8 +18,8 @@ extension Repository { } /// An Issue on Github -public struct Issue: CustomStringConvertible, Identifiable { - public enum State: String { +public struct Issue: CustomStringConvertible, ResourceType, Identifiable { + public enum State: String, ResourceType { case open = "open" case closed = "closed" } @@ -98,6 +95,24 @@ public struct Issue: CustomStringConvertible, Identifiable { self.updatedAt = updatedAt } + private enum CodingKeys: String, CodingKey { + case id + case url = "html_url" + case number + case state + case title + case body + case user + case labels + case assignees + case milestone + case isLocked = "locked" + case commentCount = "comments" + case pullRequest = "pull_request" + case closedAt = "closed_at" + case createdAt = "created_at" + case updatedAt = "updated_at" + } } extension Issue: Hashable { @@ -122,28 +137,3 @@ extension Issue: Hashable { } } -extension Issue: ResourceType { - public static func decode(_ j: JSON) -> Decoded { - let f = curry(Issue.init) - - let ff = f - <^> (j <| "id" >>- toIdentifier) - <*> (j <| "html_url" >>- toURL) - <*> j <| "number" - <*> (j <| "state" >>- toIssueState) - <*> j <| "title" - let fff = ff - <*> j <| "body" - <*> j <| "user" - <*> j <|| "labels" - <*> j <|| "assignees" - <*> j <|? "milestone" - return fff - <*> j <| "locked" - <*> j <| "comments" - <*> j <|? "pull_request" - <*> (j <|? "closed_at" >>- toOptionalDate) - <*> (j <| "created_at" >>- toDate) - <*> (j <| "updated_at" >>- toDate) - } -} diff --git a/Sources/Tentacle/JSONExtensions.swift b/Sources/Tentacle/JSONExtensions.swift new file mode 100644 index 0000000..9c3dae9 --- /dev/null +++ b/Sources/Tentacle/JSONExtensions.swift @@ -0,0 +1,49 @@ +// +// JSONExtensions.swift +// Tentacle +// +// Created by Matt Diephouse on 3/10/16. +// Copyright © 2016 Matt Diephouse. All rights reserved. +// + +import Foundation +import Result + +internal func decode(_ payload: Data) -> Result { + return Result { () -> T in + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) + return try decoder.decode(T.self, from: payload) + } +} + +internal func decodeList(_ payload: Data) -> Result<[T], DecodingError> { + return Result { () -> [T] in + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) + return try decoder.decode([T].self, from: payload) + } +} + +extension DecodingError.Context: Equatable { + public static func ==(lhs: DecodingError.Context, rhs: DecodingError.Context) -> Bool { + return lhs.debugDescription == rhs.debugDescription + } +} + +extension DecodingError: Equatable { + public static func ==(lhs: DecodingError, rhs: DecodingError) -> Bool { + switch (lhs, rhs) { + case (.dataCorrupted(let lContext), .dataCorrupted(let rContext)): + return lContext == rContext + case (.keyNotFound(let lKey, let lContext), .keyNotFound(let rKey, let rContext)): + return lKey.stringValue == rKey.stringValue && lContext == rContext + case (.typeMismatch(let lType, let lContext), .typeMismatch(let rType, let rContext)): + return lType == rType && lContext == rContext + case (.valueNotFound(let lType, let lContext), .valueNotFound(let rType, let rContext)): + return lType == rType && lContext == rContext + default: + return false + } + } +} diff --git a/Sources/Tentacle/Label.swift b/Sources/Tentacle/Label.swift index fef347b..73317e0 100644 --- a/Sources/Tentacle/Label.swift +++ b/Sources/Tentacle/Label.swift @@ -7,17 +7,30 @@ // import Foundation -import Argo -import Curry -import Runes -public struct Label: CustomStringConvertible { +public struct Label: CustomStringConvertible, ResourceType { public let name: String public let color: Color public var description: String { return name } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.color = Color(hex: try container.decode(String.self, forKey: .color)) + } + + public init(name: String, color: Color) { + self.name = name + self.color = color + } + + private enum CodingKeys: String, CodingKey { + case name + case color + } } extension Label: Hashable { @@ -30,11 +43,3 @@ extension Label: Hashable { } } -extension Label: ResourceType { - public static func decode(_ json: JSON) -> Decoded