Idea

+priority

 

+issues

We need a simple standards based api all part of Wagn.  Rack is a well establish specification for web service middlewares.  Rack integration is also part of Rails 3, so this is also part of the path to that.

 

+solution

In a nutshel, a Rack application is a ruby object that responds to the :call method, just like a Proc.  It takes a single parameter, env which has a Hash like interface.  The Rack SPEC is mostly about what keys and values are in this hash.  Most of the variables correspond to widely used standards for middleware, not just ruby, and are already being used to dispatch Wagn requests "under the hood" in Rails.

 

This proposal splits Wagn into three components, a model, view and controller (MVC).  The CardController in the diagram is much like the current CardController, only instead of calling model and view methods directly, it loads wagn.* variables into the env to tell the lower middlewares what to do.  These wagn.* attributes are a private namespace to the Wagn MVC Rack architecture, just as rack.* entries are used by Rack and to communicate with Rack.

 

RESTful Web API+Rack+solution+diagram

Routing

The routing will now all be Rack based, which means it will use env['wagn.*'] objects in the rack env hash to pass objects along the processing chain.  Testing needs to use this routing rather than controller based testing as we will not keep that interface stable and we could even eliminate the CardController object.  The remaining controller would then be the ApplicationController which could be as simple as this:

class ApplicationController << ActionController::Base
  def all
    card.call(env)
    render show
  end
end

With the 'show' view defined very close to the render_show method of the current card controller (i.e. just get the renderer for the card/format and call render on it).

It that case, we would have move the routing and the processing described below for CardController into middleware.  Because this will all be Rack based, it will be either to make this transformation if it is desirable.

Wagn Variables

wagn.id => The 'id' part of the request path.

wagn.format => A matched format extension.

wagn.action => The requested action

wagn.view => The requested view

wagn.type => The requested type

wagn.args => Parameters for a card (params['card'])

wagn.params => The rest of the parameters

Card Controller

The first version of this has the routing in a 'matches?' method used as a 'constraint' to route everything that it matches to card#all.  This stage has to transform what the routing did further.  The wagn vars above are the inputs, and more will be created in this phase.

In REST terms, some operations are on a specific item in the collection and some are on the collection.  You create new items on the collection, you read, update or remove existing ones.  Missing cards are a special case for wagn, technicaly they are a view on the collection, as is the 'new' action that formats a form that would submit a create action.  The importance in the controller phase is in where we look for card arguments, in wagn.id for existing cards, or wagn.args for the specs of a new or missing card.

Wagn Variables

wagn.id    =>      If this is d+, we might make it an Int type rather than String

wagn.name =>   Transformed with cardname rules, (String, or maybe Wagn::Cardname)

wagn.format => Add defaults and regularize (Symbol)

wagn.view   =>   (Symbol)

wagn.model  =>  The model action (CRUD)

wagn.render =>   The renderer view

wagn.card   =>    The fetched or created (new/create) card

 

params['append']  =>  For update, content to be appended to current content (comments)

params['add_item'] =>  An argument for models like Pointer that have an add_item method

params['drop_item'] => An argument for drop_item

Card Model

Rack middleware interface.  Output in wagn.status, or throw an error.

create, read, update, remove      Returns just wagn.status for most errors.

create!, read!, update!, remove! Same functions as above, but throw errors, return only for normal render

Wagn Variables

wagn.status True if no errors

Renderer

We aren't quite here yet, but in the end, it would be nice to use Rack idioms here:

use Wagn::Renderer.new Card::Rack.new() Application::Dispatcher.new()

The idea would be that Application::Dispatcher.new() would be the app represented by the Rails application, routes.rb, and the above in config.ru would create our application thusly:

class Wagn::Renderer
  def self.new(app, *pipeline)
    @app = app
    @pipeline = pipline
  end
  def call(env)
@pipeline.each { |a| a.call(env) }
    @app.call(env)
  end
end

 

 

 

Let's discuss:


This is looking even better with respect to Rails 3 features. In particular, ExceptionNotification seems to be another rack middleware. With the above design, many modular additions can go in middleware placed into this MVC card stack.

 

I'm also thinking that Notifications have all the structure we need Hooks, so those are sort of redundant: http://railscasts.com/episodes/249-notifications-in-rails-3

  --Gerry Gleason.....Sat Jun 11 09:31:28 -0700 2011


Now that I'm implementing parts of this, maybe I'll add some more concreate API info here as it is designed.

  --Gerry Gleason.....Fri Nov 18 04:23:30 -0800 2011


little confused about the api above. looks like you're setting instance variables in a class method (self.new). are those supposed to be in #initialize?

  --Ethan McCutchen.....Sat Nov 19 12:19:09 -0800 2011


Yeah, that's wrong, it should be an initialize method

  --Gerry Gleason.....Mon Nov 21 08:11:28 -0800 2011


Some notes on the emerging implementation:

Some of the argument passing has to be worked out so that you can put in alternatives to each component, but basically you have what is in the diagram where each MVC component has a Rack interface. We add ourselves in a builder stack with this in config/application.rb:

 

config.middleware.use Wagn::Rack

 

This is the controller component, and it gets the Rails app as defined in config/routes.rb, plus an optional modelapp, which it creates with CardController.new by default. Right now we ignore the return from this, but technically we should always return a valid rack response. Finally we call the Rails app. The rails part of this is pretty thin. The matches? method used by constraints does the final setup of params based wagn.* args and matching (return true) if wagn.action was set in the pre-processing. That takes use to application#all which does the rendering for all cases.

 

So, M is in CardController (I think this can be a Metal app since we don't use much Rails here),

V is in ApplicationController (as the Rails App in routes.rb), and

C is config/application.rb

 

We can split out the Rack parts of config/application.rb into another file for clarity and better separation.

  --Gerry Gleason.....Mon Nov 28 07:58:14 -0800 2011


There are some really stark distinctions about what you can do at in each component. In C, you pretty much are just changing things in the env hash, adding wagn.* variables. The little bit in matches? is to access params because it is here that we have it first as a "request", which includes the env. As mentioned, we ignore the return from M, but it can set wagn.render and wagn.status and that will control the View, but it is the only stage that can access the model. V can use the model, but only via wagn.card as loaded and modified in the M component.

  --Gerry Gleason.....Mon Nov 28 08:08:33 -0800 2011


I just realized that we have been thinking about part of this all wrong. Or maybe more that we've missed something. We've left out the idea of sessions, which connects to CRUD via permissions. The upshot is that what that account controller does isn't CRUD, but there are model actions. The linking to the second table and hence the AR model will go away, but setting and clearing the session (sign-in, sign-out) needs to be handled as model actions. I think it could even be CRUD semantics, but for the 'active session'. Sign-in is then create session, Sign-out is delete. You implicitly read the session in permission checks. (We could add "log-in as another user" as update, then allowing 'create_and_update' for sessions would mean you could 'signin' again and it would replace your session. I guess the way we use it, session is a singleton object, where both wagns and cards are at least potentially collections.

  --Gerry Gleason.....Sat Dec 03 03:26:40 -0800 2011


Mechanically, a cookie or other feature connects the network session to a session object, which is part of the request by the time we get it. Another middleware does network session to session object mapping, so the current session (or none) is something externally supplied. The card CRUD actions read the current session to get the validation actor for permission checks. The sign-in, sign-out actions do not update the session, they add and remove the network session in the authentication layer. When another request comes from a source that just signed out, the current session will be empty, when they sign in again, the next request has that session set to the new user.

 

I think this means that 'current_user' needs to always come directly from the request, and if we want to store it or related values not already in rack variables, use env['wagn.session'] or 'wagn.current' if you like that better. We will have to clear up some details about 'as_user' and administrative tasks that run without a session. In scripts, often anon becomes wagbot directly, which is different than when that happens for a normal request doing as 'as' operation. We might even consider the system user (gerry, root) as the session for scripts to complete the chain of session ownership.

  --Gerry Gleason.....Sat Dec 03 04:05:16 -0800 2011


There are lots of cases where the as_user thing is a bit of a hack, but others where it's quite important, and I think more of those will happen. For example, I think we really want an administrator to be able to see any page *as it would look to a given user*.

 

I don't think this violates the idea of the current_user always coming directly from the request, but it means that, as is currently the case, we rarely refer directly to the current_user, preferring the as_user (which in normal circumstances falls back to the current_user).

  --Ethan McCutchen.....Sat Dec 03 08:59:10 -0800 2011


btw, I think we're going to want a wagn.config that basically takes settings directly from the yaml file that's going to replace wagn.rb

  --Ethan McCutchen.....Sat Dec 03 09:18:16 -0800 2011


The exploration of these ideas is still on the 'rackish' branch, and it has been kept pretty much in sync with traits.

  --Gerry Gleason.....Tue May 22 15:03:01 +0000 2012

+relevant user stories