Skip to content
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

Merged
merged 64 commits into from
Oct 17, 2017
Merged

Migrate to Swift 4's Decodable APIs #84

merged 64 commits into from
Oct 17, 2017

Conversation

Palleas
Copy link
Collaborator

@Palleas Palleas commented Jun 15, 2017

Let me know if I forgot something in the list of stuff to do!

@Palleas
Copy link
Collaborator Author

Palleas commented Jun 17, 2017

screen shot 2017-06-17 at 5 29 28 pm

Getting there!

@Palleas
Copy link
Collaborator Author

Palleas commented Jun 18, 2017

I'm running the unit tests now and I have an issue. Basically since we treat id in all the resources as String while the JSON returns Int, the decoding fails. The only solution I've found so far is doing is for Release:

    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 id as Int.  ¯\(ツ)

@mdiep
Copy link
Owner

mdiep commented Jun 18, 2017

I think that's likely the only solution. I don't think the built-in JSON encoding is as flexible.

@mdiep
Copy link
Owner

mdiep commented Jun 18, 2017

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?

@Palleas
Copy link
Collaborator Author

Palleas commented Jun 18, 2017

@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?

@mdiep
Copy link
Owner

mdiep commented Jun 18, 2017

I think it makes sense to have a per-type ID because it adds additional type safety. You can't accidentally pass an ID for the wrong type of resource to an API.

But if it made sense, we could add a top-level protocol that they all conform to.

Copy link
Owner

@mdiep mdiep left a 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! 👏

.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!)") }
Copy link
Owner

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.

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
Copy link
Owner

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 returning directly?


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!))
Copy link
Owner

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.

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
Copy link
Owner

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.

.mapError(Error.jsonDecodingError)
}
) -> SignalProducer<(Response, Resource), Error> {
let s: SignalProducer<(Response, Data), Error> = execute(request, page: nil, perPage: nil)
Copy link
Owner

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.


import Foundation

public struct Homepage: Decodable {
Copy link
Owner

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
}
Copy link
Owner

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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

// ])
//
// let encoded = file.encode()
// XCTAssertEqual(expected, encoded)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

// ])
//
// let encoded = file.encode()
// XCTAssertEqual(expected, encoded)
Copy link
Owner

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"]
Copy link
Collaborator

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

@Palleas
Copy link
Collaborator Author

Palleas commented Jul 4, 2017

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?

@mdiep
Copy link
Owner

mdiep commented Jul 4, 2017

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.

@Palleas
Copy link
Collaborator Author

Palleas commented Jul 10, 2017

Xcode 9b3 reverts SE-110 so I'll tackle all the feedback this week!

import Foundation
import ReactiveSwift
import Result

//extension Array where Element == ResourceType : ResourceType {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😞

.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!)") }
Copy link
Owner

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

return current.concat(self.execute(request, page: nextPage, perPage: perPage))
})
Copy link
Owner

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. 🙏

Copy link
Collaborator Author

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!

@@ -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
Copy link
Owner

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
}
Copy link
Owner

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?

Copy link
Collaborator Author

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..?

@mdiep
Copy link
Owner

mdiep commented Aug 25, 2017

I'm liking these changes!

If it'd be helpful, we could add ID/Identifiable in a separate PR before this one.


import Foundation

public enum Homepage: Decodable {
Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Collaborator

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?.

Copy link
Collaborator

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?

Copy link
Owner

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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 🤔


if let base = base {
try container.encode(base, forKey: .base)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL!

Copy link
Owner

@mdiep mdiep left a 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. 🎉

.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)
Copy link
Owner

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

/// SHA of the commit
public let sha: SHA
public let sha: Hash
Copy link
Owner

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?

Copy link
Collaborator Author

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

Copy link
Owner

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.

/// 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
Copy link
Owner

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?

debugDescription: "Invalid content-type \(type)"
)
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈

Copy link
Collaborator Author

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? 🤔

Copy link
Collaborator Author

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIXME

Copy link
Collaborator Author

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? 🤔

Copy link
Collaborator Author

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!

}

// TODO: remove this, or replace `Color` with something (De)codable
private enum CodingKeys: String, CodingKey {
Copy link
Owner

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. 👍

self.color = color
}

// TODO: remove this, or replace `Color` with something (De)codable
Copy link
Owner

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)
Copy link
Owner

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?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it should 😞

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

case isTruncated = "truncated"
}

public struct SHA: Codable {
Copy link
Owner

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.

@Palleas
Copy link
Collaborator Author

Palleas commented Oct 10, 2017

I remember why there are 2 SHAs:

On Branch, we have:

  • Branch
    • Commit
      • Sha (String)

But on Tree, we have:

  • Tree
    • Sha (String)

I'll change the way it works for Branch, it feels like a hack.

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)
Copy link
Collaborator

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.

Copy link
Owner

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. 🤷‍♂️

Copy link
Collaborator Author

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?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, just for homepage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@Palleas
Copy link
Collaborator Author

Palleas commented Oct 11, 2017

Also fixed the remaining failing test (in Tree encoding)

Copy link
Owner

@mdiep mdiep left a 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
Copy link
Owner

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. 🙈

Copy link
Collaborator Author

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  🤷‍♂️

Copy link
Owner

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! 😅

.mapError(Error.jsonDecodingError)
.flatMap { error in
.failure(Error.apiError(response.statusCode, Response(headerFields: headers), error))
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This } is under indented.

/// SHA of the commit
public let sha: SHA
public let sha: Hash
Copy link
Owner

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
Copy link
Owner

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! 😅

@mdiep
Copy link
Owner

mdiep commented Oct 17, 2017

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. 😅

@mdiep mdiep merged commit 90c5757 into master Oct 17, 2017
@mdiep mdiep deleted the swift-4 branch October 17, 2017 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants