78

I've read couple tutorials, README from @mattt but can't figure out couple things.

  1. What is the proper usage of URLRequestConvertible in real world API? It looks like if I will create one router by implementing URLRequestConvertible protocol for all API - it will be barely readable. Should I create one Router per endpoint?

  2. Second question most likely caused by lack of experience with Swift language. I can't figure out why enum is used for building router? Why we don't use class with static methods? here is an example (from Alamofire's README)

    enum Router: URLRequestConvertible {
        static let baseURLString = "http://example.com"
        static let perPage = 50
    
        case Search(query: String, page: Int)
    
        // MARK: URLRequestConvertible
    
        var URLRequest: NSURLRequest {
            let (path: String, parameters: [String: AnyObject]?) = {
                switch self {
                case .Search(let query, let page) where page > 1:
                    return ("/search", ["q": query, "offset": Router.perPage * page])
                case .Search(let query, _):
                    return ("/search", ["q": query])
                }
            }()
    
            let URL = NSURL(string: Router.baseURLString)!
            let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(path))
            let encoding = Alamofire.ParameterEncoding.URL
    
            return encoding.encode(URLRequest, parameters: parameters).0
        }
    }
    
  3. There are 2 ways to pass parameters:

    case CreateUser([String: AnyObject])
    case ReadUser(String)
    case UpdateUser(String, [String: AnyObject])
    case DestroyUser(String)
    

    and (say user has 4 parameters)

    case CreateUser(String, String, String, String)
    case ReadUser(String)
    case UpdateUser(String, String, String, String, String)
    case DestroyUser(String)
    

    @mattt is using the first one in the example. But that will lead to "hardcoding" parameters' names outside the router (e.g. in UIViewControllers). Typo in parameter name can lead to error.
    Other people are using 2nd option, but in that case it not obvious at all what each parameter represents.
    What will be the right way to do it?

OgreSwamp
  • 4,602
  • 4
  • 33
  • 54

6 Answers6

112

Great questions. Let's break down each one individually.

What is the proper usage of URLRequestConvertible in real world API?

The URLRequestConvertible protocol is a lightweight way to ensure a given object can create a valid NSURLRequest. There's not really a strict set of rules or guidelines that exist forcing you to use this protocol in any particular way. It's merely a convenience protocol to allow other objects to store state required to properly create the NSURLRequest. Some more information relating to Alamofire can be found here.

Should I create one Router per endpoint?

Definitely not. That would defeat the entire purpose of using an Enum. Swift Enum objects are amazingly powerful allowing you to share a large amount of common state, and switch on the parts that actually different. Being able to create an NSURLRequest with something as simple as the following is really powerful!

let URLRequest: NSURLRequest = Router.ReadUser("cnoon")

I can't figure out why enum is used for building router? Why we don't use class with static methods?

An enum is being used because it is a much more concise way of expressing multiple related objects under a common interface. All the methods are shared between all the cases. If you used static methods, you'd have to have a static method for each case for each method. Or you would have to use an Obj-C styled enum inside the object. Here's a quick example of what I mean.

enum Router: URLRequestConvertible {
    static let baseURLString = "http://example.com"

    case CreateUser([String: AnyObject])
    case ReadUser(String)
    case UpdateUser(String, [String: AnyObject])
    case DestroyUser(String)

    var method: Alamofire.HTTPMethod {
        switch self {
        case .CreateUser:
            return .post
        case .ReadUser:
            return .get
        case .UpdateUser:
            return .put
        case .DestroyUser:
            return .delete
        }
    }

    var path: String {
        switch self {
        case .CreateUser:
            return "/users"
        case .ReadUser(let username):
            return "/users/\(username)"
        case .UpdateUser(let username, _):
            return "/users/\(username)"
        case .DestroyUser(let username):
            return "/users/\(username)"
        }
    }
}

To get the method of any of the different endpoints, you can call the same method without having to pass in any parameters to define what type of endpoint you are looking for, it's already handled by the case you select.

let createUserMethod = Router.CreateUser.method
let updateUserMethod = Router.UpdateUser.method

Or if you want to get the path, same types of calls.

let updateUserPath = Router.UpdateUser.path
let destroyUserPath = Router.DestroyUser.path

Now let's try the same approach using static methods.

struct Router: URLRequestConvertible {
    static let baseURLString = "http://example.com"

    static var method: Method {
        // how do I pick which endpoint?
    }

    static func methodForEndpoint(endpoint: String) -> Method {
        // but then I have to pass in the endpoint each time
        // what if I use the wrong key?
        // possible solution...use an Obj-C style enum without functions?
        // best solution, merge both concepts and bingo, Swift enums emerge
    }

    static var path: String {
        // bummer...I have the same problem in this method too.
    }

    static func pathForEndpoint(endpoint: String) -> String {
        // I guess I could pass the endpoint key again?
    }

    static var pathForCreateUser: String {
        // I've got it, let's just create individual properties for each type
        return "/create/user/path"
    }

    static var pathForUpdateUser: String {
        // this is going to get really repetitive for each case for each method
        return "/update/user/path"
    }

    // This approach gets sloppy pretty quickly
}

NOTE: If you don't have many properties or functions that switch on the cases, then an enum doesn't present many advantages over a struct. It is simply an alternative approach with different syntactic sugar.

Enums can maximize state and code reuse. The associated values also allow you to do some really powerful things like grouping objects that are somewhat similar, but have incredibly different requirements...such as NSURLRequest creation.

What is the right way to construct parameters for enum cases to improve readability? (had to mash this one together)

That's a terrific question. You've already laid out two possible options. Let me add a third that may suit your needs a bit better.

case CreateUser(username: String, firstName: String, lastName: String, email: String)
case ReadUser(username: String)
case UpdateUser(username: String, firstName: String, lastName: String, email: String)
case DestroyUser(username: String)

In cases where you have associated values, I think it can be helpful to add explicit names for all the values in the tuple. This really helps build the context. The downside is that you then have to redeclare those values in your switch statements like so.

static var method: String {
    switch self {
    case let CreateUser(username: username, firstName: firstName, lastName: lastName, email: email):
        return "POST"
    default:
        return "GET"
    }
}

While this gives you a nice, consistent context, it gets pretty verbose. Those are your three options at the moment in Swift, which one is the correct one to use depends on your use case.


Update

With the release of Alamofire 4.0 , the URLRequestConvertible can now be MUCH smarter and can also throw. We've added full support into Alamofire for handling invalid requests and generating sensible errors through the response handlers. This new system is documented in detail in our README.

Owen Pierce
  • 753
  • 2
  • 10
  • 25
cnoon
  • 16,575
  • 7
  • 58
  • 66
  • 8
    Thanks. Just one question regarding your answer about one router vs building router per endpoint (e.g. CRUD example from Alamofire page). Don't you think that if I have say 5 endpoints, each has 3-4 methods, - that's 15-20 `case` statements. That looks like a huge method for me. I'm not sure if that will lead to the readable code... – OgreSwamp Feb 06 '15 at 10:28
  • 1
    Regarding the second answer (enum vs static methods) - the hole point for me here is to hide an implementation inside enum/class. I don't need to know methods or paths outside it. I want to call `Router.createUser("test@mail.com", "....")` and have a block for interpreting Results for server. All details (methods, paths, API root etc) can be private for Router - that's fine. – OgreSwamp Feb 06 '15 at 12:18
  • The issue I see here (and reason of this question) is that I have not just `/users` endpoint with 4 methods. I have other endpoints, like `/tickets`, `/venues` etc. I have about 4 of them right now and potentially even more. So instead of nice `switch` statement I will have a `switch` with 20 cases. I'm not sure if it is good... It's kinda *code smell* for me. – OgreSwamp Feb 06 '15 at 12:22
  • 1
    All perfectly valid points. We're all in the same boat asking the exact same questions. To your first comment, you'd only have a single switch in each function. Also, some of the functions may return the same values so you wouldn't have a switch/case set. It totally depends on the situation. To your second, if that's all you're going to use the Enum for, then I'd say a struct or enum would work pretty much the same. Really no advantages over one or the other. You'd either have multiple cases or multiple functions. – cnoon Feb 06 '15 at 16:48
  • 2
    To your last comment, I don't think you'd want to stuff 20 different endpoints into a single enum if you also had a bunch of functions. Your switch statements would be so long it wouldn't be very readable. Definitely code smell at that point. To me, once you get over 5 or 6 cases in your switches, you really start to lose readability. – cnoon Feb 06 '15 at 16:50
  • 1
    I added a NOTE in the struct / enum comparison section of the answer detailing the case where you don't have many properties or methods. – cnoon Feb 06 '15 at 16:55
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/70411/discussion-between-ogreswamp-and-cnoon). – OgreSwamp Feb 06 '15 at 17:37
  • 2
    To your last comment @cnoon, (I read the previous comments) you're saying that (using your example of CRUD user router), if I have some requests that belongs to different contexts, like Request posts from twitter and User CRUD, those will be two separated routers? – Renan Kosicki May 18 '15 at 20:10
  • 3
    Yes that's correct @RenanKosicki. You certainly reach a point of no return when you have too many cases in a Router enum. Splitting them out into logical groups is certainly a more desirable design. – cnoon May 19 '15 at 04:33
  • I wish I could add this answer to the favorites! – Rohan Sanap Feb 22 '16 at 10:56
  • @cnoon Regarding the design where we have multiple Routers for different contexts, how do I reuse the same `baseURL` and `authorizationHeader` for all the routers? – Luong Huy Duc May 17 '16 at 07:07
  • @LuongHuyDuc Wondering the exact same thing. – George Marmaridis May 23 '16 at 16:02
  • What about cases in which you'd want to inject data (base url, or token) to said router? You can't do that with an enum. Which approach would be best here? – Christopher Francisco Jun 08 '16 at 16:56
10

Here is the up to date enum Router in Swift 3, which is recommend on Alamofire's Github. I hope you find it useful in terms of how to properly implement a Router with URLRequestConvertible.

import Alamofire

enum Router: URLRequestConvertible
{
    case createUser(parameters: Parameters)
    case readUser(username: String)
    case updateUser(username: String, parameters: Parameters)
    case destroyUser(username: String)

    static let baseURLString = "https://example.com"

    var method: HTTPMethod
    {
        switch self {
        case .createUser:
            return .post
        case .readUser:
            return .get
        case .updateUser:
            return .put
        case .destroyUser:
            return .delete
        }
     }

    var path: String
    {
        switch self {
        case .createUser:
            return "/users"
        case .readUser(let username):
            return "/users/\(username)"
        case .updateUser(let username, _):
            return "/users/\(username)"
        case .destroyUser(let username):
            return "/users/\(username)"
        }
    }

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest
    {
        let url = try Router.baseURLString.asURL()

        var urlRequest = URLRequest(url: url.appendingPathComponent(path))
        urlRequest.httpMethod = method.rawValue

        switch self {
        case .createUser(let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        case .updateUser(_, let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        default:
            break
        }

        return urlRequest
    }
}
Andi
  • 8,154
  • 3
  • 30
  • 34
7

Why don't you try to use SweetRouter. It will help you to remove all the boilerplate that you have when declaring a Router and it also supports such things as multiple environments and your code will be really readible.

Here's an example of the Router with sweet router:

struct Api: EndpointType {
    enum Environment: EnvironmentType {
        case localhost
        case test
        case production

        var value: URL.Environment {
            switch self {
            case .localhost: return .localhost(8080)
            case .test: return .init(IP(126, 251, 20, 32))
            case .production: return .init(.https, "myproductionserver.com", 3000)
            }
        }
    }

    enum Route: RouteType {
        case auth, me
        case posts(for: Date)

        var route: URL.Route {
            switch self {
            case .me: return .init(at: "me")
            case .auth: return .init(at: "auth")
            case let .posts(for: date):
                return URL.Route(at: "posts").query(("date", date), ("userId", "someId"))
            }
        }
    }

    static let current: Environment = .localhost
}

And here's how you would use it:

Alamofire.request(Router<Api>(at: .me))
Alamofire.request(Router<Api>(.test, at: .auth))
Alamofire.request(Router<Api>(.production, at: .posts(for: Date())))
Noobass
  • 1,974
  • 24
  • 26
5

I found a way to work with it, I created a Class with the Router in it: inherit classes from a request

file request.swift

class request{

    func login(user: String, password: String){
        /*use Router.login(params)*/
    }
    /*...*/
    enum Router: URLRequestConvertible {
        static let baseURLString = "http://example.com"
        static let OAuthToken: String?

        case Login([String: AnyObject])
        /*...*/

        var method: Alamofire.Method {
            switch self {
            case .Login:
                return .POST
            /*...*/
        }

        var path: String {
            switch self {
            case .Login:
                return "/login"
            /*...*/
            }
        }

        var URLRequest: NSURLRequest {
            switch self {
                case .Login(let parameters):
                    return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
                /*...*/
                default:
                    return mutableURLRequest
            }
        }
    }
}

file requestContacts.swift

class requestContacts: api{

    func getUser(id: String){
        /*use Router.getUser(id)*/
    }
    /*...*/

    enum Router: URLRequestConvertible {

        case getUser(id: String)
        case setUser([String: AnyObject])

        var method: Alamofire.Method {
            switch self {
                case .getUser:
                    return .GET
                case .setUser:
                    return .POST
                /*...*/
            }
        }

        var path: String {
            switch self {
            case .getUser(id: String):
                return "/user\(id)/"
            case .setUser(id: String):
                return "/user/"
            /*...*/
            }
        }
        // MARK: URLRequestConvertible

        var URLRequest: NSURLRequest {
            //use same baseURLString seted before
            let URL = NSURL(string: Router.baseURLString)!
                let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
                mutableURLRequest.HTTPMethod = method.rawValue

            if let token = Router.OAuthToken {
                mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            }
            switch self {
                /*...*/
                case .setUser(let parameters):
                    return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
                default: //for GET methods, that doesent need more
                    return mutableURLRequest
            }
        }
    }
}

so the son class will get paramethers of Router from the parent, and you can even use Route.login in any son. still, dont know if there is a way to get a short URLRequest, so i dont need to set parameters again and again

  • Hi, I'm trying to do like you said in your answer but when I try to use POST method I still get the GET method response. For example: When I'm accessing my url "/users" instead of creating the user using the POST method, I'm getting the list of the all users, which is the GET method response. Any idea why this is happening? Seems like even setting the method with `mutableURLRequest.HTTPMethod = method.rawValue` nothing changes. – Renato Parreira Jan 05 '16 at 19:39
  • what enum did you access?? you have to select the enum for a POST, and set a POST for that enum value, here the post is Router.setUser(...) – David Alejandro Londoño Mejía Jan 05 '16 at 23:09
  • Can you check my question here in SO? There I provide all the details. Here is the link: [Question](http://stackoverflow.com/questions/34620515/post-requests-in-alamofire-parameter-encoding-returning-get-responses/34620766?noredirect=1#comment56990280_34620766) – Renato Parreira Jan 05 '16 at 23:48
5

Types adopting the URLRequestConvertible protocol can be used to construct URL requests.

Here is an example taken from www.raywenderlich.com

public enum ImaggaRouter : URLRequestConvertible{

  static let baseURL = "http://api.imagga.com/v1"
  static let authenticationToken = "XAFDSADGDFSG DAFGDSFGL"

  case Content, Tags(String), Colors(String)

  public var URLRequest: NSMutableURLRequest {
    let result: (path: String, method: Alamofire.Method, parameters: [String: AnyObject]) = {
      switch self {
      case .Content:
        return ("/content", .POST, [String: AnyObject]())
      case .Tags(let contentID):
        let params = [ "content" : contentID ]
        return ("/tagging", .GET, params)
      case .Colors(let contentID):
        let params = [ "content" : contentID, "extract_object_colors" : NSNumber(int: 0) ]
        return ("/colors", .GET, params)
      }
    }()

    let URL = NSURL(string: ImaggaRouter.baseURL)!
    let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
    URLRequest.HTTPMethod = result.method.rawValue
    URLRequest.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization")
    URLRequest.timeoutInterval = NSTimeInterval(10 * 1000)

    let encoding = Alamofire.ParameterEncoding.URL
    return encoding.encode(URLRequest, parameters: result.parameters).0
  }
}

and we can use this ImmageRouter as the following:

Alamofire.request(ImaggaRouter.Tags(contentID))
      .responseJSON{ response in
Andi
  • 8,154
  • 3
  • 30
  • 34
Atef
  • 2,872
  • 1
  • 36
  • 32
0

instead of case UpdateUser(username: String, firstName: String, lastName: String, email: String)

you'd make

struct UserAttributes
{
    let username: String
     ....
}

and feed THAT model object as a parameter instead of a cluster of unnamed unreadable strings

case UpdateUser(parameters: UserAttributes)

Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66