Routing Controller

133 views
Skip to first unread message

Erik Lott

unread,
Nov 28, 2013, 9:44:49 AM11/28/13
to scor...@googlegroups.com
We're experimenting with Scorched to see if it might be a worthy candidate as the web delivery layer for a large warehousing application.

Since the app is likely going to have a few hundred endpoints (routes), we want to centralize our routing logic into a single file that will delegate to other controllers as required. Here is a contrived example:

class  ShowInvoiceController <  Scorched::Controller
   
get do
     
# …
   
end
end

class Routes < Scorched::Controller
 
   map pattern
: '/invoices/:id', method: ['GET'], target: ShowInvoiceController
   map pattern
: '/invoices/:id', method: ['PUT'], target: UpdateInvoiceController
   map pattern
: '/invoices/:id', method: ['DELETE'], target: DestroyInvoiceController
   
# ...
end


Honestly, I don't know if I'm sold on this approach, but centralizing the routing logic is important. This works fairly well, except that the pattern params - the :id param in '/invoices/:id'- won't be accessible from within the sub-controllers - (ShowInvoiceController, UpdateInvoiceController). 

Is there any way to accomplish this type of routing layer using Scorched? It would be helpful if Scorched assigned the captured pattern params to the rack env hash to be accessible via request.params … but then again, I love the isolation of the scorched controllers and request objects. 

Thoughts?

Tom Wardrop

unread,
Nov 28, 2013, 7:43:47 PM11/28/13
to scor...@googlegroups.com
Hey Erik,

Good to see you're interested in using Scorched. 

Scorched does indeed keep track of all captures. In fact, it keeps track of the entire request chain. This is all accessible via `request.breadcrumb`, which is just a bit of syntactic sugar for `env['scorched.breadcrumb']`. This stores an array of Scorched::Match elements. Scorched::Match is a struct containing various information about the matched route, including the captures. Scorched::Match#captures stores a hash if named captures are used (e.g. :id or ::id), or an array of captures if anonymous captures were used (* or **). 

Here's a little example which if you run, should give you a good idea of how it all works:

class DeepExample < Scorched::Controller
  controller '/:type' do
    controller '/:id' do
      get '/*' do
        request.breadcrumb.each { |v| p v }
        'Cool'
      end
    end
  end
end

run DeepExample

So Scorched provides all the raw ingredients in this case. It's up to you to apply it to suite your application. It seems very verbose to be using a different controller for each end-point. I'm curious to hear what you believe the benefits of doing it like that is?

Note, I a made a change to captures which I just released under v0.19 as a result of this topic (https://github.com/Wardrop/Scorched/blob/master/CHANGES.md). It's probably a good time to ensure you're aware that the Scorched API and core behaviours are not completely set in stone just yet. Breaking changes may occur in future 0.x releases. The more people who use Scorched 0.x however, the sooner v1.0 can be released. All changes, including of course any backwards-breaking changes, are recorded with every release in CHANGES.md. Just worth being aware of.

Cheers,
Tom
Message has been deleted

Erik Lott

unread,
Nov 29, 2013, 2:22:21 PM11/29/13
to scor...@googlegroups.com
Tom, I'm not sure what just happened, but I just wrote you a reply, which was instantly deleted when I posted? spam settings?

Erik

Erik Lott

unread,
Nov 29, 2013, 5:08:53 PM11/29/13
to scor...@googlegroups.com
To summarize my deleted reply; yeah I agree, the one-controller-per-route is verbose. However, after the number of endpoints in your app reaches a certain size, having a centralized location for your routes starts to become a win for management/maintenance. If we could have a centralized router which maps to controller actions, that would be ideal…. but this is starting to sound framework-ish, and is not likely in the bare-metal spirit of scorched - which I enjoy.

Breadcrumbs: okay, we can work with that.

Scorched is awesome Tom, so keep it up! I hope it catches on and replaces Sinatra.
Erik

On Thursday, November 28, 2013 7:43:47 PM UTC-5, Tom Wardrop wrote:

Tom Wardrop

unread,
Nov 30, 2013, 1:23:29 AM11/30/13
to scor...@googlegroups.com
Not sure why your message was flagged. It was pending and required approval. I chose the option to approve and to allow all messages from you in the future.

Anyway, you're correct in that Scorched isn't meant to be a framework, but I fully intend for frameworks to be implemented on-top of Scorched. The whole idea is that Scorched should make it easy to solve application-specific problems by creating mini bespoke frameworks specifically for the problem at hand.

In your case, there's all sorts of things you could do to make an elegant solution for centralised routes. A relatively simple and obvious solution would to just use the #route method, or associated #get, #post, etc, methods...

class Invoice
  def initialize(id)
    @id = id
  end
  
  def get
    "Invoice ##{@id}"
  end
  
  def save(data)
    "Saved invoice ##{@id}"
  end
  
  def delete
    "Deleted invoice ##{@id}"
  end
end

class App < Scorched::Controller
  get('/invoices/*') { |id| Invoice.new(id).get }
  route('/invoices/*') { |id| Invoice.new(id).save(request.POST) }
  route('/invoices/*') { |id| Invoice.new(id).delete }
end

run App


Again though, I don't see this route centralisation to help keep your sanity on a big project. Will only make it worse in my opinion.

Cheers,
Tom

Erik Lott

unread,
Nov 30, 2013, 11:54:49 AM11/30/13
to scor...@googlegroups.com
Arranging your route mapper using scorched like you have in your example would be complex. What we're looking for is something a little more simple:

class RouteMapper < Scorched::ImaginaryMapper
  post
'/invoices', InvoiceController, :create
 
get '/invoices/:id', InvoiceController, :show
 
get '/invoices/:id/edit', InvoiceController, :edit
 
delete '/invoices/:id', InvoiceController, :delete
  post
'/invoices/:id/line', InvoiceController, :create
 
get '/invoices/:id/lines', InvoiceController, :show
 
# … 200+ contrived routes
end

class InvoiceController < Scorched::ImaginaryController
 
def create
   
# request, views, params, available here
 
end

 
def show
   
# code..
 
end

 
def edit
   
# code..
 
end

 
def delete
   
# code..
 
end
end


This type of route mapping abstraction makes endpoint maintenance and management effortless, without adding any additional complexity. I like the fact that I can inspect/manage the endpoints for my application in a single location. Otherwise, my endpoints are scattered across multiple controller files, which makes for management hell when you're dealing with 60+ controllers.

Hope that better explains what I'm getting at.

Erik

Tom Wardrop

unread,
Dec 1, 2013, 5:23:57 PM12/1/13
to scor...@googlegroups.com
I still don't see how centralised routes makes management any simpler to be honest. Typically, how you structure an application with Scorched is like a tree. You have a `root` controller, which is the controller referenced in your rackup file. It then references sub-controllers, and so on. This is meant to allow one to scale their application infinitely. It should be very easy to follow where a given URL will end up, and all of your controllers are only concerned about the controllers directly beneath it, so there's better separation of concerns. Centralising it is a bit of an anti-pattern and not very DRY, not to mention it's more verbose, and technically, I believe it would be more complex. Here's an example of your typical tree structure...

require File.expand_path('../../lib/scorched.rb', __FILE__)

class Invoices < Scorched::Controller
  get '/:id' do |k,v|
    "Get invoice "
  end
  
  put '/*' do |id|
    "Save invoice ##{id}"
  end
end

class Products < Scorched::Controller
  # ...
end

class Catalog < Scorched::Controller
  map pattern: '/products', target: Products
  # ...
end

class Admin < Scorched::Controller
  # ...
end

class App < Scorched::Controller
  map pattern: '/invoices', target: Invoices
  map pattern: '/catalog', target: Catalog
  map pattern: '/admin', target: Admin
end

run App

I think your concerns about management and maintenance are perhaps idealistic and not practical.

If you need a print out of every route, it'd be easy enough to iterate through the mappings and print out, but that's a separate problem.

Cheers,
Tom

Erik Lott

unread,
Dec 3, 2013, 12:44:52 AM12/3/13
to scor...@googlegroups.com
Hey Tom,

Misunderstanding. I'm not suggesting that your controllers are centralized. The way that Scorched::Controller is using inheritance for filters, exceptions, etc, is correct. However, if you want your controllers to be infinitely scaleable (aka used in large application architecture), you'll likely need to untangle routing concerns from your controllers into a separate request routing abstraction. If you don't, the way that you're baking an endpoint and a control block together (sinatra style) is going to bite you. 

Here is another look at my imaginary micro-framework where controller and routing concerns are properly separated.

# proj/controllers/admin/base_controller.rb
module Admin
 
class BaseController < ImaginaryController
      before
do
         
# authorize administrator
     
end
 
end
end

# proj/controllers/admin/dogs_controller.rb
module Admin
 
class DogsController < BaseController
      after
do
         
# add 'bark' to body response
     
end
     
     
def index
         
# list all dogs code
     
end
 
end
end

# proj/controllers/admin/cats_controller.rb
module Admin
 
class CatsController < BaseController
     
def index
         
# list all cats code
     
end
 
end
end

# proj/controllers/goats_controller.rb
class GoatsController < ImaginaryController
   
def index
       
# list all goats code
   
end
end

# proj/routes.rb
class RequestRouter < Imagery::Router
   
get '/admin/animals/dogs', Admin::DogController, :index
   
get '/cats', Admin::CatsController, :index
   
get '/admin/animals/goats', GoatsController, :index
end

run
RequestRouter

Controller
  • Has no knowledge of routes
  • Has a single responsibility: providing control (pass request into the application core, and returning response)
  • Can use class inheritance for filters, exceptions, just like scorched
Router
  • Has no knowledge of controller internals (before/after filters, inheritance, etc)
  • Has a single responsibility: match a controller action to an incoming request
  • Routes are a first class citizen, and get the same amount of attention and respect as controllers
Result
  • All code is uncluttered, concise, and easy to comprehend.
  • Mappings between routes and actions are explicit, flexible, and easy to comprehend.
  • Routes and controller are very maintainable

This is as simple as it gets. Notice how concise and readable the code above is when controller and routing responsibilities have been properly separated? This imaginary microframework will scale from the medium into the large and complex while keeping controller and routing logic simple and concise.

Separation of Concerns
Separation of concerns is not related to subcontrollers only being accessible through a parent controller. If you're not familiar with separation of concerns, here is a quick primer:

DRY
The code above is as dry as it gets.

I'll ignore your comments about idealistic and unpractical. I'm sure it was just a misunderstanding about centralized controllers.

Issues with Scorched Routing In Large Scale
The Sinatra routing style that Scorched uses works great in the small apps. Sinatra was designed as single file application micro framework, and when used for this purpose, it shines. Although you can push Sinatra into a multi-file configuration (ie padrino), you can't avoid some awkwardness, since Sinatra was never intended for that purpose.

In the context of a single file app, the Sinatra routing style make sense; it's kept simple and concise by baking an endpoint and chunk of control code together into a block:

get '/myresource/blah/:id' do
 
# my wunderbar control block
end

Even though routes and control logic are baked together, in this context, it is acceptable. Control is broken into actions, and routing is kept together (single file app - no choice) and remain maintainable and readable.

This type of routing was never meant to be scaled up, and scorched segmented routes are proof of that.

Maintainable Routing

example 1
class Cont1 < Scorched::Controller
   map pattern
: '/segmented', target: Cont2
end

class Cont2 < Scorched::Controller
   map pattern
: '/routes', target: Cont3
end

class Cont3 < Scorched::Controller
   map pattern
: '/arenot', target: Cont4
end

class Cont4 < Scorched::Controller
   
get '/maintainable' do
   
# faint.
 
end
end

 OR

example 2
class Cont1 < Imaginary::Controller
 
def index
 
end
end

class Routes < Proper::Router
 
get '/whole/routes/are/very/maintainable', Cont1, :index
end

I can't imagine anyone saying that example 1 is more maintainable. I hope it's just obvious to see that when you scale-up with example 1, your routing logic is scattered across controllers. 

If you want to keep your routes maintainable, just keep them together. 

class RequestRouter < Imagery::Router
   
get '/im/happy/to/change/in', Admin::DogController, :index
   
get '/the/future/without/affecting', Admin::CatsController, :index
   
get '/the/rest/of/the/application', GoatsController, :index
end

Hope that makes sense.

Erik Lott

unread,
Dec 3, 2013, 3:19:10 PM12/3/13
to scor...@googlegroups.com
Tom, 

I posted an example a few days ago in a rush, and I can see where I gave you the impression that I'm asking for centralized controllers. The example on the Nov 30 should look like this:

class RouteMapper < Scorched::ImaginaryMapper
  post
'/invoices', InvoiceController, :create
 
get '/invoices/:id', InvoiceController, :show
 
get '/invoices/:id/edit', InvoiceController, :edit
 
delete '/invoices/:id', InvoiceController, :delete

  post
'/invoices/:id/lines', InvoiceLineController, :create
 
get '/invoices/:id/lines',  InvoiceLineController, :index
 
# … 200+ contrived routes

end

class InvoiceController < Scorched::ImaginaryController
 
def create
   
# request, views, params, available here
 
end

 
def show
   
# code..
 
end

 
def edit
   
# code..
 
end

 
def delete
   
# code..
 
end
end

class  InvoiceLineController < Scorched::ImaginaryController
 
def index
   
# code..
 
end


 
def create
   
# request, views, params, available here
 
end
end

Sorry about that buddy. 
e

Tom Wardrop

unread,
Dec 4, 2013, 8:32:53 PM12/4/13
to scor...@googlegroups.com
Hey Erik,

Still, despite your clear explanation (thanks), I would still personally use traditional nested controllers. I still think you're introducing unnecessary repetition and the possibility for errors by having all that wiring to connect your centralised routes to your endpoints. You also miss out on some benefits of Scorched which can keep your application more implicit and DRY, which would be easier to maintain. For example, if you have a deep URL such as in the example you gave with Cont1, Cont2, Cont3, etc, if the request passes through all of these controllers, each controller has an opportunity to have it's own conditions, filters, and other behaviours. You miss out on some of this if you go directly to the end controller.

Controller/Class inheritance and nesting are two separate concepts which are at their most powerful when used together. With your centralised routes, you get inheritance, but you miss out on the potential benefits gained by nesting.

With your example #1 vs. example #2 in your previous post, I find example #1 more desirable. It's more implicit, which is the same reason most people prefer "convention over configuration". `Cont4` in example #1, is pretty much exactly the same as `Cont1` in example #2, except instead of just defining methods as the endpoints, you're defining the route endpoints at the same time, killing two birds with one stone.

In the end though, I do suppose it comes down to personal preference. There's not a huge difference between the two approaches, but I do personally prefer example #1 using nested controllers. It also fits better with Scorched.

Anyway, to implement your centralised routes, overriding `get`, `post`, etc, wouldn't be advisable. Instead I'd come up with a new method name, such as `endpoint`. Here's a working example of how you may implement this...


We use `conditions` as a way of communicating the `action` to the target controller, which looks up the `action` via the breadcrumb.

By the way, how are you getting the code highlighting in your posts here on Google Groups?

Cheers,
Tom

Erik Lott

unread,
Dec 5, 2013, 2:09:41 AM12/5/13
to scor...@googlegroups.com
Hey Tom,

Thanks for sticking with the thread.

Here is a video and book you might enjoy:

You also miss out on some benefits of Scorched which can keep your application more implicit and DRY, which would be easier to maintain. For example, if you have a deep URL such as in the example you gave with Cont1, Cont2, Cont3, etc, if the request passes through all of these controllers, each controller has an opportunity to have it's own conditions, filters, and other behaviours. You miss out on some of this if you go directly to the end controller.

Not at all. Take a close look at the example below, and keep your eye on class inheritance and filters. 

class BaseController < ImaginaryController
  before
do

    authenticate_session
 
end

 
def authenticate_session
     
# authenticate session code
 
end
end

class AdminController < BaseController
  before
do
    authorize_admin
 
end

 
def authorize_admin
     
# authorize admin code
 
end
end

class CatsController < AdminController
 
def index
   
# code...
 
end

 
def create
   
# code...
 
end
end

class Router < Framework::Router
 
get '/blah/cats', CatsController, :index
  get '/another/route/for/cats', CatsController, :index
end

The '/blah/cats' path maps directly to the CatsController index action. The CatsController is a descendant of AdminController, which is a descendant of BaseController. The filters and conditions defined on a controller are inherited by its descendants. The CatsController will have 2 before filters applied to it: 1) authenticate_session, and 2) authorize_admin - in that order. 

Can you see it now? Hopefully, that's a eureka moment. You should realize that with a router and controller inheritance, you can achieve any complex routing effortlessly.

Controller/Class inheritance and nesting are two separate concepts which are at their most powerful when used together. With your centralised routes, you get inheritance, but you miss out on the potential benefits gained by nesting.

Now that you understand the example above, i'm sure you realize that you don't need scorched's 'nesting' to achieve what you're looking for.

Class inheritance is a ruby feature, and is the right concept to use in your controllers. Nesting is an arbitrary feature that you needed to construct because you have Sinatra style routing (routing and controllers firmly baked together). I'm guessing that when you developed the Scorched controllers you 1) added the Sinatra style routing 2) added inheritable filters, conditions, etc  and then 3) realized that your controllers weren't flexible enough to express the wide range of paths that you hoped they could, so you added the concept of 'nesting' into the controllers.

Feature 1 limited you, so you created feature 3 to fight that limitation.

As soon as you extract the routing concerns from your controller, 'nested' controllers (not to be confused with controller inheritance) are no longer needed and disappear from scorched altogether. Every feature you want scorched to provide can be achieved with a simple router and controller. If that's still not clear, propose a routing/controller combination that can be defined by Scorched, but not by a Router + Controller combination. I'll be happy to walk you through it.

With your example #1 vs. example #2 in your previous post, I find example #1 more desirable. It's more implicit, which is the same reason most people prefer "convention over configuration". 

If you enjoy example #1, just keep coding and strive to achieve better and better OO code. I'm confident your opinion will change. Convention over configuration is fine, as long as you understand and accept the assumptions that are being imposed on the design of your code. Must noobs don't, which is why most new developers love Rails.
 
`Cont4` in example #1, is pretty much exactly the same as `Cont1` in example #2, except instead of just defining methods as the endpoints, you're defining the route endpoints at the same time, killing two birds with one stone.

It may seem better that way in the beginning, but If you subscribe to the killing-two-birds-with-one-stone logic, then we should also keep our view html code inside controllers as well - our controllers could then kill 3 birds with one stone: routing, control and html rendering… maybe not a good idea.
 
In the end though, I do suppose it comes down to personal preference. There's not a huge difference between the two approaches, but I do personally prefer example #1 using nested controllers. 

I can see how it may seem like these are personal preferences, and that's okay. My advice would be to use scorched in a large application, and see how it goes. 

Like I said before, all of the comments I'm making above are a response to your attempt to make scorched scale infinitely (work in large scale) - I'd like to see Scorched succeed. Scorched will work fine in small and mid size apps as it is, much like Padrino. If you want Scorched to scale into the large, take some time to build and maintain a large application to understand the needs and complexities of working at large scale. Large design is not simply scaled-up small design.

If you haven't looked at other successful lightweight frameworks, look at Merb (no longer maintained). Unlike Sinatra (padrino), merb was successful in the small and large, because it included the proper abstractions to do so. 

E

Tom Wardrop

unread,
Dec 5, 2013, 8:32:16 PM12/5/13
to scor...@googlegroups.com
Not sure where to begin on my reply Erik.

With regards to inheritance, yes, I'm fully aware that your controller will inherit almost everything from it's parents. The difference between going direct to CatsController compared to going via the AdminController is more subtle. Centralising your routes makes them much more static. This may be desirable. Nesting on the other hand makes request routing much more dynamic. As an example, let's say the mapping for the AdminController has a condition defined on it. Maybe it only serves HTML, for example (I'm also showing an alternate strategy for nesting controllers)...

class RootController < Scorched::Controller
end

AdminController = RootController.controller '/admin', conditions: {media_type: 'text/html'}  do
end

CatsController = AdminController.controller '/cats' do
  get '/' do
    '<strong>Meow!</strong>'
  end
end

In this case, when a JSON request comes in, it's going to fail that condition, and move onto the next matching route. If there isn't another matching route, an appropriate 406 Not Acceptable status is returned. Another possibility is that the AdminController may define some funky compatibility mode for XML to JSON (for lack of a better example). It may define a `before` filter that converts XML to JSON (or vice versa), modifying the request headers appropriately, and then converting the format back to what the client expects in an `after` filter. This compatibility layer which modifies the request headers may alter the routing of the request as it descends through the chain of nested controllers.

You may prefer (understandably) the static-ness of centralised routing, but I'm just pointing out that there are differences that can affect the design of your application. You have managed to convince the benefits of centralised routing though, and I may in fact add facilities in Scorched to provide native support for centralised routes. Centralised routes are certainly more explicit and predictable, and in fact may provide a more sane option in larger applications whilst still getting most the advantages of Scorched. There's also nothing stopping one from mixing the two strategies depending on requirements.

Tom

Tom Wardrop

unread,
Dec 5, 2013, 9:39:29 PM12/5/13
to scor...@googlegroups.com
Keep in mind too that as your paths get deeper, and you require more conditions, your routes are going to get pretty gnarly...

class Root < Scorched::Controller
  endpoint
'/some/quite/deep/path/:type/:id', conditions: {method: 'GET', media_type: 'text/html', some_other_conditions: true}, MyController, :view
  endpoint
'/some/quite/deep/path/:type/:id/edit', conditions: {method: 'GET', media_type: 'text/html', some_other_conditions: true}, MyController, :edit
  endpoint
'/some/quite/deep/path/:type/:id/edit', conditions: {method: 'POST', media_type: 'text/html', some_other_conditions: true}, MyController, :save
end

You could use nested anonymous controller to make it more DRY however...

class Root < Scorched::Controller
  controller
'/some/quite/deep/path', conditions: {media_type: 'text/html', some_other_conditions: true} do
    endpoint
'/:type/:id', conditions: {method: 'GET'}, MyController, :view
    endpoint
'/:type/:id/edit', conditions: {method: 'GET'}, MyController, :edit
    endpoint
'/:type/:id/edit', conditions: {method: 'POST'}, MyController, :save
 
end
end

Do you think you would use anonymous nested controllers in your route mapping controller?

Tom

Erik Lott

unread,
Dec 5, 2013, 10:58:09 PM12/5/13
to scor...@googlegroups.com
Hi Tom, 

Yup, that's all moving in the right direction. It looks gnarly, but that's understandable; the Scorched Controller is trying to be used as a router, and its dsl hasn't been specifically designed for this purpose.

endpoint '/:type/:id', conditions: {method: 'GET'}, MyController, :view

If you decide that you want to move forward with a routing abstraction, I would recommend making a strong division between your control concerns (filters, control, request, response), and your routing concerns (paths, namespacing, route conditions, content type), and extracting all routing concerns from your controller class, into a new router class. Next, design a nice clean dsl for your router, so that you can write the above route as follows:

get '/:type/:id', MyController, :view

Do you think you would use anonymous nested controllers in your route mapping controller?

Yes. We don't need to concept of 'controllers' to pull this off however. If we build a new router class, we can accomplish this only using routes and scopes:

class Root < Scorched::ImaginaryRouter
  scope
'/some/quite/deep/path', conditions: {media_type: 'text/html', some_other_conditions: true} do
   
get '/:type/:id', MyController, :view
   
get '/:type/:id/edit', MyController, :edit
    post
'/:type/:id/edit', MyController, :save
 
end
end

You can call a 'scope' whatever you want. It simply adds a path prefix to the routes contained inside its block.

You can always refer to the Rails router documentation for ideas. Their router is heavy weight (it contains every feature under the sun), but the abstraction is correct….  A nice lightweight router would be preferable though.

Looking good Tom!
E

Erik Lott

unread,
Dec 6, 2013, 9:31:18 AM12/6/13
to scor...@googlegroups.com
Tom, 

sorry, I didn't have time to reply to all your messages last night.

In this case, when a JSON request comes in, it's going to fail that condition, and move onto the next matching route. If there isn't another matching route, an appropriate 406 Not Acceptable status is returned.

Like you've done in your second email, I would move the json condition into the router. If the condition is used for 'matching' the incoming request, it belongs in the router.

class Root < Scorched::ImaginaryRouter
  scope
'/admin', conditions: {media_type: 'text/html', some_other_conditions: true} do

   
get '/:type/:id', MyController, :view
   
get '/:type/:id/edit', MyController, :
edit
 
end
end

Another possibility is that the AdminController may define some funky compatibility mode for XML to JSON (for lack of a better example). It may define a `before` filter that converts XML to JSON (or vice versa), modifying the request headers appropriately, and then converting the format back to what the client expects in an `after` filter. This compatibility layer which modifies the request headers may alter the routing of the request as it descends through the chain of nested controllers.

What you've described is Rack Middleware. For example:

class FunkyCompatibilityXmlMode
 
def initialize app
   
@app = app
 
end

 
def call env
   
# modify request here...
    app
.call(env)
   
#modify response here...
 
end
end


class RootController < Scorched::Controller
end

class AdminController < RootController
 
use FunkyCompatibilityXmlMode
end


class CatsController < AdminController
 
def show
 
end
end

The middleware behaviour (adjusting the request object as the request flows up the middleware chain, and adjusting the response as it flows down the chain) lives in it's own class. You then assign this middleware to the appropriate controller. Now any controller action inside AdminController, or actions inside of classes descending from AdminController (ie. CatsController) will be wrapped by the middleware.

The way the request/response flows through the app in the example above would look something like this:
rackin -> Router -> Controller -> FunkyCompatibilityXmlMode ->  Controller action ->  FunkyCompatibilityXmlMode -> rackout

You can accomplish the same thing using a before and after filter in a controller like you've mentioned, but for me, modifying the incoming request in a before filter would a warning light that this functionality may belong in middleware instead.

E

Tom Wardrop

unread,
Feb 23, 2014, 8:43:00 PM2/23/14
to scor...@googlegroups.com
Hey Erik,

I was playing around the other day seeing how Scorched could better accommodate centralised routing like you've explained. The conclusion I've come to is that Scorched more-or-less already accommodates this. The only thing to consider is that in order for a Scorched controller to "dispatch" a request, it needs to be able to match a mapping. This is something that can't really be changed, but it doesn't need to be.

I've included an example in the main repo of how you may achieve rails-style routing: https://github.com/Wardrop/Scorched/blob/master/examples/rails_style_routing.ru

I'm curious to get your feedback. Basically, you just need to ensure the target controller contains a "mapping". You can easily create a catch all route like the following though if you prefer:

route('/*') { }

# Or if you want even more control

map
(pattern: '/*$', target: proc { })

But, you're normally better of defining the individual end-points like in the rails-style routing example, as you get a bit of extra stuff for free, like the returning of appropriate status codes like 405 method not allowed.

I've also separated the dispatch logic into it's own "dispatch" method as of v0.21, if for some reason you want/need more control.

Thoughts?

Erik Lott

unread,
Feb 24, 2014, 1:20:28 PM2/24/14
to scor...@googlegroups.com
Hey Tom,

I had a quick look at your example code at https://github.com/Wardrop/Scorched/blob/master/examples/rails_style_routing.ru . I can see how you're trying to separate the code into 2 logical concerns (routing and control). Conceptually, that's moving in a better direction, but, the core of scorched really hasn't changed, and it's still adding awkwardness where no awkwardness should exist. 

If I were to refactor your example code using Erik's imaginary framework (EIF), my code might look some thing like this:

class RootController < EIF::Controller
 
def index
   
'Hello'
 
end
 
 
def create
   
'Creating it now'
 
end
end


class CustomerController < EIF::Controller
 
def index
   
'Hello customer'
 
end
end


class OrderController < EIF::Controller
 
def index
   
'Me order'
 
end
end


Class Router < EIF::Router
 
get '/', 'root#index'
  post
'/', 'root#create'
 
get '/customer', 'customer#index'
 
get '/order', 'order#index'
end


app
= EIF::App.new
app
.register_router(Router)
app
.register_controller(RootController)
app
.register_controller(CustomerController)
app
.register_controller(OrderController)

run app

My point with my above code is to show that with the proper abstractions, your code can remain clean and simple, for both the end user and the developer.

The routing in your example code....

def self.inherited(klass)
  klass
.get('/') { invoke_action :index }
  klass
.get('/new') { invoke_action :new }
  klass
.post('/') { invoke_action :create }
  klass
.get('/:id') { invoke_action :show }
  klass
.get('/:id/edit') { invoke_action :edit }
  klass
.route('/:id', method: ['PATCH', 'PUT']) { invoke_action :update }
  klass
.delete('/:id') { invoke_action :delete }
end

App.controller '/customer', Customer
App.controller '/order', Order
App.controller '/', Root

... is still unnecessarily complicated, and makes a bunch of global assumptions about my REST endpoints. This complexity exists because scorched wasn't originally designed to be used in this configuration (router + controller), but now it's trying to be adapted to do so. 

Because a bottom-up refactor is probably out of the question, I would stick to Scorched's original use-case, forget about the router + controller separation, and keep the code base clean, simple, and targeted.

That's my 2 cents.

Cheers
Erik

Tom Wardrop

unread,
Feb 24, 2014, 6:21:12 PM2/24/14
to scor...@googlegroups.com
Thanks for the reply Erik,

Perhaps a fundamental point of design is that Scorched controllers - and I'm about to invent some new terminology here - are internally coordinated. Every Scorched controller is a valid Rack application in it's own right, and is invoked as such. In other words, you can say every controller is it's own router. The controller is `call`'ed, and it takes over from there. This, as you've acknowledged, prevents one from separating out the routing and controller concerns.

To achieve the following style of routing...

get '/order', 'order#index'

...the router/controller in which that's defined would likely need to fully coordinate the instantiation of the target controller, and the subsequent invocation of the target method, running of filters, error handling, etc. For that to be possible, you'd end up with a framework with little resemblance to Scorched, and would end up solving a completely different problem, as you've also acknowledged.

With that said, my rails-style routing example shows that it's possible to implement your own predefined routes in a base class to reduce repetition. There are many other DSL changes you could make on a per-project basis, but the fundamental caveat is the internal vs external coordination of controllers.

Again, I appreciate the discussion. Always makes for a good test of knowledge and assumptions.

Tom

Erik Lott

unread,
Feb 24, 2014, 9:55:50 PM2/24/14
to scor...@googlegroups.com
Hey Tom,

Just incase the following code spurred the comment about internal vs external 'coordination'...

app = EIF::App.new
app
.register_router(Router)
app
.register_controller(RootController)
app
.register_controller(CustomerController)
app
.register_controller(OrderController)

... just ignore these 'register' methods. I could just as easily have delegated to these controllers internally. This is just an explicit registration style that we tend to use in-house because it's unambiguous.

With that said, my rails-style routing example shows that it's possible to implement your own predefined routes in a base class to reduce repetition

Your example looks cleaner, but I'm not yet convinced that these global routes would be useful to us in a context of a large project:

klass.get('/') { invoke_action :index }
    klass
.get('/new') { invoke_action :new }
    klass
.post('/') { invoke_action :create }
    klass
.get('/:id') { invoke_action :show }
    klass
.get('/:id/edit') { invoke_action :edit }
    klass
.route('/:id', method: ['PATCH', 'PUT']) { invoke_action :update }
    klass
.delete('/:id') { invoke_action :delete }

This code seems to assume that each resource (customer, order, etc) responds to exactly 7 REST endpoints. What if 50% of my resources don't respond to all 7?
Do I have the flexibility to create a non-restful route when exceptional cases arise? such as /customer/:id/blahblah?

Erik

Tom Wardrop

unread,
Feb 25, 2014, 12:14:47 AM2/25/14
to scor...@googlegroups.com
The internal vs external coordination comment was more in reference to:

get '/order', 'order#index'

Where the controller AND action are defined in another controller/router. It's not possible to instantiate a Controller manually, and then call an arbitrary method on it and expect filters, error handlers, etc, to all work. The target controller needs to have a matching mapping.

As for my example, the `invoke_action` method action checks for the existence of the corresponding action method, if it doesn't exist, it will `pass` the request. If `new` is defined but `create` isn't, and the request corresponds to `create`, Scorched will automatically return a 405 Method Not Allowed, assuming there's no other potential match for the request.

Tom

Erik Lott

unread,
Feb 25, 2014, 12:01:13 PM2/25/14
to scor...@googlegroups.com
Yeah, I guess your invoke_action method works, but at this point, the solution is still a hack on the framework, rather than the internals truly supporting this usage.

If you happen to expand scorched in the future, let me know. At the moment, there don't seem to be any well supported lightweight frameworks in the Ruby ecosystem (such as Merb used to be). There are a plethora of microframeworks (scorched, sinatra, padrino, camping, cuba, grape, etc), and then Rails. If you could fill that gap, we'd use it :)
Reply all
Reply to author
Forward
0 new messages