Rocket
Rocket copied to clipboard
[feature request] Allow dynamic path segments to be delimited by characters other than '/'
For example, I want to create a route like:
#[get("/<name>.json")]
pub fn name_as_json() { /* ... */ }
Currently this fails to compile with:
error: malformed parameter
--> src/web/mod.rs:3:9
|
3 | #[get("/<name>.json")]
| ^^^^^^^^^^^
|
= help: parameters must be of the form '<param>'
1. Why you believe this feature is necessary.
Using a pseudo-extension to manage response content type is a common convention in web frameworks (e.g., Rails). For some kinds of requests, the name will be a dynamic path segment, which creates the scenario above.
There are also cases where one might wish to split dynamic path segments by a dash, e.g.:
#[get("/models/<id>-<slug>")]
or even by random characters:
#[get("/api/v<api_version>/some/request")]
2. A convincing use-case for this feature.
See 1.
3. Why this feature can't or shouldn't exist outside of Rocket.
This behavior would need to be handled at the codegen stage.
All of these cases can be easily implemented via custom FromParam implementations. With const generics, we could even have generic implementations. For instance, you could have an Ext<T: FromParam, const &str> type that implements FromParam by removing the extensions in its const parameter from the incoming request parameter and then converting the rest into T via its FromParam implementation. You'd then use it like:
#[get("/<name>")]
fn item(name: Ext<&RawStr, ".json">) { ... }
There are downsides to this approach. First, it complicates the type signature, though it could also be argued that it simplifies the route syntax while making the requirements more obvious. Second, and more importantly, it means that Rocket will see the following two routes as colliding, even though they aren't:
#[get("/<name>")]
fn first(name: Ext<&RawStr, ".json">) { ... }
#[get("/<name>")]
fn second(name: Ext<&RawStr, ".xml">) { ... }
This would mean that you'd need to manually rank at least one of the two routes which is suboptimal given that Rocket could have known about the non-collision. Of course, we could teach Rocket to assume that different const arguments in types indicates a non-collision, but that could be a fragile, and even incorrect change.
The decision to disallow compound dynamic parameters was very intentional, and in particular, was made conservatively. The internal routing mechanism actually supports all of these cases, but it is not exposed to the high-level interface for various reasons. I'm not convinced one way or another. I'm inclined to leave things as they are, prompting for the creation of custom FromParam implementations when needed, and ultimately having a collection of generic implementations using const generics in Rocket itself. If we decide to support this in the future, the change can be made in a fully backwards-compatible fashion.
By the way, you should be using content negotiation instead of extensions in a path to negotiate content. Rocket makes this easy via the format route attribute parameter:
#[get("/name", format = "application/json")]
fn item(name: String) { .. }
@SergioBenitez thanks for the reply and workaround.
To be perfectly clear, I am not requesting compound dynamic parameters in the sense of e.g.:
#[get("/<name><something_else>")]
where the parser would have to dynamically determine the bounds of each capture segment. Dynamic segments as I've requested would still have to be delimited by a literal char or string (or end of string).
Can you explain a little more the reasoning behind disallowing dynamic segments as identified above? It seems to me that wanting to capture parts of a URL path is related to but separate from the concept of "path segments". It seems an artificial limitation to require that the parts captured be delimited by '/'.
Thanks!
+1 for this. I'm going to move my web application from php into rocket and it's not possible now. I already have routes like: /<id>-<slug> or even: /<type>/filter:<fitler1>,<filter2>. I can't change this because site is already indexed by search systems and there is a lot of traffic.
@max-frai Just so it's clear: any URL scheme is possible with Rocket, as I illustrated in my first comment. The question is: which should Rocket implement support for out-of-the-box?
I've also run up against this issue when implementing a server for an api that uses <name>.json style paths.
While this url style can be made to work using FromParam with a custom type it seems at odds with the framework's focus on ease-of-use and expressibility to require users to write this boilerplate which will presumably be separately reimplemented by many users of the library.
As this case is already supported by the internal routing mechanism it seems odd that it is not supported in route decorators. I think only modifications to parser::uri::valid_segments in the codegen module are required and I'd be happy to make them.
This is something that I just hit when trying to build something that matches an existing implementation. I'll use the type signature workaround described, but wanted to register that I'd like to see this fixed.
(For the record, I just want to have ".xml" at the end of my URL; I don't believe I would need anything more than "." being considered a path separator for that.)
Ugh, actually, I just realised that the example given involves writing a whole bunch of type boilerplate just to be able to append ".xml" to a pattern. (I had assumed that the Ext described was provided by rocket for such cases.)
So I guess I'll just handle this inside the handler function; that seems substantially less painful.
Becaus &'str was not available for use in const generic arguments, I used a macro to build multiple structs.
macro_rules! generate_fromparam_ext {
($struct_name: ident, $ext: expr) => {
struct $struct_name<'a> (&'a str);
impl<'a> FromParam<'a> for $struct_name<'a> {
type Error = ();
fn from_param(param: &'a str) -> Result<Self, Self::Error> {
match param.strip_suffix($ext) {
None => Err(()),
Some(x) => FromParam::from_param(x).map(|x| $struct_name(x)).map_err(|_| ()),
}
}
}
};
}
generate_fromparam_ext!(NarinfoName, ".narinfo");