Skip to content

Commit

Permalink
Merge pull request #87 from mdiep/add-identifiable-type
Browse files Browse the repository at this point in the history
Implement `Identifiable` type in model with ids
  • Loading branch information
mdiep authored Aug 27, 2017
2 parents fe46f82 + dc0d08b commit 0558692
Show file tree
Hide file tree
Showing 15 changed files with 108 additions and 61 deletions.
14 changes: 8 additions & 6 deletions Sources/Tentacle/ArgoExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,17 @@ extension JSON {
}
}

internal func decode<T: Argo.Decodable>(_ object: Any) -> Result<T, DecodeError> where T == T.DecodedType {
let decoded: Decoded<T> = decode(object)
switch decoded {
internal func decode<T: Argo.Decodable>(_ 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 decode<T: Argo.Decodable>(_ object: Any) -> Result<[T], DecodeError> where T == T.DecodedType {
let decoded: Decoded<[T]> = decode(object)
switch decoded {
internal func decode<T: Argo.Decodable>(_ json: JSON) -> Result<[T], DecodeError> where T == T.DecodedType {
switch [T].decode(json) {
case let .success(object):
return .success(object)
case let .failure(error):
Expand All @@ -61,6 +59,10 @@ internal func toString(_ number: Int) -> Decoded<String> {
return .success(number.description)
}

internal func toIdentifier<T: Identifiable>(_ number: Int) -> Decoded<ID<T>> {
return .success(ID<T>(rawValue: number))
}

internal func toInt(_ string: String) -> Decoded<Int> {
if let int = Int(string) {
return .success(int)
Expand Down
28 changes: 14 additions & 14 deletions Sources/Tentacle/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import ReactiveSwift
import Result

extension JSONSerialization {
internal static func deserializeJSON(_ data: Data) -> Result<Any, AnyError> {
return materialize(try JSONSerialization.jsonObject(with: data))
internal static func deserializeJSON(_ data: Data) -> Result<JSON, AnyError> {
return materialize(JSON(try JSONSerialization.jsonObject(with: data)))
}
}

Expand Down Expand Up @@ -188,38 +188,38 @@ public final class Client {
}

/// Fetch a request from the API.
private func execute<Value>(_ request: Request<Value>, page: UInt?, perPage: UInt?) -> SignalProducer<(Response, Any), Error> {
private func execute<Value>(_ request: Request<Value>, page: UInt?, perPage: UInt?) -> SignalProducer<(Response, JSON), Error> {
return urlSession
.reactive
.data(with: urlRequest(for: request, page: page, perPage: perPage))
.mapError { Error.networkError($0.error) }
.flatMap(.concat) { data, response -> SignalProducer<(Response, Any), Error> in
.flatMap(.concat) { data, response -> SignalProducer<(Response, JSON), 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<Value, Error>)`
// over `init(_ action: @escaping () -> Value)`.
let producer: SignalProducer<Any, Error> = SignalProducer { () -> Result<Any, Error> in
let producer: SignalProducer<JSON, Error> = SignalProducer { () -> Result<JSON, Error> in
return JSONSerialization.deserializeJSON(data).mapError { Error.jsonDeserializationError($0.error) }
}
return producer
.attemptMap { JSON in
.attemptMap { json in
if response.statusCode == 404 {
return .failure(.doesNotExist)
}
if response.statusCode >= 400 && response.statusCode < 600 {
return decode(JSON)
return decode(json)
.mapError(Error.jsonDecodingError)
.flatMap { error in
.failure(Error.apiError(response.statusCode, Response(headerFields: headers), error))
}
}
return .success(JSON)
return .success(json)
}
.map { json in
return (Response(headerFields: headers), json)
}
.map { JSON in
return (Response(headerFields: headers), JSON)
}
}
}

Expand All @@ -228,8 +228,8 @@ public final class Client {
_ request: Request<Resource>
) -> SignalProducer<(Response, Resource), Error> where Resource.DecodedType == Resource {
return execute(request, page: nil, perPage: nil)
.attemptMap { response, JSON in
return decode(JSON)
.attemptMap { response, json in
return decode(json)
.map { resource in
(response, resource)
}
Expand All @@ -248,7 +248,7 @@ public final class Client {
) -> SignalProducer<(Response, [Resource]), Error> where Resource.DecodedType == Resource {
let nextPage = (page ?? 1) + 1
return execute(request, page: page, perPage: perPage)
.attemptMap { (response: Response, json: Any) in
.attemptMap { (response: Response, json: JSON) in
return decode(json)
.map { resource in
(response, resource)
Expand Down
6 changes: 3 additions & 3 deletions Sources/Tentacle/Comment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ extension Repository {
}
}

public struct Comment: CustomStringConvertible {
public struct Comment: CustomStringConvertible, Identifiable {

/// The id of the issue
public let id: String
public let id: ID<Comment>
/// The URL to view this comment in a browser
public let url: URL
/// The date this comment was created at
Expand Down Expand Up @@ -57,7 +57,7 @@ extension Comment: ResourceType {
let f = curry(Comment.init)

return f
<^> (j <| "id" >>- toString)
<^> (j <| "id" >>- toIdentifier)
<*> (j <| "html_url" >>- toURL)
<*> (j <| "created_at" >>- toDate)
<*> (j <| "updated_at" >>- toDate)
Expand Down
39 changes: 39 additions & 0 deletions Sources/Tentacle/Identifiable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Identifiable.swift
// Tentacle-OSX
//
// Created by Romain Pouclet on 2017-06-18.
// Copyright © 2017 Matt Diephouse. All rights reserved.
//

import Foundation

public protocol Identifiable {
var id: ID<Self> { get }
}

public struct ID<Of: Identifiable> {
let rawValue: Int
}

extension ID: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self.rawValue = value
}
}

extension ID: Hashable {

public var hashValue: Int {
return rawValue.hashValue
}

}

extension ID: Equatable {

static public func == (lhs: ID, rhs: ID) -> Bool {
return lhs.rawValue == rhs.rawValue
}

}
8 changes: 4 additions & 4 deletions Sources/Tentacle/Issue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ extension Repository {
}

/// An Issue on Github
public struct Issue: CustomStringConvertible {
public struct Issue: CustomStringConvertible, Identifiable {
public enum State: String {
case open = "open"
case closed = "closed"
}

/// The id of the issue
public let id: String
public let id: ID<Issue>

/// The URL to view this issue in a browser
public let url: URL?
Expand Down Expand Up @@ -79,7 +79,7 @@ public struct Issue: CustomStringConvertible {
return title
}

public init(id: String, url: URL?, number: Int, state: State, title: String, body: String, user: UserInfo, labels: [Label], assignees: [UserInfo], milestone: Milestone?, isLocked: Bool, commentCount: Int, pullRequest: PullRequest?, closedAt: Date?, createdAt: Date, updatedAt: Date) {
public init(id: ID<Issue>, url: URL?, number: Int, state: State, title: String, body: String, user: UserInfo, labels: [Label], assignees: [UserInfo], milestone: Milestone?, isLocked: Bool, commentCount: Int, pullRequest: PullRequest?, closedAt: Date?, createdAt: Date, updatedAt: Date) {
self.id = id
self.url = url
self.number = number
Expand Down Expand Up @@ -127,7 +127,7 @@ extension Issue: ResourceType {
let f = curry(Issue.init)

let ff = f
<^> (j <| "id" >>- toString)
<^> (j <| "id" >>- toIdentifier)
<*> (j <| "html_url" >>- toURL)
<*> j <| "number"
<*> (j <| "state" >>- toIssueState)
Expand Down
6 changes: 3 additions & 3 deletions Sources/Tentacle/Milestone.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import Argo
import Curry
import Runes

public struct Milestone: CustomStringConvertible {
public struct Milestone: CustomStringConvertible, Identifiable {
public enum State: String {
case open = "open"
case closed = "closed"
}

/// The ID of the milestone
public let id: String
public let id: ID<Milestone>

/// The number of the milestone in the repository it belongs to
public let number: Int
Expand Down Expand Up @@ -84,7 +84,7 @@ extension Milestone: ResourceType {
let f = curry(self.init)

let ff = f
<^> (j <| "id" >>- toString)
<^> (j <| "id" >>- toIdentifier)
<*> j <| "number"
<*> (j <| "state" >>- toMilestoneState)
<*> j <| "title"
Expand Down
16 changes: 8 additions & 8 deletions Sources/Tentacle/Release.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ extension Repository {
}

/// A Release of a Repository.
public struct Release: CustomStringConvertible {
public struct Release: CustomStringConvertible, Identifiable {
/// An Asset attached to a Release.
public struct Asset: CustomStringConvertible {
public struct Asset: CustomStringConvertible, Identifiable {
/// The unique ID for this release asset.
public let id: String
public let id: ID<Asset>

/// The filename of this asset.
public let name: String
Expand All @@ -53,7 +53,7 @@ public struct Release: CustomStringConvertible {
return "\(url)"
}

public init(id: String, name: String, contentType: String, url: URL, apiURL: URL) {
public init(id: ID<Asset>, name: String, contentType: String, url: URL, apiURL: URL) {
self.id = id
self.name = name
self.contentType = contentType
Expand All @@ -63,7 +63,7 @@ public struct Release: CustomStringConvertible {
}

/// The unique ID of the release.
public let id: String
public let id: ID<Release>

/// Whether this release is a draft (only visible to the authenticted user).
public let isDraft: Bool
Expand All @@ -87,7 +87,7 @@ public struct Release: CustomStringConvertible {
return "\(url)"
}

public init(id: String, tag: String, url: URL, name: String? = nil, isDraft: Bool = false, isPrerelease: Bool = false, assets: [Asset]) {
public init(id: ID<Release>, tag: String, url: URL, name: String? = nil, isDraft: Bool = false, isPrerelease: Bool = false, assets: [Asset]) {
self.id = id
self.tag = tag
self.url = url
Expand Down Expand Up @@ -127,7 +127,7 @@ extension Release: Hashable {
extension Release.Asset: ResourceType {
public static func decode(_ j: JSON) -> Decoded<Release.Asset> {
return curry(self.init)
<^> (j <| "id" >>- toString)
<^> (j <| "id" >>- toIdentifier)
<*> j <| "name"
<*> j <| "content_type"
<*> j <| "browser_download_url"
Expand All @@ -139,7 +139,7 @@ extension Release: ResourceType {
public static func decode(_ j: JSON) -> Decoded<Release> {
let f = curry(Release.init)
return f
<^> (j <| "id" >>- toString)
<^> (j <| "id" >>- toIdentifier)
<*> j <| "tag_name"
<*> j <| "html_url"
<*> j <|? "name"
Expand Down
6 changes: 3 additions & 3 deletions Sources/Tentacle/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ extension User: Hashable {
}

/// Information about a user on GitHub.
public struct UserInfo: CustomStringConvertible {
public struct UserInfo: CustomStringConvertible, Identifiable {
public enum UserType: String {
case user = "User"
case organization = "Organization"
}

/// The unique ID of the user.
public let id: String
public let id: ID<UserInfo>

/// The user this information is about.
public let user: User
Expand Down Expand Up @@ -124,7 +124,7 @@ extension UserInfo: Hashable {
extension UserInfo: ResourceType {
public static func decode(_ j: JSON) -> Decoded<UserInfo> {
return curry(self.init)
<^> (j <| "id" >>- toString)
<^> (j <| "id" >>- toIdentifier)
<*> (j <| "login").map(User.init)
<*> j <| "html_url"
<*> j <| "avatar_url"
Expand Down
6 changes: 6 additions & 0 deletions Tentacle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@
7A8A9D6B1E5548070009DA9E /* repos-Carthage-ReactiveTask-branches.response in Resources */ = {isa = PBXBuildFile; fileRef = 7A8A9D681E5548070009DA9E /* repos-Carthage-ReactiveTask-branches.response */; };
7A8A9D6C1E5548070009DA9E /* repos-Carthage-ReactiveTask-branches.response in Resources */ = {isa = PBXBuildFile; fileRef = 7A8A9D681E5548070009DA9E /* repos-Carthage-ReactiveTask-branches.response */; };
7AAB00FD1CF51FC5005A7319 /* IssuesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAB00FC1CF51FC5005A7319 /* IssuesTests.swift */; };
7AB114501F53463A002795A1 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB1144F1F53463A002795A1 /* Identifiable.swift */; };
7AB114511F53463A002795A1 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB1144F1F53463A002795A1 /* Identifiable.swift */; };
7ABFB4D71D51519B0067B500 /* RepositoryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABFB4D61D51519B0067B500 /* RepositoryInfo.swift */; };
7ABFB4D81D51519B0067B500 /* RepositoryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABFB4D61D51519B0067B500 /* RepositoryInfo.swift */; };
7ABFB4DD1D5159F50067B500 /* RepositoryInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABFB4DC1D5159F50067B500 /* RepositoryInfoTests.swift */; };
Expand Down Expand Up @@ -285,6 +287,7 @@
7A8A9D671E5548070009DA9E /* repos-Carthage-ReactiveTask-branches.data */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "repos-Carthage-ReactiveTask-branches.data"; sourceTree = "<group>"; };
7A8A9D681E5548070009DA9E /* repos-Carthage-ReactiveTask-branches.response */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "repos-Carthage-ReactiveTask-branches.response"; sourceTree = "<group>"; };
7AAB00FC1CF51FC5005A7319 /* IssuesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssuesTests.swift; sourceTree = "<group>"; };
7AB1144F1F53463A002795A1 /* Identifiable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = "<group>"; };
7ABFB4D61D51519B0067B500 /* RepositoryInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryInfo.swift; sourceTree = "<group>"; };
7ABFB4DC1D5159F50067B500 /* RepositoryInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryInfoTests.swift; sourceTree = "<group>"; };
927847F61E0B28CF003B9EE6 /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -487,6 +490,7 @@
927847FF1E0C78DB003B9EE6 /* FileResponse.swift */,
BECB8A8D1CBDD919005D70A6 /* FoundationExtensions.swift */,
BEB0765C1C8A001C00ABD373 /* GitHubError.swift */,
7AB1144F1F53463A002795A1 /* Identifiable.swift */,
BE88E7F71C88C6B30034A112 /* Info.plist */,
7A1A82551CF3DBAC0076E2DD /* Issue.swift */,
7A1A82571CF3DE4C0076E2DD /* Label.swift */,
Expand Down Expand Up @@ -874,6 +878,7 @@
7A27887E1D49223E007AC936 /* Comment.swift in Sources */,
7A1F20EE1D3E862200F275F8 /* Color.swift in Sources */,
7ABFB4D81D51519B0067B500 /* RepositoryInfo.swift in Sources */,
7AB114511F53463A002795A1 /* Identifiable.swift in Sources */,
BF01A25E1EAA5BEC0028ECFC /* Tree.swift in Sources */,
BEEE474F1C92623E000FFC21 /* Response.swift in Sources */,
927848011E0C78EA003B9EE6 /* FileResponse.swift in Sources */,
Expand Down Expand Up @@ -938,6 +943,7 @@
7A27887A1D4920DA007AC936 /* Comment.swift in Sources */,
7A1F20ED1D3E85BB00F275F8 /* Color.swift in Sources */,
7ABFB4D71D51519B0067B500 /* RepositoryInfo.swift in Sources */,
7AB114501F53463A002795A1 /* Identifiable.swift in Sources */,
BF01A25D1EAA5BEC0028ECFC /* Tree.swift in Sources */,
BEEE474E1C92623E000FFC21 /* Response.swift in Sources */,
927848001E0C78DB003B9EE6 /* FileResponse.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions Tests/TentacleTests/CommentsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ class CommentsTests: XCTestCase {

func testDecodedCommentsOnSampleRepositoryIssue() {
let palleasOpensource = UserInfo(
id: "15802020",
id: 15802020,
user: User("Palleas-opensource"),
url: URL(string: "https://github.com/Palleas-opensource")!,
avatarURL: URL(string: "https://avatars.githubusercontent.com/u/15802020?v=3")!,
type: .user
)

let palleas = UserInfo(
id: "48797",
id: 48797,
user: User("Palleas"),
url: URL(string: "https://github.com/Palleas")!,
avatarURL: URL(string: "https://avatars.githubusercontent.com/u/15802020?v=3")!,
Expand All @@ -31,15 +31,15 @@ class CommentsTests: XCTestCase {

let expected: [Comment] = [
Comment(
id: "235455442",
id: 235455442,
url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/issues/1#issuecomment-235455442")!,
createdAt: DateFormatter.iso8601.date(from: "2016-07-27T01:28:21Z")!,
updatedAt: DateFormatter.iso8601.date(from: "2016-07-27T01:28:21Z")!,
body: "I know right?!\n",
author: palleas
),
Comment(
id: "235455603",
id: 235455603,
url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/issues/1#issuecomment-235455603")!,
createdAt: DateFormatter.iso8601.date(from: "2016-07-27T01:29:31Z")!,
updatedAt: DateFormatter.iso8601.date(from: "2016-07-27T01:29:31Z")!,
Expand Down
Loading

0 comments on commit 0558692

Please sign in to comment.