2
\$\begingroup\$

I need to append some query parameters to my API URL. The consumer of my API would pass me this optional structure called as APIParameters, which contain 5 optionals, one array of strings and rest strings

Lets assume the following:

let baseURL = "baseURL"
let path = "path"

This is the final URL structure

//assuming since and offset is nil, this would be the URL 
result = "baseURL" + "path" + "?"+ "with=some1,some2,some3&limit=100&width=0"

To solve this, I made a struct, and added a method to return me the string that I need to append to my URL.

struct APIParameters {
    var with: [String]?
    var since: NSDate?
    var width: Int?
    var offset: Int?
    var limit: Int?

    func stringToAppendToURL() -> String{
    //started with this approach

        var stringToAppend = ""

        if with != nil {

            var withParamaters = ",".join(with!)
            println("withParamaters = \(withParamaters)")

            stringToAppend += "with=\(withParamaters)"

        }

        if limit != nil {
            println("limit = \(limit)")



        }
    //realized this would be too long so tried using switch 


        switch (with, limit, width, offset, since){
        case (nil, nil, nil, nil, nil):
            return ""
        case (.Some(let withParameters), nil, nil, nil, nil):
            return "with=" + ",".join(withParameters)
        case (nil, .Some(let limitParams), nil, nil, nil):
            return "limit=\(limitParams)"
        case (nil, nil, .Some(let widthParams), nil, nil):
            return "width=\(widthParams)"
        case (nil, nil, nil, .Some(let offsetParams), nil):
            return "offset=\(offsetParams)"
        case (nil, nil, nil, nil, .Some(let sinceParams)):
            return "since=\(Int(sinceParams.timeIntervalSince1970))"
        case (.Some(let withParams), .Some(let limitParams), nil, nil, nil):
        return "with=" + ",".join(withParams) + "&" + "limit=\(limitParams)"
    //realized that number of conditions would become too many so decided to ask for help 

        default:
            return ""

        }


    }
}

Example:

let apiParams1 = APIParameters(with: ["option", "full"], since: nil, width: nil, offset: nil, limit: 10)
let apiParams2 = APIParameters(with: ["option", "full"], since: nil, width: nil, offset: nil, limit: nil)


println("\(apiParams1.stringToAppendToURL())")
println("\(apiParams2.stringToAppendToURL())")

Output:

"with=option,full&limit=10"
"with=option,full"

Is there a more elegant way to do this?

\$\endgroup\$

3 Answers 3

3
\$\begingroup\$

I would use the fact that assigning nil into a dictionary means no value is inserted. You can just put all the values in the dictionary, not worrying about nil, and use the keys as the parameter names:

var with: [String]? = ["a","b"]
var since: NSDate? = NSDate()
var width: Int? = 5
var offset: Int?
var limit: Int?

var params: [String:String] = [:]

params["withParameters"] = with.map { ",".join($0) }
params["limit"] = limit.map(toString)
params["width"] = width.map(toString)
params["since"] = map(since?.timeIntervalSince1970) { toString(Int($0)) }

Here, the map is optional map – if the optional contains a value, it runs the supplied function on that value and returns that as an optional. But if its nil, it does nothing and returns nil.

(if you wanted to supply default values, you could also do params["limit"] = toString(limit ?? 0))

Then you can render the dictionary down into a single string joined by &:

let urlParameters = "&".join( map(params) { "\($0)=\($1)" } )
// urlParameters is now width=5&since=1430347776&withParameters=a,b

Here, the map is a map on SequenceType, dictionary sequences being key/value pairs.

If you care about the order of the parameters, you might want a sorted in there before the map.

\$\endgroup\$
2
  • \$\begingroup\$ elegant! I think i need to wrap my head around map better! \$\endgroup\$
    – ManicMonkOnMac
    Apr 29, 2015 at 23:09
  • \$\begingroup\$ have added a bit more description of what the two different maps are doing \$\endgroup\$ Apr 29, 2015 at 23:22
3
\$\begingroup\$

I do it a bit more elegantly I believe. First, this extension function is pretty useful.

extension Array {
    static func filterNils(array: [T?]) -> [T] {
        return array.filter { $0 != nil }.map { $0! }
    }
}

An example of my request models.

class PostRequest: BaseRequest {
    var startIndex: Int?
    var fetchCount: Int?
    var programID: String?
    var userID: String?
    let postType: FeedPostType

    var userPostFilter: String? {
        switch postType {
        case .User:
            return "all"
        default:
            return nil
        }
    }

    init(postType: FeedPostType) {
        self.postType = postType
    }

    override var path: String {
        let basePath = "/posts"
        var arguments: [String] = Array.filterNils([basePath,postType.rawValue,programID,userID,userPostFilter,startIndex?.toString(),fetchCount?.toString()])
        return "/".join(arguments)
    }
}

toString is also an extension on Int

extension Int {
    func toString() -> String {
        return String(self)
    }
}
\$\endgroup\$
5
  • 1
    \$\begingroup\$ I can't recommend writing that Int extension, given .description already exists and is basically the same thing. \$\endgroup\$
    – nhgrif
    Apr 30, 2015 at 0:03
  • 1
    \$\begingroup\$ @nhgrif agree, in fact, toString already exists as a free function (and is probably preferable to .description – note, String doesn’t conform to Printable :) Also, I would write filterNils as a free function that takes a generic SequenceType rather than a static extension to array i.e. func filterNils<S: SequenceType, T where S.Generator.Element == T?>(source: S) -> [T] { return filter(source) { $0 != nil }.map { $0! } } \$\endgroup\$ Apr 30, 2015 at 1:43
  • \$\begingroup\$ @AirspeedVelocity thanks for the insight! Very good points. \$\endgroup\$
    – Kaan Dedeoglu
    Apr 30, 2015 at 9:25
  • \$\begingroup\$ @AirspeedVelocity, Also, there's a catch with using the global toString free function with optional values. You get "Optional(5)" if you evaluate an optional Int with value 5. \$\endgroup\$
    – Kaan Dedeoglu
    Apr 30, 2015 at 9:31
  • \$\begingroup\$ @KaanDedeoglu yup hence the limit.map(toString) in my answer :) \$\endgroup\$ Apr 30, 2015 at 9:32
2
\$\begingroup\$

This is a nice question and the answer by @Airspeed Velocity is an aesthetic experience. I'm afraid the following is unlikely to be to everyone's taste.

To begin with, notice that the problem is domain specific and that as such it may be calling for a domain specific solution. To this end we may introduce a custom infix operator that would allow us to express a query string as follows:

var options: [String]? = ["a","b"]
var date: NSDate? = NSDate()
var offset: Int?
var limit: Int? = 3

"example/?" &+ ("with", options) &+ ("since", date) &+ ("width", 5) &+ ("offset", offset) &+ ("limit", limit)

//--> example/?with=a,b&since=452592878&width=5&limit=3

(If you are sceptical about custom operators in general, please consider that it is now possible to Alt- and Command-Click on all custom operators to instantly reveal the exact api that is being invoked.)

Notice that the infix operator &+ besides being almost self-explanatory also preserves the relative order of field-value pairs. Notice also that (String, T?) tuples with a nil value are completely omitted from the resulting string. Moreover, date and array properties are appropriately formatted (as seconds since reference date and a comma-separated list). The "width" field also shows that we can pass in any expression (an Int literal in this case).

The base implementation of this operator may be written as follows:

import Foundation

infix operator &+ { associativity left }

func &+ <T> (lhs: String, rhs: (String, T)) -> String {
    let amper = contains(lhs, "=") ? "&" : ""
    return "\(lhs)\(amper)\(rhs.0)=\(rhs.1)"
}

This only caters for tuples of (String, T) type – i.e. where value is not an optional and which is meant to be string interpolated using Swift's default implementation. That somewhat awkward first line of the function is only meant to ensure that the ampersand is not used for the first field-value pair.

To support optional values, we can introduce a helping function that delegates to the base implementation if rhs has a value:

func &+ <T> (lhs: String, rhs: (String, T?)) -> String {
    if rhs.1 == nil { return lhs }
    return lhs &+ (rhs.0, rhs.1!)
}

Hereon, we can add helper functions as needed, e.g. for NSDate and SequenceType values:

func &+ (lhs: String, rhs: (String, NSDate?)) -> String {
    if rhs.1 == nil { return lhs }
    return lhs &+ (rhs.0, Int(rhs.1!.timeIntervalSinceReferenceDate))
}

func &+ <S: SequenceType> (lhs: String, rhs: (String, S?)) -> String {
    if rhs.1 == nil { return lhs }
    return lhs &+ (rhs.0, join(",", map(rhs.1!, toString)))
}

Of course, one could implement this for the regular & or + operator, but I personally prefer that &+ is custom. An obvious drawback is that the & symbol is already used for other, very different things. On the other hand, for someone who often writes query strings, the expressiveness of &+ and the resulting code may well be the most important consideration...

\$\endgroup\$
1
  • \$\begingroup\$ this is such an amazing solution and I learnt a lot from it, i thought infix operator would be really hard but you explained your thought process very well. \$\endgroup\$ May 12, 2015 at 22:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.