Moya: Optional parameters

Created on 27 Dec 2015  Â·  20Comments  Â·  Source: Moya/Moya

I have a couple of endpoints with optional parameters specified like this

public enum MyAPI {
    ...
    case Content(Int, Int?, String?)
    ...
}

The problem is that public var parameters expects a type [String: AnyObject]? so if I pass optional value I have to unwrap it with ! operator which causes Exception if the parameter is nil.

I was thinking about filtering all the params from the dictionary where the value is nil or is there any other way? Like for example specifying parameters as [String: AnyObject?]?

Thanks.

documentation

Most helpful comment

I've an idea, but may break design goal of Moya: Providing a non-generic MoyaProvider

enum isn't a good way for complex endpoints

For example, there is an endpoint

api/v1/topics

| param | type | optional | sample | desc |
| --- | --- | --- | --- | --- |
| offset | Integer | Yes | 20 | |
| limit | Integer | Yes | 20 | |
| type | String | Yes | recent | should in last_actived, recent, no_reply, popular, excellent |
| node_id | Int | Yes | 1 | |

In Moya flavour, I must define a case like Topics(Int?, Int?, String?, Int?), that's unreadable and uneasy to use.

How about struct or class ?

For complex endpoints, struct or class has definitly advantage than enum, and Moya can use it actually (if they adopt the TargetType protocol).

struct ListingTopics: TargetType {
    enum TypeFieldValue: String {
        case LastActived = "last_actived"
        case Recent = "recent"
        case NoReply = "no_reply"
        case Popular = "popular"
        case Excellent = "excellent"
    }

    var type: TypeFieldValue?
    var nodeId: String?
    var offset: Int?
    var limit: Int?

    init(type: TypeFieldValue? = nil, nodeId: String? = nil, offset: Int? = nil, limit: Int? = nil) {
        self.type = type
        self.nodeId = nodeId
        self.offset = offset
        self.limit = limit
    }

    var baseURL: NSURL { return Global.baseURL }
    var path: String { return "api/v1/topics" }
    var method: Moya.Method { return .GET }
    var parameters: [String: AnyObject]? {
        var parameters = [String: AnyObject]()

        if let type = self.type {
            parameters["type"] = type.rawValue
        }
        if let nodeId = self.nodeId {
            parameters["nodeId"] = nodeId
        }
        if let offset = self.offset {
            parameters["offset"] = offset
        }
        if let limit = self.limit {
            parameters["limit"] = limit
        }

        return parameters
    }
}

BUT, there's a little STRANGE when using:

provider = MoyaProvider<ListingTopics>()
provider.request(ListingTopics(nodeId: 1, type: .Recent)) { ... }

The provider rely on ListingTopics so that can't be reuse.

Conclusion

Moya is limiting by generic MoyaProvider

One More Thing

For simple endpoints, struct or class still works well

struct GetTopic: TargetType {
    var id: String

    init(id: String) {
        self.id = id
    }

    var baseURL: NSURL { return RubyChinaV3.BaseURL }
    var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)" }
    var method: NetworkAbstraction.Method { return .GET }
    var parameters: [String: AnyObject]? { return nil }
}

Not too much longer than enum's, and there's another advantage for RESTful-flavour: namespace

struct MySiteAPI {
    static let baseURL = NSURL("http://mysite.fake")!
}

extension MySiteAPI {
    struct Topics {
        static let path = "topics"
    }
}

extension MySiteAPI.Topics {
    struct Listing: TargetType { ... }
    struct Get: TargetType { ... }
}

That's all I thought.

All 20 comments

Hmm, good question! I think the filtering idea you have could work, but it's not ideal. Like, Moya should handle this somehow. Let me think about it, maybe someone else has a better idea. In the meantime, filtering is your best bet.

Thanks for such quick answer.

I have found another workaround. Assigning nil to initialized dictionary is equal to removing the key from the dictionary so instead of naming all the params in the initializer you create new empty dictionary of type [String: AnyObject]? and than you assign each parameter like parameters["foo"] = nil.

Ah, that makes sense! We should add that as an example to our documentation. Would you like to send a pull request? :wink:

I will have time for PR hopefully anytime soon :+1:

Cool, thanks! Take your time, no rush! :christmas_tree:

With a separated out TargetType for each endpoint I take this approach:

var parameters: [String: AnyObject]? {
    let optionalParameters: [String: AnyObject?] = ["value": nonOptionalValue, "optionalValue": optionalValue]
    return optionalParameters.mapMaybe { $0 }
}

where mapMaybe is to take a [String: AnyObject?] to [String: AnyObject]

As from Swiftz:

func mapMaybe<Value2>(f : Value -> Value2?) -> [Key : Value2]

I've an idea, but may break design goal of Moya: Providing a non-generic MoyaProvider

enum isn't a good way for complex endpoints

For example, there is an endpoint

api/v1/topics

| param | type | optional | sample | desc |
| --- | --- | --- | --- | --- |
| offset | Integer | Yes | 20 | |
| limit | Integer | Yes | 20 | |
| type | String | Yes | recent | should in last_actived, recent, no_reply, popular, excellent |
| node_id | Int | Yes | 1 | |

In Moya flavour, I must define a case like Topics(Int?, Int?, String?, Int?), that's unreadable and uneasy to use.

How about struct or class ?

For complex endpoints, struct or class has definitly advantage than enum, and Moya can use it actually (if they adopt the TargetType protocol).

struct ListingTopics: TargetType {
    enum TypeFieldValue: String {
        case LastActived = "last_actived"
        case Recent = "recent"
        case NoReply = "no_reply"
        case Popular = "popular"
        case Excellent = "excellent"
    }

    var type: TypeFieldValue?
    var nodeId: String?
    var offset: Int?
    var limit: Int?

    init(type: TypeFieldValue? = nil, nodeId: String? = nil, offset: Int? = nil, limit: Int? = nil) {
        self.type = type
        self.nodeId = nodeId
        self.offset = offset
        self.limit = limit
    }

    var baseURL: NSURL { return Global.baseURL }
    var path: String { return "api/v1/topics" }
    var method: Moya.Method { return .GET }
    var parameters: [String: AnyObject]? {
        var parameters = [String: AnyObject]()

        if let type = self.type {
            parameters["type"] = type.rawValue
        }
        if let nodeId = self.nodeId {
            parameters["nodeId"] = nodeId
        }
        if let offset = self.offset {
            parameters["offset"] = offset
        }
        if let limit = self.limit {
            parameters["limit"] = limit
        }

        return parameters
    }
}

BUT, there's a little STRANGE when using:

provider = MoyaProvider<ListingTopics>()
provider.request(ListingTopics(nodeId: 1, type: .Recent)) { ... }

The provider rely on ListingTopics so that can't be reuse.

Conclusion

Moya is limiting by generic MoyaProvider

One More Thing

For simple endpoints, struct or class still works well

struct GetTopic: TargetType {
    var id: String

    init(id: String) {
        self.id = id
    }

    var baseURL: NSURL { return RubyChinaV3.BaseURL }
    var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)" }
    var method: NetworkAbstraction.Method { return .GET }
    var parameters: [String: AnyObject]? { return nil }
}

Not too much longer than enum's, and there's another advantage for RESTful-flavour: namespace

struct MySiteAPI {
    static let baseURL = NSURL("http://mysite.fake")!
}

extension MySiteAPI {
    struct Topics {
        static let path = "topics"
    }
}

extension MySiteAPI.Topics {
    struct Listing: TargetType { ... }
    struct Get: TargetType { ... }
}

That's all I thought.

Actually I'm doing some researching.

(PS: I'm a full stack Ruby on Rails developer but still a new guy to learning Swift and iOS development about 1 month)
(PS2: I really appreciate Moya and SwiftyJSON)

Here's my project for learning (a client for Ruby-China community):https://github.com/jasl/RubyChinaAPP

It's not done yet, but I've done some interesting works about invoking endpoints.

@jasl You can used labeled parameters with an enum in Swift, they're just not required. So for longer endpoints such as in your example, I would definitely use labeled parameters to improve readability.

So your endpoint would become:

enum MyTarget {
  case Topics(offset: Int?, limit: Int?, type: String?, nodeID: Int?)
}

And using your provider, it would be:

let provider = MoyaProvider<MyTarget>()
provider.request(.Topics(offset: 20, limit: nil, type: nil, nodeID: nil), completion { ... })

@aamctustwo but you can't give them default vaules

Besides, when the APIs growth big (consider there have hundreds endpoints), organizing them by enum seem to be difficult.

For example:
https://github.com/artsy/eidolon/blob/master/Kiosk/App/Networking/ArtsyAPI.swift
We need switch ... case on every properties, just for path we have 55 lines in https://github.com/artsy/eidolon/blob/master/Kiosk/App/Networking/ArtsyAPI.swift#L51-L106, and if I ask you: what path is the AuctionListings used for? first you need find the definition of path then search AuctionListings

But for struct or class, that will be fine.

Anyway, I admit in usual case, enum might be a good way.

@jasl You can provide default parameter values in your definition of the parameters variable by using the nil coalescing operator. Example:

var parameters: [String: AnyObject]? {
  switch self {
    case let .Topics(offset, limit, type, nodeID):
      return ["offset" : offset ?? 20,
                 "limit" : limit ?? 20,
                 "type" : type ?? "recent",
                 "node_id" : nodeID ?? 1]
  }
}

Also, for cases where you have hundreds of endpoints, it may be better to categorize them into separate enums anyway for organization. It'd be messy for than many endpoints in any type structure - enum, struct, or class.

@aamctustwo sorry I'm misled by struct and enum, actually choose what is no mattar

My point is, generic Provider limiting its flexible.

  • It should be reused, as your said endpoints can be categorized into separate enums, that's really solved endpoints management, but Providers still need to be initialized for every enums with the same configuration but with different type, if a Provider can be reused for a specified type, why can not be reused for all types?
  • There is little benefit from generic except made easier invoking request when give it an enum (provider.request(MySiteAPI.Topics) to provider.request(.Topics))

Interesting discussion! I hadn't considered making the provider non-optional, since the original Moya was heavily configuration-based (where now you can create a provider with sensible defaults by passing in no initializer parameters at all).

I can see both sides to this. On the one hand, having a shared provider for all networking (multiple target types) would be convenient. OTOH, custom behaviour around specific cases of the enum (or whatever) is really hard without generics (see this example).

I'd be comfortable moving away from generics if there is a compelling reason and if we can keep the existing philosophy and customizability intact. That second part is a big question mark right now, and it may be outside the scope of optional parameters – maybe it's time to make a new issue?

It's also worth noting that for a large number of endpoints, something like a generated-enum may work, which has been an open issue for some time.

@ashfurrow
Without generic Endpoint your example still work with a small refactor, the key is protocol contravariance.

the refactored codes should looks like:

let endpointClosure = { (target: TargetType) -> Endpoint in
    let endpoint: Endpoint = Endpoint(URL: url(target), sampleResponseClosure: {.NetworkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters)

    // Here's the magic
    guard let myTarget = target as? MyTarget else {
        return endpoint
    }

    // Sign all non-authenticating requests
    switch myTarget {
    case .Authenticate:
        return endpoint
    default:
        return endpoint.endpointByAddingHTTPHeaderFields(["AUTHENTICATION_TOKEN": GlobalAppStorage.authToken])
    }
}

Yeah, that _compiles_ but as I tried to describe in my earlier comment, it betrays the philosophy of Moya. Conditionally casting a parameter to a specific enum (or one of several enums, to expand to many) is... icky.

At a higher level, we're discussing the separation of the one-to-one relationship between the provider and the target type. That's a big change, I think we can all agree, and it's one that would need to be taken with care and thoughtfulness.

@ashfurrow I understand, and I'm still researching how to refactor but keeping interfaces stable, maybe I could PR some proving codes to discuss few days later.

Sounds good!

I _think_ this is resolved, not 100% sure. @jasl sounded like you had an idea of what to do next here, is that right?

@ashfurrow
Let's close this

Was this page helpful?
0 / 5 - 0 ratings