TL;DR - the API cannot be changed, and the variable parameters are a vital part of my app.
I've created a type that stores parameters for an API call.
To make use of them with a successful request, I first need to format them into the URL.
Say, my parameters are expressed as such:
struct Parameters: Encodable {
var name: String
var taste: Taste
var numberOfSpoons: Int?
var topping: Taste?
private enum CodingKeys: String, CodingKey {
case name, taste, topping
case numberOfSpoons = "spoons"
}
init(name: String, taste: Taste, numberOfSpoons: Int? = 1, topping: Taste? = nil){
self.name = name
self.taste = taste
self.numberOfSpoons = numberOfSpoons
self.topping = topping
}
}
Notice that my structure uses both Optional
s (as the API does not need all the parameters from my app) and CodingKeys
(as the names requested by the API look rather ugly in the code, but are necessary).
Here's the Taste
- not much going on except explicit Encodable
conformance.
enum Taste: String, Encodable {
case choco, strawberry, vanilla
}
Now I want to call the API, so, according to the suggestion of John Sundell (I can't provide a link to the article due to Apple's censorship), I'm using URLComponents
. For example:
var components = URLComponents()
components.scheme = "ftp"
components.host = "icecream.ogs"
components.path = "/spoons"
If I were to add queryItems
by hand, it would be fairly easy.
components.queryItems = [
URLQueryItem(name: "name", value: iceCreamParameters.name),
URLQueryItem(name: "numberOfSpoons", value: "\(iceCreamParameters.numberOfSpoons!)")
]
How to dynamically create URLQueryItems from a non-CaseIterable type?
The URLQueryItem
needs both name
and value
parameters. I want the i-th name
to be equal to I-thParameters.PropertyName
, the same goes for values.
As CaseIterable
would not be feasible to implement* in the Parameters
struct, I tried to fake it using the Encodable
protocol - I thought about serializing the type to JSON
(as the encoder gets rid of nil values, too) and then somehow get the keys from a deserialized Dictionary
of AnyType
(?), but there's got to be a better way.
If this approach is not feasible, please provide your answers with helpful examples.
What you want here is a TopLevelEncoder
, like JSONEncode
, that encodes to a URLQueryItems
. Building that is possible but challenging. I know folks who’ve done this for other serialisation formats but my experience is that it involves a lot of work dealing with all the corner cases.
I thought about serializing the type to JSON
That’s what I’d do if I were in your shoes. This is pretty straightforward, at least for the example you posted:
let p = Parameters(name: "99", taste: .vanilla, numberOfSpoons: 1, topping: .choco)
let jsonData = try JSONEncoder().encode(p)
let d = try JSONSerialization.jsonObject(with: jsonData) as! [String: Any]
let q = d.map { k, v in URLQueryItem(name: k, value: "\(v)") }
print(q)
// [spoons=1, taste=vanilla, name=99, topping=choco]
There are other ways you could approach this — driving this from an array of key paths, property wrappers, dynamic value lookup, and so on — but I think it’s going to be hard to beat the above.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"