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

Flatten enums & parameters into extended Task type #1147

Merged
merged 31 commits into from Aug 9, 2017
Merged

Flatten enums & parameters into extended Task type #1147

merged 31 commits into from Aug 9, 2017

Conversation

Jeehut
Copy link
Contributor

@Jeehut Jeehut commented Jun 28, 2017

This solution was dicussed starting here:
#1135 (comment)

Specifically this PR does the following:

  • Remove parameters and parameterEncoding from everywhere
  • Extend Task with different types of request data (e.g. data & compositeEncoded)
  • Flatten the DownloadType and UploadType enums into the Task type
  • Use those new task cases to set url params & http body appropriately
  • Update Tests
  • Update Docs
  • Write a migration guide
  • Add Changelog entry
  • Handle the TODO: comments (exception handling) appropriately

This solves the most apparent part of #1135 and it probably also solves #314.

@MoyaBot
Copy link

MoyaBot commented Jun 28, 2017

1 Warning
⚠️ Big PR, try to keep changes smaller if you can

SwiftLint found issues

Warnings

File Line Reason
GiphyAPI.swift 11 Force unwrapping should be avoided.
GiphyAPI.swift 34 Force unwrapping should be avoided.
GiphyAPI.swift 44 Force unwrapping should be avoided.
GitHubAPI.swift 22 Force unwrapping should be avoided.
GitHubAPI.swift 33 Force unwrapping should be avoided.
GitHubAPI.swift 72 Force unwrapping should be avoided.
GitHubAPI.swift 74 Force unwrapping should be avoided.
GitHubAPI.swift 76 Force unwrapping should be avoided.
GitHubAPI.swift 88 MARK comment should be in valid format.
GitHubAPI.swift 99 Files should have a single trailing newline.
GitHubUserContentAPI.swift 11 Force unwrapping should be avoided.
GitHubUserContentAPI.swift 45 Force unwrapping should be avoided.
GitHubUserContentAPI.swift 43 Lines should not have trailing whitespace.
GitHubUserContentAPI.swift 47 Lines should not have trailing whitespace.
GitHubUserContentAPI.swift 50 Files should have a single trailing newline.
MoyaProvider+Internal.swift 94 Function should have 5 parameters or less: it currently has 7
MoyaProviderSpec.swift 833 File should contain 400 lines or less: currently contains 833

Errors

File Line Reason
GiphyAPI.swift 4 Variable name should start with a lowercase character: 'GiphyProvider'
GitHubAPI.swift 16 Variable name should start with a lowercase character: 'GitHubProvider'
GitHubUserContentAPI.swift 4 Variable name should start with a lowercase character: 'GitHubUserContentProvider'
GitHubUserContentAPI.swift 41 Variable name should start with a lowercase character: 'DefaultDownloadDestination'
MoyaProviderSpec.swift 8 Type body should span 350 lines or less excluding comments and whitespace: currently spans 493 lines

Generated by 🚫 Danger

@Jeehut
Copy link
Contributor Author

Jeehut commented Jun 28, 2017

I have just integrated my fork into the project where I'm using the Encodable type.

Here's how my target now looks:

import Foundation
import Moya

enum MyTarget {
    case createUser(CreateUserRequest)
    case signIn(SignInRequest)
}

// MARK: - TargetType Protocol Implementation
extension MyTarget: TargetType {
    var baseURL: URL {
        return URL(string: "https://example.com/api/v1/")!
    }

    var path: String {
        switch self {
        case .createUser:
            return "user"

        case .signIn:
            return "session"
        }
    }

    var method: Moya.Method {
        return .post
    }

    var sampleData: Data {
        return Data()
    }

    var task: Task {
        switch self {
        case .createUser(let createUserRequest):
            let requestData = encode(createUserRequest)
            return .request(.data(requestData))

        case .signIn(let signInRequest):
            let requestData = encode(signInRequest)
            return .request(.data(requestData))
        }
    }
}

// MARK: - Helper Methods
extension Tm5Target {
    private func encode<T: Encodable>(_ encodable: T) -> Data {
        do {
            return try JSONEncoder().encode(encodable)
        } catch {
            fatalError("Could not encode '\(type(of: encodable))' using JSONEncoder. Error: \(error)")
        }
    }
}

It builds fine and I think it should work (haven't tested yet). But this is how the usage might look now. Just to have something to review.

@sunshinejr
Copy link
Member

sunshinejr commented Jun 29, 2017

I will go ahead and say we might not want to add it to 9.0.0 release since we have many breaking changes already (and few big ones). To make transition smoother we may just hold onto this one till after the release. This is a really big change and a great amount of work from @Dschee, so it would be awesome to get some feedback from @Moya/contributors in the meanwhile.

This PR helps with a lot of undefined behavior but from the user perspective it might look messy. We may need to think about this one a little bit more:

return .request(.parameters(["test1": "test", "test2": "test"]))

Looks kinda uncomfortable at first glance. And especially because it is the default use-case, we may think of a cleaner solution. Would one enum instead of 3 help? Not sure. Opinions?

@ashfurrow
Copy link
Member

I looked this over and it seems to make sense to me.

I agree with @sunshinejr that it gets a lot more verbose at the call site... definitely not great. I'm wondering if instead of associating the new RequestDataType with the request enum case, we can extend the number of enum cases? like requestWithData, requestWithParameters etc?

@Jeehut
Copy link
Contributor Author

Jeehut commented Jul 3, 2017

@ashfurrow Sure, why not. Think that makes sense. Just rebased onto the current master and added the requested change. The task computed property example from above now looks like this:

    var task: Task {
        switch self {
        case .createUser(let createUserRequest):
            return .requestData(encode(createUserRequest))

        case .signIn(let signInRequest):
            return .requestData(encode(signInRequest))
        }
    }

Copy link
Member

@ashfurrow ashfurrow left a comment

Choose a reason for hiding this comment

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

This makes a lot of sense, good work 👍 What do you think @sunshinejr ?

@Jeehut
Copy link
Contributor Author

Jeehut commented Jul 3, 2017

Shall I flatten the UploadType and DownloadType enums into the Task enum as well? It would be consistent, for sure. But I'm not sure cause there's no pressing issue with them and it would require more people to change their code when updating to a newer version, so I'd understand if we would keep them as is. But we should probably tackle that in a different PR anyways to keep this lean.

@Jeehut
Copy link
Contributor Author

Jeehut commented Jul 3, 2017

Any preferences on how to deal with the exceptions thrown in places like this:

do {
    preparedRequest = try parameterEncoding.encode(preparedRequest, with: parameters)
} catch {
    // TODO: Add exception handling here
}

@ashfurrow @sunshinejr ?

I think, I could deal with the other TODO steps left myself, but I'm not sure about that one.

case requestData(Data)

/// A requests body set with parameters and encoding.
case requestEncoded(parameters: [String: Any], encoding: ParameterEncoding)
Copy link
Member

Choose a reason for hiding this comment

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

maybe request/requestParameters? or requestWithParameters? I feel like encoded is not as a user-friendly name as parameters 😄 requestWithParameters reminds me of Swift 2.0 though 😞

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've renamed to requestParameters, although I also considered requestParameterEncoded (tell me if you like that more, otherwise I'll keep it).


/// The method used for parameter encoding.
var parameterEncoding: ParameterEncoding { get }
/// The default parameterEncoding for the `.encoded` RequestDataType case.
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we don't actually need to add this to the core. We do not use it anywhere, and this can be easily added to the migration guide as a best practice to use. Or better yet, make an extension to ParameterEncoding:

extension ParameterEncoding. {
    static var default: JSONEncoding {
        return JSONEncoding.default
    }
}

This way it would be easily used in task:

return .request(parameters: [:], encoding: .default)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did as suggested. Two variations though. One: I used ParameterEncoding as the return type isntead of JSONEncoding. Two: I had to backtick default as I saw an error (in Xcode 9 beta 3).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually the extension didn't work correctly. I received the following error in Xcode 8.3:
Static member 'default' cannot be used on protocol metatype 'ParameterEncoding.Protocol'

I'm removing it now ... I think JSONEncoding.default and URLEncoding.default should be fine enough for now.

cancellableToken.innerCancellable = self.sendRequest(target, request: preparedRequest, queue: queue, progress: progress, completion: networkCompletion)

case let .requestEncoded(parameters: parameters, encoding: parameterEncoding):
do {
Copy link
Member

Choose a reason for hiding this comment

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

Usually this is done in an Endpoint, take a look here. If we cannot encode parameters, we throw a MoyaError.requestMapping(_:) error to the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I don't really understand how I could throw a MoyaError.requestMapping from within the performNetworking closure, also I have no idea how to move the code to the endpoint. How can I switch on the target.task from within the Endpoint class? Need some help with this ...

@sunshinejr
Copy link
Member

I left you comments around most issues @Dschee. For flattening I agree we would need to flat upload & download types as well. I think we might do it in the same PR since naming convention in this one would be similar.

Anyhow, I like the direction that this PR has taken. Keep it up :)

@Jeehut
Copy link
Contributor Author

Jeehut commented Jul 17, 2017

Sorry for letting you wait for two weeks, I've been on vacation this entire time. Now that I'm back I've had a look onto your feedback @sunshinejr and commented on them (two of three I fixed). Also I have flattened the upload and download types into the Task enum. Unfortunately I will need more help to handle the exceptions properly (the TODOs).

Other than that, is there anything that speaks against merging this except for the TODOs on the initial Post that are still open? If not, then I would proceed implementing the TODOs so we can merge this soon.

@sunshinejr
Copy link
Member

Hey @Dschee, just a heads-up. I'll try to get to it ASAP and edit this comment. Thank you for the constant work on this one. 👍

@Jeehut Jeehut changed the title [WIP] Replace parameters & encoding with extended Task Replace parameters & encoding with extended Task Jul 20, 2017
@Jeehut
Copy link
Contributor Author

Jeehut commented Jul 20, 2017

Okay, I have now worked through my TODOs. Except for the exception handling, I think this is now finished. I've removed the [WIP] from the title cause I think it's theoretically mergeable as it is, cause I can imagine the work on the exception handling could be decided upon later and the TODO's kept in code until then, as it is right now.

Btw, when I updated the docs, I've seen that some examples are outdated and I didn't fix that (although my code change has to do with it) cause it is using an old syntax of Alamofire. Specifically I'm talking about https://github.com/Moya/Moya/blob/master/docs/Examples/ArrayAsRootContainer.md

We should think about removing or updating it in a different PR / Issue.

@Jeehut
Copy link
Contributor Author

Jeehut commented Jul 20, 2017

Just rebased onto the current master so things are updated (and tests can pass, hopefully).

@Jeehut Jeehut changed the title Replace parameters & encoding with extended Task Replace parameters & encoding with extended Task + Flattening enums Jul 20, 2017
@Jeehut Jeehut changed the title Replace parameters & encoding with extended Task + Flattening enums Flatten enums & parameters + encoding into extended Task type Jul 20, 2017
@Jeehut Jeehut changed the title Flatten enums & parameters + encoding into extended Task type Flatten enums + parameters with encoding into extended Task type Jul 20, 2017
@Jeehut Jeehut changed the title Flatten enums + parameters with encoding into extended Task type Flatten enums & parameters into extended Task type Jul 20, 2017
httpHeaderFields: [String: String]? = nil) {

self.url = url
self.sampleResponseClosure = sampleResponseClosure
self.method = method
self.parameters = parameters
Copy link
Member

Choose a reason for hiding this comment

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

I think that we can't just remove option to update parameters/parameterEncoding in the Endpoint closure. Yes, we don't have it as separate properties, but we can replace it with task - it would be even more customizable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, let me try ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I got this now. Note that there's no convenience .addingParameters or something. Don't know if we can provide a replacement with the task enum. We can't append all enum cases with each other (e.g. Data with Data), can we?

Copy link
Member

Choose a reason for hiding this comment

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

We probably can't, yeah. But I think just the fact that you can change the task yourself in the endpointClosure is enough, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. 👍

@@ -99,7 +69,7 @@ extension Endpoint {
request.httpMethod = method.rawValue
request.allHTTPHeaderFields = httpHeaderFields

return try? parameterEncoding.encode(request, with: parameters)
return request
Copy link
Member

Choose a reason for hiding this comment

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

This is a place where you would do a switch and prepare the request based on a task. We don't really want to do it in the core file since preparing the request based on an Endpoint is Endpoint's responsibility. Another benefit is the fact that when we return nil, there is an error thrown in basic request mapping and we don't even have to do a request (source).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I see. Let me try ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I got this, too. Trying to clean up the internal MoyaProvider now ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, that was easy. As you suggested, I'm now returning nil when an exception is thrown. This way I can even mark my last TODO as completed. Thanks for the feedback! 👍

@@ -30,37 +24,42 @@ public protocol TargetType {
}

public extension TargetType {
var defaultParameterEncoding: ParameterEncoding {
Copy link
Member

Choose a reason for hiding this comment

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

I think we can remove the defaultParameterEncoding from our codebase - I meant that someone can implement it in their own codebase if its convenient for them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Uh, I forgot to remove it (thought I actually did that). No problem with that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed this.


/// Represents a type of download task.
public enum DownloadType {
/// A requests body set with parameters and encoding.
Copy link
Member

Choose a reason for hiding this comment

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

"A requests body set with encoded parameters"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Readme.md Outdated

Please follow the appropriate guide below when **upgrading to a new major version** of Moya (e.g. 8.0 -> 9.0).

### Upgrade from 8.x to 9.x
Copy link
Member

Choose a reason for hiding this comment

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

I feel like this have its own file in the docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I've moved it to its own MigrationGuides.md file within the docs folder. I think we should consider linking to the file when doing major releases within the changelog & release notes.

/// Represents a type of upload task.
public enum UploadType {
/// Represents an HTTP task.
public enum Task {
Copy link
Member

Choose a reason for hiding this comment

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

we also need to properly test encoding the request from an endpoint

Copy link
Contributor Author

@Jeehut Jeehut Jul 21, 2017

Choose a reason for hiding this comment

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

Can we do that in a subsequent PR so this one can be in the 9.0 release? Cause I won't have much time to work on this in the near future (I'm concentrating my free time onto this project and on Carthage/Carthage#1990 right now) and I feel like the logic of adding the task cases data to the request isn't overly complicated, so it should all work fine. Maybe except for some edge cases, but those would probably be related to the new cases (like .composite...) anyways ... which nobody uses yet.

Copy link
Member

Choose a reason for hiding this comment

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

Making this one into the 9.0.0 release would be cool, but we can't do that without testing it (this is one of the major things we changed in the whole core). We can do that in separate PR, but still I think merging this should happen with tests in it.

I understand that you may be busy now, though. If that's the case, let me know and maybe someone can help with the tests and additional feedback on the code review (and rebase in the near future). There is still work to do, unfortunately, but we are close.

Copy link
Contributor Author

@Jeehut Jeehut Jul 28, 2017

Choose a reason for hiding this comment

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

I have rebased and worked through some more feedback from @SD10. I'd be happy to work through any other feedback this weekend, but regarding tests it would be great to have help there. I don't think I'll have the time to set them up in addition to reacting to change requests ...

Copy link
Member

Choose a reason for hiding this comment

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

I can help with tests, no problem with that 👍 I'll try to do it ASAP.

@sunshinejr sunshinejr mentioned this pull request Jul 21, 2017
4 tasks
httpHeaderFields: [String: String]? = nil) {

self.url = url
self.sampleResponseClosure = sampleResponseClosure
self.method = method
self.parameters = parameters
Copy link
Member

Choose a reason for hiding this comment

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

We probably can't, yeah. But I think just the fact that you can change the task yourself in the endpointClosure is enough, though.

request.httpBody = data

case let .requestParameters(parameters: parameters, encoding: parameterEncoding):
do {
Copy link
Member

Choose a reason for hiding this comment

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

we can just do returns in cases I think:

return try? parameterEncoding.encode(request, with: parameters)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, updated the cases requestParameters and downloadParameters where this was possible. This didn't work in the composite cases though as they need to to multiple things with the request.

@Jeehut
Copy link
Contributor Author

Jeehut commented Jul 27, 2017

Worked through your comments again and rebased. Are you planning to add this to the current release after the alpha is out? Would be awesome to see this in the next version. :)

@sunshinejr
Copy link
Member

Thanks, @Dschee! I'd love to have it, but we are still without tests and I'm not the only one to decide such things. Would love to get some feedback on it from @Moya/contributors, since it is almost finished now.

Also, sorry that I didn't mentioned it earlier, but would you rebase it to 9.0.0-alpha.1 branch, please?

@Jeehut
Copy link
Contributor Author

Jeehut commented Aug 9, 2017

Thanks @sunshinejr, I've worked through your comments and rebased onto the 9.0.0-dev branch!

@codecov-io
Copy link

Codecov Report

Merging #1147 into 9.0.0-dev will decrease coverage by 1.05%.
The diff coverage is 58.33%.

Impacted file tree graph

@@              Coverage Diff              @@
##           9.0.0-dev    #1147      +/-   ##
=============================================
- Coverage      79.53%   78.48%   -1.06%     
=============================================
  Files             23       23              
  Lines            738      739       +1     
=============================================
- Hits             587      580       -7     
- Misses           151      159       +8
Impacted Files Coverage Δ
Sources/Moya/TargetType.swift 100% <ø> (ø) ⬆️
Sources/Moya/MultiTarget.swift 100% <ø> (ø) ⬆️
Sources/Moya/MoyaProvider+Defaults.swift 95% <100%> (-0.24%) ⬇️
Sources/Moya/Endpoint.swift 66.03% <36.36%> (-27.97%) ⬇️
Sources/Moya/MoyaProvider+Internal.swift 67.44% <76%> (+3.76%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update cd290c7...2216c2f. Read the comment docs.

Copy link
Member

@SD10 SD10 left a 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 ready to go after testing. The basic documentation updates look good too. I'm a bit busy right now but can help with any missing documentation after the merge.

@Dschee Thank you for all your hard work and re-iterating on all the requested changes 🍻

Copy link
Member

@sunshinejr sunshinejr left a comment

Choose a reason for hiding this comment

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

Let's merge it and test it both manually and automatically to make sure we didn't miss anything 👍 Thank you so much for your work @Dschee and everyone involved, really good work around here, 100+ comments, 30+ commits 😮

@sunshinejr sunshinejr merged commit a58202d into Moya:9.0.0-dev Aug 9, 2017
@BasThomas
Copy link
Contributor

Amazing work everyone, @Dschee especially! 💪

@Jeehut
Copy link
Contributor Author

Jeehut commented Aug 10, 2017

Thank you everyone for reviewing and going all the way with me. You're the best! 💪 👏 👍

@T1ASH
Copy link

T1ASH commented Sep 14, 2017

Thank you for your work on this, however I'm having issues getting it to work. I looked through this thread hoping to figure it out, but unfortunately I've come up empty. Is there an example or documentation on how to get .requestData to work in my Task?

@sunshinejr
Copy link
Member

sunshinejr commented Sep 14, 2017

Hey @T1ASH. If I understand your question correctly, you can do it as follows:

enum GitHub {
    case zen
}

extension GitHub: TargetType {
    ...
    var task: Task {
        let data = "test data".data(using: .utf8)
        return .requestData(data)
    }
    ...
}

Where, of course, your data is whatever you want to send in this endpoint. This will send the data request (meaning it will replace httpBody in the URLRequest) for GitHub.zen endpoint.

Let me know if this is what you wanted or I've completely missed 😄

@pedrovereza
Copy link
Member

@sunshinejr I Edited your reply to be return .requestData(data) instead of return .requestData() 😉

@sunshinejr
Copy link
Member

Damn it! Thanks for the great catch, though, @pedrovereza!

@jonathansolorzn
Copy link

I have just integrated my fork into the project where I'm using the Encodable type.

Here's how my target now looks:

import Foundation
import Moya

enum MyTarget {
    case createUser(CreateUserRequest)
    case signIn(SignInRequest)
}

// MARK: - TargetType Protocol Implementation
extension MyTarget: TargetType {
    var baseURL: URL {
        return URL(string: "https://example.com/api/v1/")!
    }

    var path: String {
        switch self {
        case .createUser:
            return "user"

        case .signIn:
            return "session"
        }
    }

    var method: Moya.Method {
        return .post
    }

    var sampleData: Data {
        return Data()
    }

    var task: Task {
        switch self {
        case .createUser(let createUserRequest):
            let requestData = encode(createUserRequest)
            return .request(.data(requestData))

        case .signIn(let signInRequest):
            let requestData = encode(signInRequest)
            return .request(.data(requestData))
        }
    }
}

// MARK: - Helper Methods
extension Tm5Target {
    private func encode<T: Encodable>(_ encodable: T) -> Data {
        do {
            return try JSONEncoder().encode(encodable)
        } catch {
            fatalError("Could not encode '\(type(of: encodable))' using JSONEncoder. Error: \(error)")
        }
    }
}

It builds fine and I think it should work (haven't tested yet). But this is how the usage might look now. Just to have something to review.

How can I use the encode extension? Was it added to the library or should I simple add it myself?

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.

None yet

10 participants