Named Routing Parameters
This is a request for feedback on an intended change to how routing works in Vapor. If feedback is positive, this change will likely land in Vapor 4.0.
Summary
Currently, in Vapor 3, route parameters are accessed by calling req.parameters.next(). Each call to next() removes and returns a single route parameter from the internal storage. Parameters are accessed in the order that they appear in the URL string.
/posts/42/comments/1337
r.get("posts", Int.parameter, "comments", Int.parameter) { req in
let postID = try req.parameters.next(Int.self)
let commentID = try req.parameters.next(Int.self)
return "Post #\(postID) Comment #(commentID)"
}
Current Problem
Because parameters can only be accessed in the order they appear and parameter storage is mutated upon access, it is difficult to access arbitrary parameters without fetching all of them. It's also difficult to access parameters from different layers of an application, such as from both middleware and route closures. In these situations, parameter access must be carefully ordered to avoid unintended side effects.
The current API also does not make clear that registering multiple dynamic parameter types at a single point is not supported. For example, it is a common error to see routes registered like:
r.get("users", Int.parameter) { ... }
r.get("users", Double.parameter) { ... }
While one might expect both of these routes to be available, in reality only the Double route will be accessible since it was registered last.
Proposed Solution
In order to simplify parameter access, a system for naming route parameters is proposed. Each dynamic parameter in a given path will be given a unique name. The unique name can be used to later fetch the parameter from the request without mutating storage.
Here is an example of the proposed API:
/posts/42/comments/1337
r.get("posts", .postID, "comments", .commentID) { req in
let commentID = try req.parameters.get(.commentID)
let postID = try req.parameters.get(.postID)
return "Post #\(postID) Comment #(commentID)"
}
extension PathComponent {
static var postID: PathComponent {
return .dynamic(name: "postID")
}
static var commentID: PathComponent {
return .dynamic(name: "commentID")
}
}
Note that the parameters can be accessed in any order.
By default, req.parameters.get(_:) will return a String. If a different type is desired, the get(_:as:) method can be used instead:
try req.parameters.get(.postID, as: Int.self)
The get(_:as:) method will also include a default value, allowing for compiler inference.
return try Post.find(req.parameters.get(.postID), on: self.db).first()
Stringly typed API
In addition to the strongly typed static extension to PathComponent, it could be possible to register dynamic path components by String using the prefix :.
/posts/42/comments/1337
r.get("posts", ":postID", "comments", ":commentID") { req in
let postID = try req.parameters.get(":postID")
let commentID = try req.parameters.get(":commentID")
return "Post #\(postID) Comment #(commentID)"
}
While this API is slightly more prone to typos since the unique name must be written twice, it is less verbose than needing to add extensions. Inclusion of this stringly-typed addition to the API is dependent on feedback.
Detailed Design
PathComponent will be updated to the following:
enum PathComponent {
case part(String)
case dynamic(name: String)
case anything
case catchall
}
All router methods (i.e., r.get(...), r.post(...), etc.) will accept PathComponent....
ExpressibleByStringLiteral conformance will allow for paths to be registered concisely as strings.
extension PathComponent: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
if value.hasPrefix(":") {
self = .dynamic(name: value[1...])
} else if value == ":" {
self = .anything
} else if value == "*" {
self = .catchall
} else {
self = .part(value)
}
}
}
For example:
// normal route
r.get("users", "me") { ... }
// any parameter (discarded parameter)
r.get("users", ":", "posts")
// catchall
r.get("users", "*") { ... }
Deprecation of .next()
req.parameters.next() and Foo.parameter will continue to work after this change. However, I propose that the API should be deprecated in order to reduce confusion over existence of two separate methods for accessing parameters.