-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate to Swift 4's Decodable APIs #84
Conversation
I'm running the unit tests now and I have an issue. Basically since we treat public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
self.id = "\(id)"
self.tag = try container.decode(String.self, forKey: .tag)
self.url = try container.decode(URL.self, forKey: .url)
self.name = try container.decode(String.self, forKey: .tag)
self.isDraft = try container.decode(Bool.self, forKey: .isDraft)
self.isPrerelease = try container.decode(Bool.self, forKey: .isPrerelease)
self.assets = try container.decode([Asset].self, forKey: .assets)
} Do you see a different approach we could take with this? Right now the only solution I see is going back to treating |
I think that's likely the only solution. I don't think the built-in JSON encoding is as flexible. |
On second thought: I think we should add a property type. extension Release {
public struct ID {
private let id: Int
public var string: String {
return "\(id)"
}
}
} That might make decoding easier? |
@mdiep Ha! That's exactly what we were talking about in Slack, Cf: https://gist.github.com/Palleas/645b6c30306603feaab148c9e167a307 Although since it will happen for all the Resources, maybe we need to move it to a higher level? |
I think it makes sense to have a per-type But if it made sense, we could add a top-level protocol that they all conform to. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really neat to see this with the built-in Swift Decodable
stuff! 👏
Sources/Tentacle/Client.swift
Outdated
.filter { _, value in value != nil } | ||
.map { name, value in URLQueryItem(name: name, value: "\(value!)") } | ||
.filter { arg -> Bool in let (_, value) = arg; return value != nil } | ||
.map { arg -> URLQueryItem in let (name, value) = arg; return URLQueryItem(name: name, value: "\(value!)") } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we move to a newer Swift build, we should be able to revert this change since SE-0110 was reverted.
Sources/Tentacle/Client.swift
Outdated
private func execute<Value>(_ request: Request<Value>, page: UInt?, perPage: UInt?) -> SignalProducer<(Response, Any), Error> { | ||
return urlSession | ||
private func execute<Value: Decodable>(_ request: Request<Value>, page: UInt?, perPage: UInt?) -> SignalProducer<(Response, Data), Error> { | ||
let s = urlSession |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be able to remove this local variable and go back to return
ing directly?
Sources/Tentacle/Client.swift
Outdated
|
||
if httpResponse.statusCode >= 400 && httpResponse.statusCode < 600 { | ||
let error: Result<GitHubError, Error> = decode(data).mapError { Error.jsonDecodingError($0) } | ||
return .failure(.apiError(httpResponse.statusCode, githubResponse, error.value!)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This !
isn't really safe. We should go back to the previous structure using flatMap
.
Sources/Tentacle/Client.swift
Outdated
let s: SignalProducer<(Response, Data), Error> = execute(request, page: nil, perPage: nil) | ||
|
||
return s.attemptMap({ (arg) -> Result<(Response, Resource), Client.Error> in | ||
let (response, data) = arg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here WRT SE-0110.
Sources/Tentacle/Client.swift
Outdated
.mapError(Error.jsonDecodingError) | ||
} | ||
) -> SignalProducer<(Response, Resource), Error> { | ||
let s: SignalProducer<(Response, Data), Error> = execute(request, page: nil, perPage: nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be able to remove s
and just return
.
Sources/Tentacle/Homepage.swift
Outdated
|
||
import Foundation | ||
|
||
public struct Homepage: Decodable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe make this an enum instead?
enum Homepage {
case url(URL)
case string(String)
}
private enum CodingKeys: String, CodingKey { | ||
case name | ||
case color | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need these since they match what would be generated?
// return | ||
// } | ||
// | ||
// XCTAssertEqual(expected, decoded) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
Tests/TentacleTests/FileTests.swift
Outdated
// ]) | ||
// | ||
// let encoded = file.encode() | ||
// XCTAssertEqual(expected, encoded) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
Tests/TentacleTests/FileTests.swift
Outdated
// ]) | ||
// | ||
// let encoded = file.encode() | ||
// XCTAssertEqual(expected, encoded) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
.gitmodules
Outdated
[submodule "Carthage/Checkouts/Runes"] | ||
path = Carthage/Checkouts/Runes | ||
url = https://github.com/thoughtbot/Runes.git | ||
[submodule "Carthage/Checkouts/result"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Result is now duplicated in this file, and (nit) it should be capitalized Result
Thank you all for the feedback, I'll take care of most of them this week. The rollback of SE-0110 should be in the next Xcode seed right? |
I think so, yes. Otherwise you can probably download a snapshot from swift.org. |
Xcode 9b3 reverts SE-110 so I'll tackle all the feedback this week! |
Sources/Tentacle/Client.swift
Outdated
import Foundation | ||
import ReactiveSwift | ||
import Result | ||
|
||
//extension Array where Element == ResourceType : ResourceType {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😞
Sources/Tentacle/Client.swift
Outdated
.filter { _, value in value != nil } | ||
.map { name, value in URLQueryItem(name: name, value: "\(value!)") } | ||
.filter { (_, value) -> Bool in value != nil } | ||
.map { (name, value) -> URLQueryItem in URLQueryItem(name: name, value: "\(value!)") } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to remove these return types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
Sources/Tentacle/Client.swift
Outdated
} | ||
|
||
return current.concat(self.execute(request, page: nextPage, perPage: perPage)) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing closure syntax here, please. 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how I missed it, fixed!
Sources/Tentacle/Client.swift
Outdated
@@ -272,7 +266,7 @@ extension Client.Error: Hashable { | |||
return (error1 as NSError) == (error2 as NSError) | |||
|
|||
case let (.jsonDecodingError(error1), .jsonDecodingError(error2)): | |||
return error1 == error2 | |||
return error1 == error2 // FIXME |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this labeled FIXME
?
case mode | ||
case url | ||
case size | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be needed since all the cases have the default value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If think they are because url and size are not exposed as properties and we are using them for encoding, but maybe I'm missing something..?
I'm liking these changes! If it'd be helpful, we could add |
Sources/Tentacle/Homepage.swift
Outdated
|
||
import Foundation | ||
|
||
public enum Homepage: Decodable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this type? Keep using URL
seems better to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless the API is expected to respond with an arbitrary String and not a valid URL, I agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand now that the problem here is empty string handling (""
). Empty string is not valid input even if a property is URL?
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case it seems like we should revert to URL?
and change the decoding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub will let you set this field to any string.
BUT your github.com profile will only show the string if it's actually a URL. So I think we could revert to a URL
and just set it to nil
if it's not a valid URL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed aa43141 to address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, missed this, removed my comment 🙊
Are we sure it’s the same with a repository? I remember having this conversation a while ago 🤔
Sources/Tentacle/Tree.swift
Outdated
|
||
if let base = base { | ||
try container.encode(base, forKey: .base) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall! Thanks for tackling this! It's no small effort. 🎉
Sources/Tentacle/Client.swift
Outdated
.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.concat(.empty) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.concat(.empty)
is effectively a noop. This can just be return current
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
Sources/Tentacle/Commit.swift
Outdated
/// SHA of the commit | ||
public let sha: SHA | ||
public let sha: Hash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did this change from a SHA
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it’s not exactly the same type IIRC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on this? I think it should be the same thing.
Sources/Tentacle/Commit.swift
Outdated
/// URL to see the parent commit in a browser | ||
public let url: URL | ||
|
||
/// SHA of the parent commit | ||
public let sha: SHA | ||
public let sha: Hash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did this change from a SHA
?
Sources/Tentacle/Content.swift
Outdated
debugDescription: "Invalid content-type \(type)" | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How else would you handle this? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a proposal in 261c768, let me know what you think. At least we're no longer switching over strings.
extension DecodingError: Equatable { | ||
static public func ==(lhs: DecodingError, rhs: DecodingError) -> Bool { | ||
switch (lhs, rhs) { | ||
default: return false // FIXME |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIXME
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any idea how to do proper Equatable implementation for DecodingError
? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a proposition suggested to me in Slack in f9d214e, let me know what you think!
Sources/Tentacle/Label.swift
Outdated
} | ||
|
||
// TODO: remove this, or replace `Color` with something (De)codable | ||
private enum CodingKeys: String, CodingKey { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with being explicit. 👍
Sources/Tentacle/Label.swift
Outdated
self.color = color | ||
} | ||
|
||
// TODO: remove this, or replace `Color` with something (De)codable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment can go away?
self.owner = try container.decode(UserInfo.self, forKey: .owner) | ||
self.name = try container.decode(String.self, forKey: .name) | ||
self.nameWithOwner = try container.decode(String.self, forKey: .nameWithOwner) | ||
self.body = try? container.decode(String.self, forKey: .body) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be try container.decodeIfPresent
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it should 😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
Sources/Tentacle/Tree.swift
Outdated
case isTruncated = "truncated" | ||
} | ||
|
||
public struct SHA: Codable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't need this one either. A single SHA
type should be enough.
I remember why there are 2 SHAs: On
But on
I'll change the way it works for |
self.nameWithOwner = try container.decode(String.self, forKey: .nameWithOwner) | ||
self.body = try container.decodeIfPresent(String.self, forKey: .body) | ||
self.url = try container.decode(URL.self, forKey: .url) | ||
self.homepage = try container.decodeIfPresent(URL.self, forKey: .homepage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work for empty string. That should be why try?
was used here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't totally match the intent, since a string that's not a URL should be nil
, but a number should throw. But it's probably close enough. 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Soooo, back to try?
is what you meant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, just for homepage
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
Also fixed the remaining failing test (in Tree encoding) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The question of the SHA
types is really the only remaining thing.
/// Sha of the commit the branch points to | ||
public let sha: SHA | ||
/// The commit the branch points to | ||
public let commit: Commit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still confused about why this isn't just a SHA
. 🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what the payload returns:
[
{
"name": "master",
"commit": {
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc"
},
"protected": true,
"protection_url": "https://api.github.com/repos/octocat/Hello-World/branches/master/protection"
}
]
I can retrieve the sha
directly with a custom init
, is that what you have in mind? Maybe I'm just missing something 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, okay! I didn't realize there was another property returned on commit
! 😅
Sources/Tentacle/Client.swift
Outdated
.mapError(Error.jsonDecodingError) | ||
.flatMap { error in | ||
.failure(Error.apiError(response.statusCode, Response(headerFields: headers), error)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This }
is under indented.
Sources/Tentacle/Commit.swift
Outdated
/// SHA of the commit | ||
public let sha: SHA | ||
public let sha: Hash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on this? I think it should be the same thing.
/// Sha of the commit the branch points to | ||
public let sha: SHA | ||
/// The commit the branch points to | ||
public let commit: Commit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, okay! I didn't realize there was another property returned on commit
! 😅
This is great! Thank you SO SO SO much for doing this. ✨ It's great to see what Swift's built-in decoding looks like, warts and all. Sorry about the long review times and my general cluelessness in this PR. Things have been a little crazy here. 😅 |
Result
submoduleLet me know if I forgot something in the list of stuff to do!