Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better hints for mismatching routes (i.e. due to format=) #1202

Open
gilescope opened this issue Jan 15, 2020 · 6 comments
Open

Better hints for mismatching routes (i.e. due to format=) #1202

gilescope opened this issue Jan 15, 2020 · 6 comments
Labels
accepted An accepted request or suggestion request Request for new functionality
Milestone

Comments

@gilescope
Copy link

The feedback and compile time errors in Rocket are excellent - they've saved me lots of time already.

I did loose a chunk of time wondering why the url wasn't matching when I was trying to invoke it and it was because the ContentType was different.

It would be most excellent if rocket in development mode could say the closest matching url was to the console and why it failed to match.

It's already a pleasure to work with, but that would make rocket extremely easy to use.

@jebrosen
Copy link
Collaborator

We log "Outcome: Forward" before trying the next route if a request guard returns Outcome::Forward. I like the idea of adding similar logging for routes that mismatch for reasons other than path - format in particular is an easy one to forget about or miss.

@jebrosen jebrosen added accepted An accepted request or suggestion request Request for new functionality labels Jan 15, 2020
@jebrosen jebrosen changed the title Hints in development mode Better hints for mismatching routes (i.e. due to format=) Jan 15, 2020
@gilescope
Copy link
Author

Another really easy class of error to detect would be to warn about unused query parameters passed to the url. That could be a sign that we've mistyped an Option parameter...
I appreciate if there are <path...> statements then this can't be done, but if there aren't it would pick up quite a few simple errors.

@SergioBenitez
Copy link
Member

It would be most excellent if rocket in development mode could say the closest matching url was to the console and why it failed to match.

This is an excellent idea! I'd absolutely love a PR in this direction. I'm happy to mentor as well.

Another really easy class of error to detect would be to warn about unused query parameters passed to the url. That could be a sign that we've mistyped an Option parameter...

This may prove to be overly verbose. I wonder if this can be a fairing? Perhaps Rocket would need to expose some extra functionality to make such a fairing possible? That'd be fantastic. Better yet, if the above could also be a fairing, that would be great! We could have a helmet style fairing for extra debugging help.

@SergioBenitez SergioBenitez added this to the 0.6.0 milestone Jun 5, 2020
@LittleEntity
Copy link

LittleEntity commented Jan 29, 2021

Hello @SergioBenitez, hello @jebrosen and hello rocket community <(^_^)>

Please have a look at my prototype to solve this issue. It is built around a Matches enum which can either be a Success or a Fail. If a Route does match a Request, the result of route.matches(request) will be a Success and a Fail otherwise.

A Fail describes itself by giving a Reason. A Reason is another Enum. A Reason describes itself by its kind. A Reason can give additional information based on its kind.

Take for example Route "/topic/a".
Matching it against Request "/topic/b" will return a Fail(Reason::SegmentDoesNotMatch(1, "a", "b")). The kind of Reason SegmentDoesNotMatch describes the Fail. The Reason SegmentDoesNotMatch contains the tuple (1, "a", "b"). With the tuple the SegmentDoesNotMatch says 'route segment "a" does not match request segment "b" at segment index 1'.

Matches::Fail(Reason::NumOfRouteSegsGreater(CmpSegLen)) contains a struct CmpSegLen instead of a tuple. This is an experiment to evaluate the advantages and disadvantages of more elaborate code in this context. It would probably be possible to not add any additional information and recalculate it later, if so desired. Though that approach comes also with its own disadvantages. I found these advantages and disadvantages for Matches:Fail(Reason::Kind(Info)) implementations:

  • Info contains a struct with additional information
    • (+) elaborate description of the Reason
    • (-) more structs
    • (-) may lead to longer code lines for the user
    • (-) Runtime performance may be affected by allocating a few bytes
    • (+) the user can express the intent of his code by using the given struct fields
    • (+) precise reporting of Fail Reason possible
  • Info contains a tuple
    • (-) tuple fields don't describe themselves
    • (+) less library code than with structs
    • (-) runtime performance may be affected by allocating a few bytes
    • (-) the user can be irritated by similar values inside a tuple Info
    • (+) precise reporting of Fail Reason possible
  • without Info
    • (+) maybe less affected runtime performance, because we don't allocate any additional bytes
    • (-) precise reporting of Fail Reason maybe possible, but requires extensive code for recalculating the Info

I prefer the Info to be a struct. The ease of use and self explanatory style of code outweighs the disadvantages in my opinion.

I tried to make the prototype expressive in terms of a proof of concept. For that reason a Route and a Request are very simple structs, just enough to show the gist of it. The Reason enum does not yet cover reasons for a query mismatch or a format mismatch. After reading the code I hope you can imagine roughly how those could be implemented too.

I hope it meets your requirements for a concept and if it does not, please let me know. I will try to elaborate the prototype further to relieve your concerns if you desire.

I haven't doven into implementing fairings yet. So I cannot estimate the effort to implement the proposed idea as a fairing, nor can I estimate if it would require extensive refactoring.
Edit: see the following post for my approach on implementing this with a Fairing

Deriving the "closest match" from a set of Fails should be doable with a proper definition of "closest match" with the Info and Reason given for each Fail. We could even analyse strings to hint a fix in some cases.

I would be glad to implement this for rocket. To get started I would need a little help setting up the development environment. In return I would write a small guide to set up the development environment for newbie rocket lib devs like myself. What do you say? May I take this issue with your guidance?

A note on performance:
Matches::Fail(Reason::Kind(Info{ field, .. })) might be a hell of a type definition, but it shouldn't take much space in memory. To further relieve concerns, we could implement production code which serves the type Matches::Fail without any additions. Then the runtime performance should be equal to using bool instead of enum Matches{Success, Fail}.

A note on the used lifetime markers:
Because Reason::SegmentDoesNotMatch carries a reference to the original path segment str, the lifetime of a Reason is bound to the original segment str. Because Matches::Fail contains a Reason, Matches lifetime is also bound to the original segment str.

Particular sources of rocket I examined before I wrote the prototype:
https://github.com/SergioBenitez/Rocket/tree/master/core/lib/src/router

  • mod.rs especially Router::route(..)
  • collider.rs especially paths_match(..)
  • route.rs
fn main() {
    let router = Router {
        routes: vec![
            Route { path: vec![] },
            Route {
                path: vec!["guide"],
            },
            Route {
                path: vec!["guide", "*"],
            },
        ],
    };

    router.route(&Request {
        path: vec!["guide"],
    });
    router.route(&Request { path: vec![] });
    router.route(&Request {
        path: vec!["not_found"],
    });
    router.route(&Request {
        path: vec!["guide", "stuff", "a thing"],
    });
}

enum Matches<'s> {
    Success,
    Fail(Reason<'s>),
}

#[derive(Debug)]
enum Reason<'s> {
    NumOfRouteSegsGreater(CmpSegLen),
    NumOfRouteSegsLesser(CmpSegLen),
    SegmentDoesNotMatch(usize, &'s str, &'s str),
    // fail reasons for query mismatch
    // fail reasons for format mismatch
}

#[derive(Debug)]
struct CmpSegLen {
    route_segs_len: usize,
    req_segs_len: usize,
}

struct Router {
    routes: Vec<Route>,
}

impl Router {
    fn route(&self, request: &Request) {
        println!("try to route {:?}", request.path);
        for route in &self.routes {
            match route.matches(request) {
                Matches::Success => {
                    println!("Matched route {:?}", route.path);
                    println!("routing SUCCEEDED 🚀");
                    println!();
                    return;
                }
                Matches::Fail(reason) => println!(
                    "route {:?} did not match, because: {:?}",
                    route.path, reason
                ),
            }
        }
        println!("routing FAILED 😱");
        println!();
    }
}

struct Route {
    path: Vec<&'static str>,
}

struct Request {
    path: Vec<&'static str>,
}

impl Route {
    fn matches<'s>(&'s self, request: &'s Request) -> Matches<'s> {
        let route_segs = &self.path;
        let req_segs = &request.path;

        use Matches::*;
        if route_segs.len() > req_segs.len() {
            return Fail(Reason::NumOfRouteSegsGreater(CmpSegLen {
                route_segs_len: route_segs.len(),
                req_segs_len: req_segs.len(),
            }));
        }

        for (i, (route_seg, req_seg)) in route_segs.iter().zip(req_segs).enumerate() {
            if *route_seg == "*" {
                return Success;
            } else if route_seg != req_seg {
                return Fail(Reason::SegmentDoesNotMatch(i, route_seg, req_seg));
            }
        }

        if route_segs.len() < req_segs.len() {
            return Fail(Reason::NumOfRouteSegsLesser(CmpSegLen {
                route_segs_len: route_segs.len(),
                req_segs_len: req_segs.len(),
            }));
        }

        // !(route_segs.len() < req_segs.len()) && !(route_segs.len() > req_segs.len())
        // == (route_segs.len() == req_segs.len())
        // !route_segs.iter().zip(req.segs).any(|(route_seg, req_seg)| route_seg != req_seg)
        // == route_segs.iter().zip(req.segs).all(|(route_seg, req_seg)| route_seg == req_seg)
        // I hope this is somewhat understandable. In code I would elaborate further why this is a Success.
        Success
    }
}

@LittleEntity
Copy link

Hello everyone,

I tinkered with with Fairings and have found a way to implement this feature with a Fairing. My current approach would be to mimic the routing process of rocket and logging the process in data structures like the ones in the previous post. The logged data could be analyzed further if no match would be found.

There are drawbacks to this approach:

  • If routing would differ in the future, the code for the RouteHinter must be mimicked again.
  • There is no simple automatic way to proof the mimic of RouteHinter is correct. The RouteHinter must not log a mismatch, when rocket::Router doesn't and vice versa.
  • Testing mimicked code against rockets routing code adds additional complexity to tests.
  • The current implementation copies the Routes to shared memory via rocket's State mechanism. If the user dynamically adds Routes after launch(), they are not automatically considered in this Fairing. The user can add them to the Fairing manually though.
#![feature(proc_macro_hygiene, decl_macro)]

use rocket::fairing::{Fairing, Info, Kind};
use rocket::{get, routes, Data, Request, Rocket, State};
use std::sync::RwLock;

fn main() {
    rocket::ignite()
        .mount("/", routes!(get_hello, get_index, get_guide))
        .attach(RouteHinter {})
        .launch();
}

#[get("/hello")]
fn get_hello() -> &'static str {
    "Hello World!"
}

#[get("/")]
fn get_index() -> &'static str {
    "Welcome Visitor!"
}

#[get("/guide/*")]
fn get_guide() -> &'static str {
    "Welcome to the Guide Rustacian!"
}

struct RouteHinter {}

#[derive(Debug)]
struct RouteBox {
    routes: RwLock<Vec<rocket::Route>>,
}

impl Fairing for RouteHinter {
    fn info(&self) -> Info {
        Info {
            name: "Route Hinter",
            kind: Kind::Attach | Kind::Launch | Kind::Request,
        }
    }

    fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
        Ok(rocket.manage(RouteBox {
            routes: RwLock::new(Vec::new()),
        }))
    }

    fn on_launch(&self, rocket: &Rocket) {
        let route_box = rocket
            .state::<RouteBox>()
            .expect("RouteBox initialized in on_attach");
        let mut hint_routes = route_box.routes.write().unwrap();
        for route in rocket.routes() {
            hint_routes.push(route.clone());
        }
    }

    fn on_request(&self, request: &mut Request, _: &Data) {
        let route_box = request
            .guard::<State<RouteBox>>()
            .expect("RouteBox initialized in on_attach");
        let routes = route_box.routes.read().unwrap();
        println!(
            "can check the following routes against request '{}':",
            request.uri()
        );
        for route in routes.iter() {
            println!("- {}", route.uri);
        }
    }
}

@LittleEntity
Copy link

LittleEntity commented Feb 9, 2021

Here is the fork with the progress so far:
https://github.com/LittleEntity/Rocket/tree/1202_routehint

Here is an example you may run and experiment with:
https://github.com/LittleEntity/Rocket/tree/1202_routehint/examples/routehint

So far the code is unpolished and not documented enough. The print for the format parameter match is only partially implemented yet.

Collaboration on this is much appreciated. I am new to contributing software to open source projects. If there is anything I can do better, please let me know. Especially if you prefer a better solution for collaboration than the fork I made, let me know.

If you have any questions, please contact me here or over at our matrix channel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted An accepted request or suggestion request Request for new functionality
Projects
Status: Ready
Development

No branches or pull requests

4 participants