Skip to content

Mimic Rails more opinionated router #444

Closed
jaredonline opened this Issue Dec 22, 2013 · 16 comments

5 participants

@jaredonline

Hey there,

I'm really new to D as a language, but am very interested in it. I've been a Ruby developer for awhile and use Rails for all my web based applications. I've been tinkering the past few weeks on how to setup a router that mimics Rails routing.

auto router = new ActionRouter;
router.resources("books");

The above code makes a lot of assumptions. It assumes you have a class called BooksController with methods void index(), void show(), void init(), void create(), void update(), void delete() and that they route (respectively) to

GET /books
GET /books/:id
GET /books/new
POST /books
PUT /books/:id
DELETE /books/:id

You can see the router I created here (I stole a ton of the source from vibe.d):

https://github.com/jaredonline/action-pack/blob/master/source/action_dispatch/router.d

You can see how the controller piece works here:

https://github.com/jaredonline/action-pack/blob/master/source/action_dispatch/controller.d

I know I've probably made a ton of mistakes in my implementation, but the idea is there and works. Let me know what you think!

@etcimon
etcimon commented Dec 22, 2013

You solved the dynamic classes problem in a very elegant way, it was addressed in vibe.web.web if you look at it and it seems like both solutions are very good for their own purpose. Making request/response available is certainly the biggest upside of this approach, while vibe.web is great about being able to customize the method and have the ability to match parameters through function parameters. I'd certainly choose this for quickly developing a collection-based service, and it gives me plenty of ideas too. Nicely done

@jaredonline

I can't take all of the credit for the dynamic classes problem... I got a ton of help on StackOverflow (http://stackoverflow.com/questions/20451131/use-object-factory-without-needing-to-cast-to-a-specific-type).

I actually didn't even look in vibe.web or vibe.web.web... I was basing what I was doing off the basic documentation on the Vibe homepage and my knowledge of Rails internals.

I updated my ActionRouter class to be able to nest resources, so you can now do

router.resources("bloggers", delegate void (ActionNamespace bloggers) {
    bloggers.resources("blogs", delegate void (ActionNamespace blogs) {
      blogs.resources("comments", delegate void(ActionNamespace comments) {
        comments.resources("responses");
      });
    });
  });

And get auto generated routes like:

GET /bloggers/:blogger_id/blogs/:blog_id/comments/:comment_id/responses

EDIT: I'm not really a fan of the interface for the way I did the nested routes, but it works.

Should I do some work to open a pull request against Vibe for this? Should I just keep working on my own project and let you pull out the pieces you want? Is any of it useful for Vibe?

@etcimon
etcimon commented Dec 23, 2013

With D language you could certainly shorten the router to rewrite as router.bloggers.blogs.comments.responses. In my opinion, vibe.web was also on the way to recursing through every member tagged as @DynamicallyAvailable to register them through the router, even if they were classes. But how are you going to redirect those queries to the database? That's what I am most interested in, it seems like something an ORM could understand as well.

@jaredonline

Working on an ORM seems like a much larger project, but one I want to take on eventually. For now I don't intend to work on that part... I'm still figuring out a lot of the routing and controller work. If I were I would probably mimic ActiveRecord from Rails as close as I could.

I still have a lot of work on this to do... I want to be able to render templates more easily; have different extensions (.html, .json, .xml) configurable for automatic rendering; have better options and the ability to define custom routes on resources

@etcimon
etcimon commented Dec 23, 2013

You certainly seem like a leader in your field and I think that would be some great improvements. You have a pretty good benchmark here showing this vibe.d is faster than the big C, Erlang, Go, C++, etc. https://atilanevesoncode.wordpress.com/2013/12/05/go-vs-d-vs-erlang-vs-c-in-real-life-mqtt-broker-implementation-shootout/

Anything that can convince you of keeping on working on your project is good for me, you and for vibe, but very bad for rails. So, keep up the good work, I for one will positively support all of it and willfully test it all because that's what I love to do.

@rikkimax

Hmm, this has a lot of assumptions of routing (which are good ones). With regards to e.g. index, show, init ext.
How would you feel about UDA's?
My current approach to routing is as follows:

Have a package file which basically imports all files and inits route classes. This enables version support and disabling whole parts of the site.
These routes are basically as follows:

class Main : Routes {
    this(){super();}

   @RouteFunction(RouteType.Post, "/login")    
    void login(HTTPServerRequest req, HTTPServerResponse res) {
    }

    @RouteGroup(&isAuthed) {
        @RouteFunction(RouteType.Get, "/home")
        void home(HTTPServerRequest req, HTTPServerResponse res) {
        }

       @RouteFunction(RouteType.Post, "/logout")
        void logout(HTTPServerRequest req, HTTPServerResponse res) {
            res.terminateSession();
            res.redirect("/");
        }
    }
}

A route group also supports giving a name to a group.

    @RouteGroup(&isAuthed, "/authed") {
        @RouteFunction(RouteType.Get, "/home")
        void home(HTTPServerRequest req, HTTPServerResponse res) {
        }

       @RouteFunction(RouteType.Post, "/logout")
        void logout(HTTPServerRequest req, HTTPServerResponse res) {
            res.terminateSession();
            res.redirect("/");
        }
    }

You can add params if you wish. RouteGroup's also stack so you can have 5 (atleast in theory) to do /blogs/:blogid/pages/:pageid/:id

Would associating method names by default to the given route work?

Although my design is OOP and probably not what you're intending for, I hope it might gives some ideas.

@s-ludwig
rejectedsoftware member

Just for comparison, this is how it would currently look like using vibe.web.web:

class Main {
    // path and method are inferred from the name. req/res parameters are optional or taken directly from the post form/query string
    void postLogin(string user, string password) { ... }

    @before!authenticate("auth_info") {
        @method(HTTPMethod.GET) @path("/home") // override method/path
        void renderTheHomePage(AuthInfo auth_info) {
            render!("home.dt", auth_info);
        }

        void postLogout(HTTPServerResponse res, AuthInfo auth_info) {
            res.terminateSession();
            redirect("/");
        }
    }
}

To make a custom path prefix, it's currently necessary to put the corresponding methods into a separate class. But the named group feature looks interesting.

The first "books" example can currently be represented as:

@rootPath("/books")
class Books {
    void get() { ... } // GET /books/
    void getNew() { ... } // GET /books/new
    void get(string id) { ... } // GET /books/:id
    void create() { ... } // POST /books/
    void set(string id) { ... } // POST /books/:id
    void erase(string id) { ... } // DELETE /books/:id
}

But adding something especially tailored for such collection-like cases also sounds interesting. Maybe this can be done by using a UDA @collection or similar.

As for the implementation, I don't think dynamic dispatch is necessary or desirable, considering D's meta programming capabilities. But some things like nesting "collections" would be very interesting. Nesting in general will be possible by returning class instances, like in the vibe.web.rest system:

class Blogs {
    void get() {} // GET /blogs/
}

class Main {
    Blogs m_blogs;
    @property blogs() { return m_blogs; }
}

But I'm still unsure how to best combine this with "id". Maybe using structs instead of classes:

struct Comment {
    private string m_bloggerID, m_blogID, m_id;
    this(string blogger_id, string blog_id, string comment_id) { ... }
    void get() { ... } // GET /bloggers/:blogger_id/blogs/:blog_id/comments/:comment_id/
}

struct Blog {
    private string m_bloggerID, m_id;
    this(string id) { m_id = id; }
    void comments(string id) { return Comment(m_bloggerID, m_id, id); } 
}

struct Bloggers {
   private m_id;
   Blog blogs(string id) { return Blog(m_id, id); }
}

class Main {
    Blogger bloggers(string id) { return Blogger(id); }
}

At least this would have near zero overhead in terms of passing the ids around. But maybe there is a nicer way to do this. It also wouldn't work for vibe.web.rest without an additional UDA, because it interferes with the return value based response mechanism.

In general, I think the approach and some ideas of the ActionRouter system are interesting (and largely overlap with the current or planned vibe.web.web), but the best that can be done at this point is to try and fit the ideas into the vibe.web system, because it is supposed to be consistent in how it works to the vibe.web.rest system, which is already in use for quite a while (and of course I also think the __traits based compile time approach is the best choice).

@rikkimax

Hmm, so id's for a route is more important then?
How would this feel:

@RouteGroupId("book") {
  @RouteGroup(&isAuthed) {

    @RouteFunction(RouteType.Get, "/add")
     void add(...) {}

     @RouteFunction(RouteType.Get, "/delete")
     void delete(...){}
  }

 @RouteFunction(RouteType.Get, "book")
  void get(...){}
}

Where a RouteGroupId essentially acts like a RouteGroup except it produces:
/book/:bookid
Instead of:
/book

To me a route managers job is to handle mapping controllers to a request and nothing more. So to me a route controller is a tangible part of a website. Where it may have many data models associated with it.
The benefit for me atleast with my design is that methods names don't matter. The UDA handles what it is linked to.

I'll implement this and the name association as I think it would be an interesting comparison.

@s-ludwig
rejectedsoftware member

The benefit for me atleast with my design is that methods names don't matter. The UDA handles what it is linked to.

This is possible for the vibe.web system as well (using @method and @path), it's just that often those methods happen to have a name that already perfectly describes how the HTTP method and path should look like. It just adds a lot of convenience in those cases. For one, it also works get getter/setter @property methods, which is especially interesting in the REST case.

The @RouteGroupId is also a possible approach, but it would get difficult for nested collections if it's the only way to achieve this. Ultimately I think it produces a cleaner D code when logical "collections" are also implemented as separate classes or structs, that's why I opted for the return value based approach when starting the vibe.web.rest system (which @Dicebot has heavily revamped using new D features since then).

@rikkimax

I can see what you mean.
Heres my example using nested:

class T2 : ActionRouter {
    this() { super(); }

    @RouteGroupId("book") {
        @RouteFunction(RouteType.Get)
        void getBook(HTTPServerRequest req, HTTPServerResponse res) {
            res.writeBody("get book");
        }

        @RouteFunction(RouteType.Delete)
         void deleteBook(HTTPServerRequest req, HTTPServerResponse res) {
            res.writeBody("delete book");
        }

        @RouteGroupId("page") {
            @RouteFunction(RouteType.Get)
            void getPage(HTTPServerRequest req, HTTPServerResponse res) {
                res.writeBody("get page");
            }

            @RouteFunction(RouteType.Delete)
             void deletePage(HTTPServerRequest req, HTTPServerResponse res) {
                res.writeBody("delete page");
            }
        }
    }
}

And the equivalent having separated out Book and Page.

class Book : ActionRouter {
    this() { super(); }
    @RouteGroupId("book"):

    @RouteFunction(RouteType.Get)
    void getBook(HTTPServerRequest req, HTTPServerResponse res) {
        res.writeBody("get book");
    }

    @RouteFunction(RouteType.Delete)
    void deleteBook(HTTPServerRequest req, HTTPServerResponse res) {
        res.writeBody("delete book");
    }
}

class Page : ActionRouter {
    this() { super(); }
    @RouteGroupIds(["book", "page"]):

    @RouteFunction(RouteType.Get)
    void getPage(HTTPServerRequest req, HTTPServerResponse res) {
        res.writeBody("get page");
    }

    @RouteFunction(RouteType.Delete)
    void deletePage(HTTPServerRequest req, HTTPServerResponse res) {
        res.writeBody("delete page");
    }
}
@s-ludwig
rejectedsoftware member

I was thinking about this case:

@RouteGroupId("book") {
    @RouteGroupId("page") {
        void getBook(...) { ... }
    }
}

but actually you are right, this works of course, because UDAs are represented as a list/tuple (my mental model was more set-like). So the equivalent notation is @RouteGroupId("book") @RouteGroupId("page") void getBook....

@rikkimax

I personally think both interfaces are quite nice. They also do the same job.
I've since added the RouteGroupIds to do a bunch at a time (although slight dmd bug so variadic constructor doesn't work for the UDA).
But for reference here is my router https://gist.github.com/rikkimax/8097560

@Dicebot
Dicebot commented Dec 23, 2013

I have been following this thread briefly, don't have much time to chime in on topic of actual implementation but have something to say about such proposals in general. I think this stuff is really cool and useful but only packages that conform existing routing / UDA schemes should be included as part of vibe.web.* package into base distribution. All other cool stuff will work better as an additional dub registry extension - otherwise newcomer mind will just explode from amount of options when getting to the docs for the first time.

@jaredonline

@s-ludwig The idea of a @collection UDA seems good. Or perhaps a @belongsTo or something? (being new to D I'm not really sure how UDA's work, so the following might not be possible):

@rootPath("books")
class Books {
    void get() { ... } // GET /books/
    void getNew() { ... } // GET /books/new
    void get(string id) { ... } // GET /books/:id
    void create() { ... } // POST /books/
    void set(string id) { ... } // POST /books/:id
    void erase(string id) { ... } // DELETE /books/:id
}

@rootPath("pages")
@belongsTo("book")
class Pages {
  void get() { ... } //GET /books/:books_id/pages
  void getNew() { ... } // GET /books/:books_id/pages/new
  ...
}

The problem with this is you can't use the same controller to respond to multiple routes... I was hoping to have the same Pages controller respond to both GET /books/:book_id/pages and GET /pages

With the ActionRouter I wrote, you could have:

router.resources("books", delegate void(ActionNamespace books) {
  books.resources("pages");
});

router.resources("pages");

I don't think dynamic dispatch is necessary or desirable

My big goal here was to do as little typing as possible to set up my routes, but to allow as much flexibility as I could.

@rikkimax

Another way of looking at it is that a book has many pages. Not a page is in a book.
You could use traits to grab all arrays in a class (and check if has e.g. @collection on it).

import std.traits;

class A {
    int i;
    string s;

    string[] sa;

    void test(){}   
}


void main() {
    A me = new A;

    foreach(f; __traits(allMembers, typeof(me))) {
        mixin("alias symbol = me." ~ f ~ ";");

        // gets rid of Monitor which is a class that mucks up typeof
        static if (!__traits(compiles, { Object o = cast(Object)symbol; })) {
            // gets rid of strings because they are an array and we only want real array members
            static if (isArray!(typeof(symbol)) && typeof(symbol).stringof != string.stringof) {
                pragma(msg, f); 
            }
        }
    }
}
@jaredonline

I've started another project for action-pack and registered it with the DUB repository http://jaredonline.github.io/action-pack/

@s-ludwig s-ludwig closed this May 16, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.