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

Feature - Parameter Encoding Protocol #1465

Merged
merged 8 commits into from Sep 8, 2016

Conversation

cnoon
Copy link
Member

@cnoon cnoon commented Sep 7, 2016

This PR is an alternative to #1464 that goes WAY further in refactoring the ParameterEncoding logic into a protocol instead of an enumeration.

Motivation

The ParameterEncoding enum hasn't been flexible enough to allow us to customize the behavior of a particular encoding type. For example, we had to introduce the new .urlEncodedInURL case just to allow users to be able to force the parameters into the query string when their backends required them to do so on a POST or PUT.

The enum also doesn't allow us to open up different writing options for the JSON and plist serializers. We can't customize plist formats, etc. The list goes on and on since we're locked to the enum that can't add extra cases or associated values in a backwards compatible manner.

Solution

Get rid of the enumeration altogether! Now the ParameterEncoding type of a protocol backed by concrete URL, JSON and PropertyList conforming structs.

public typealias Parameters = [String: Any]

public protocol ParameterEncoding {
    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}

It was amazing after I split everything up how much sense it really makes. ParameterEncoding implementations are now free of the enumeration bounds and are free to customize to their own needs.

URLEncoding

The .url and .urlEncodedInURL cases have now been combined in the URLEncoding struct with some new enumerations and properties of it's own.

public struct URLEncoding: ParameterEncoding {
    public enum Destination {
        case methodDependent, queryString, httpBody
    }

    public static var `default`: URLEncoding { return URLEncoding() }
    public static var methodDependent: URLEncoding { return URLEncoding() }
    public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) }
    public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) }

    public let destination: Destination

    public init(destination: Destination = .methodDependent) {
        self.destination = destination
    }

    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        // left out for simplicity
    }
}

I added convenience properties for accessing instances without having to go through the initializer. This was a nice touch. We could actually hide the initializer if we wanted, but I figured that was a step too far.

JSONEncoding

The JSONEncoding struct is much less complicated than the URLEncoding struct for obvious reasons.

It's simpler!

public struct JSONEncoding: ParameterEncoding {
    public static var `default`: JSONEncoding { return JSONEncoding() }
    public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) }

    public let options: JSONSerialization.WritingOptions

    public init(options: JSONSerialization.WritingOptions = []) {
        self.options = options
    }

    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        // left out for simplicity
    }
}

PropertyListEncoding

The PropertyListEncoding struct is very similar to the JSONEncoding struct, just with a few more properties.

public struct PropertyListEncoding: ParameterEncoding {
    public static var `default`: PropertyListEncoding { return PropertyListEncoding() }
    public static var xml: PropertyListEncoding { return PropertyListEncoding(format: .xml) }
    public static var binary: PropertyListEncoding { return PropertyListEncoding(format: .binary) }

    public let format: PropertyListSerialization.PropertyListFormat
    public let options: PropertyListSerialization.WriteOptions

    public init(
        format: PropertyListSerialization.PropertyListFormat = .xml,
        options: PropertyListSerialization.WriteOptions = 0)
    {
        self.format = format
        self.options = options
    }

    public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        // left out for simplicity
    }
}

If you noticed default and xml are exact duplicate properties, bonus points for you. I wanted to follow the same .default property convention across all the ParameterEncoding types for consistency. That way at the call sight you can always use <encoding_struct>.default and know it will be there. I wonder if that should be enforced in the protocol 🤔

Wrapping Up Parameters with Encoding

So I tried for probably 2 hours to work the parameters and the encoding into a single type. I had Parameters as the protocol and URLParameters, JSONParameters, etc. and you'd create the object like URLParameters(parameters) and call it like so:

Alamofire.request(urlString, parameters: URLParameters(parameters)

Part of this was great because it got rid of the encoding parameter altogether which I was all for. However, it greatly increased the majority of our calls because it can't rely on the default parameter value of .url anymore. So we end up having to write URLParameters(parameters) everywhere instead of just parameters. Not to mention it just feels a little off.

Because of all of this, I finally decided enough was enough and it was just a square peg, round hole scenario. Once I backed away, I was able to finally land on the solution you see here.

Throwing vs. Double Tuple

This also includes all the changes to the encode method itself which was part of the previous PR. These changes should definitely be adopted in AF 4. It's time to eliminate the last odd double optional case we have left! 🙌🏼🏆


Summary

Overall, I think this is an awesome set of changes for AF 4. It makes it so much easier to create your own custom ParameterEncoding type. It also rids the world of the .urlEncodedInURL case that I sadly had to introduce a year ago.

@cnoon cnoon added this to the 4.0.0 milestone Sep 7, 2016
} catch {
encodingError = error
if let method = HTTPMethod(rawValue: urlRequest.httpMethod!), encodesParametersInURL(with: method) {
if var URLComponents = URLComponents(url: urlRequest.url!, resolvingAgainstBaseURL: false), !parameters.isEmpty {
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't think if var was still in Swift 3. Are we sure all of the IUOs here are safe? Or should it be part of the error handling?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure offhand. I didn't change any of this logic directly. Will take another deep look. 👍🏼

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in 8ce9088.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, and if var thing is still allowed in if statements. I think it's only been removed from function signatures.

@jshier
Copy link
Contributor

jshier commented Sep 7, 2016

@cnoon Looks great, just a few comments. Also, how would this interact with the URLRequestConvertible protocol and the router pattern? It would be great to use these encoders or a custom one in a router to be able to generate an error if necessary and pass it through the AF request cycle.

@cnoon
Copy link
Member Author

cnoon commented Sep 7, 2016

I agree @jshier, but unfortunately it's not possible to pass it down through the request cycle. I posted a big description of the limitations in our Slack channel. We should probably have that discussion over a call.

Long story short is that we can't because the only way to push the error down is to actually make (and execute) some request. That causes all sorts of issues.

@jshier
Copy link
Contributor

jshier commented Sep 7, 2016

I remember, just didn't realize the same thing applied here as well. I'll give it more thought and we can make the improvement later.

@cnoon cnoon force-pushed the feature/parameter-encoding-protocol branch from 26190ed to 6a1e267 Compare September 8, 2016 01:09
@cnoon
Copy link
Member Author

cnoon commented Sep 8, 2016

All comments have been addressed here @jshier...thanks for the good catches! I'm going to go ahead and merge this.

@cnoon cnoon merged commit 6814e7a into swift3 Sep 8, 2016
@cnoon cnoon deleted the feature/parameter-encoding-protocol branch September 8, 2016 01:38
@cnoon cnoon removed the in progress label Sep 8, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants