Pancake
Modern web applications often need to be constructed by interacting with numerous independent service/applications, each optimised for a specific task. However, making each of these components interact and work cohesively is difficult and time consuming. Pancake helps you construct clean, modular, re-usable code to create your web masterpieces.
Pancake is primarily a tool for making rack applications. Rack has come up in the Ruby web world as the framework that matters when developing web applications. All the major frameworks use it, although many of the application frameworks and their middlewares are not really re-usable away from their specific implementations yet.
Pancake addresses this by making Rack the fundamental building block of an application..
This README is a very high level overview only and doesn’t really cover a great deal of Pancake unfortunately.
Stacks
While Rack provides the low level framework for building web applications on, Pancake provides a stack as a place to start your application. A stack provides a whole bunch of behavior for you including a router and middleware stack. The stack will accept any valid Rack application as the endpoint. If you’re not familiar with what makes a valid Rack application it basically comes down to an object that receives the “call” method with exactly one argument, the environment hash, and returns an array with exactly 3 elements. The status, headers and body.
The general form of a stack is as follows:
----------------------------------- | | | Router | | | |---------------------------------| | Middleware | |---------------------------------| | Middleware | |---------------------------------| | Middleware | |---------------------------------| | | | Application | | Endpoint | | | -----------------------------------</p> <p>
Each stack has its own router, middleware stack and application endpoint. Stacks can be combined and also put inside Pancake.
Combined into a Pancake application it may look like this:
------------------------------------------------------------------------------- | Pancake Middleware | |-----------------------------------------------------------------------------| | Pancake Middleware | |-----------------------------------------------------------------------------| | Pancake Middleware | |-----------------------------------------------------------------------------| | | | ----------------------------------- ----------------------------------- | | | | | | | | | Router | | Router | | | | | | | | | |---------------------------------| |---------------------------------| | | | Middleware | | Middleware | | | |---------------------------------| |---------------------------------| | | | Middleware | | Middleware | | | |---------------------------------| |---------------------------------| | | | Middleware | | Middleware | | | |---------------------------------| |---------------------------------| | | | | | | | | | Application | | Application | | | | Endpoint | | Endpoint | | | | | | | | | ----------------------------------- ----------------------------------- | | | -------------------------------------------------------------------------------
Stacks are designed to work together. It’s trivially easy to mount one stack inside another, or just mount any valid rack application inside your stack. They’re also designed to be gemmed and used as libraries or full applications.
MyStack.router do |r| r.mount(OtherStack, "/stack/mount/point") end
A stack doesn’t have to mount other stacks. Pancake stacks are full applications in their own right and can be used standalone as Rack endpoints, or as middleware.
All stacks are namespaced. Pancake makes heavy use of namespacing to help construct applications concisely.
Middleware In Stacks
Middleware is a specialised kind of rack application that can either respond to a request, or pass it on to another application.
There’s two main places for middleware in Pancake. Pancake level, and stack level. If you attach middleware to Pancake itself, all requests will pass through those middlewares. If you attach it at the stack level, only that stack will have that middleware active for its requests.
When you use Pancake middleware management, you can name middleware, set middleware to come before or after other middleware, or be included or not based on a stack type label(s). Lets have a look at that.
class MyStack < Pancake::Stack stack(:session).use(Rack::Sessions::Cookie) stack(:auth, :after => :session).use(Warden::Manager, #...) stack(:bugz, :labels => [:development]).use(Rack::Bug) use(AlwaysUseThisMiddleware) end
What can a stack do
Pancake provides a whole bunch of functionality as mixins.
- BootLoaders
- Router (Usher Based. Please read.)
- Sophisticated Middlware Configuration
- Stack/Custom Configuration
- Path Management with multiple roots (makes working with gem stacks transparent)
- Content Type negotiation
- Inheritable Stacks
- Inheritable Templating System (see below)
Stacks bundle these behaviors together into a cohesive unit.
The true power of a Pancake stack is that it is fully inheritable.
Stack Inheritance
When you inherit a stack, you’re really inheriting a full application. All the features above are inherited along with the stack class.
The easiest way to explain stack inheritance is perhaps with an example.
Let’s take the example of a basic application skeleton. Many times I’ve seen GitHub repos with pre-generated applications with some basic functionality. This is then cloned as a start point for further app development. It’s basically the same as a generator but a little more flexible. What if rather than generate an application skeleton with the basic functionality, you could construct a stack complete with router, default configuration, base templates, css and javascript, even mounted applications, and gem it up? Then when you want to start an application you just require the gem and inherit the stack class. This is what Pancake stacks provide.
class MyStack < SomeOtherStack; end # A full application
Namespacing
Pancake makes heavy use of namespacing. Namespacing is a good idea to begin with, but inside mounted applications it’s a must. Embracing the constraint of namespacing provides many benefits. Avoiding unexpected name clashes, of course, is the big one.
Inheritable Inner Classes
There is a fairly unique issue when inheriting a full application. Model data may clash.
Normally when you inherit a class in Ruby, when you reference an inner class in the child, it’s actually a reference to the inner class in the parent. Let’s see why this is a problem:
class BlogStack < Pancake::Stack class Post < ActiveRecord::Base # post code end end class BobsBlog < BlogStack; end class RosesBlog < BlogStack; end BobsBlog::Post == RosesBlog::Post == BlogStack::Post
Now, let’s mount those inside some master stack:
class MyMasterStack < Pancake::Stack router.mount(RobsBlog, "/rob") router.mount(RosesBlog, "/rose") end
Now, Bob creates a post at his blog. Then goes away and creates the model (BlogStack::Post).
Now someone visits Rose’s blog. Because both Bob’s and Rose’s blogs reference the same BlogStack::User model, Bob’s post is fetched when we do a RosesBlog::Post.all
It’s efficient for Ruby to reference the parent class’s inner class like this, but it’s not always what we want.
Awesomely, some of the ORM’s include STI (Single Table Inheritance) that we can make use of. When using Pancake, we can set an inner class to inherit along with the parent. Lets have another look at that example:
class BlogStack < Pancake::Stack inheritable_inner_classes :Post class Post < ActiveRecord::Base # post code end end class BobsBlog < BlogStack; end class RosesBlog < BlogStack; end class RosesOtherBlog < RosesBlog; end # Results in class BobsBlog::Post < BlogStack::Post; end class RosesBlog::Post < BlogStack::Post; end class RosesOtherBlog::Post < RosesBlog::Post; end
In this case STI will be activated and each stack will use its own STI version of the Post model, segregating the data between the stacks.
If your Data layer doesn’t support STI, there are on_inherit hooks for all classes provided by Pancake to make any changes you need to.
Mounting Applications
You can mount any valid Rack application inside Pancake. See Rack Spec for guidance on what a valid Rack application is.
Stacks look inside their root(s) for a “mounts” directory. Where they in-turn look for a sub-directory containing the file “pancake_init.rb”. When you want to mount an application you just include that file (and any support files) and the Pancake stack will load it.
my_stack -- mounts -- another_app -- pancake_init.rb # Called to initialize the mounted app
Usually pancake_init.rb would just require the files for your application. But there’s nothing stopping you from defining the entire application in there if you wanted.
Inheritable Templates
Templates in Pancake are similar to Django templates. They define named content_blocks that can supply a default, but that can also be inherited from, and have their content combined with some other content or replaced. Unlike Django, templates are built on known Ruby templating languages like ERB and Haml. The templating system in Pancake is built on top of the great Tilt project.
This is best demonstrated with an example:
# base.html.erb <html> <head><title>Hey There</title></head> <body> <h1>My Page</h1> <ul class='nav'> <% content_block :navigation do -%> <li><a href="/">Home</a></li> <% end -%> </ul> <hr/> <% content_block :content do- %> <div class='emtpy'>Nothing Found</div> <% end -%> <% content_block :footer do -%> Footer Text <% end -%> </html>
# index.html.erb <% inherits_from :base %> <% content_block :navigation do -%> <%= content_block.super %> <li><a href="/index">Home</a></li> <% end -%> <% content_block :content do -%> # Stuff for the content <% end -%>
When you
render :index in your stack, the index template will be activated, rendering the :base template but replacing the content_block, :content, with new data. And appending to the :navigation block.
Templates can be inherited as many times as required, so you could inherit from the :index template to build on its content.
When specifying which template to inherit from, you can dynamically specify which template you’d like to use if it makes sense to do so.
At the moment only ERB, Erubis and Haml are supported with inheritable templates. Other Tilt templates are able to be rendered, but the inheritance features are not supported.
Short Stack
As part of the development of Pancake, the initial milestone was to develop a stack based on the awesome Sinatra framework. This was done as a way to develop Pancake to a simple level, not as a way to replace Sinatra. Pancake is very happy to mount Sinatra applications inside and make use of them.
A short stack is a resource focused. It uses the HTTP verbs and routes to access the code that defines a “resource”
Short stacks make use of content negotiation and views in addition to the normal stack behavior.
Install
$ gem install pancake
Generate your stack
To start with a short stack use pancakes built in generator:
$ pancake-gen short my_stack
This will generate the basic stack for you. It’s generated as a gem and based on Jeweler so you can gem it up super easy.
class MyStack < Pancake::Stacks::Short add_root(__FILE__) router.mount(UserStack, "/users") provides :html, :xml, :json get "(/)", :_name => :home do vars.some_models = SomeModel.all render :welcome end get "/new", :_name => :new do vars.some_model = SomeModel.new(params.some_model) render :new end post "(/)" do vars.some_model = SomeModel.new(params.some_model) if vars.some_model.save redirect url(:home) else render :new end end end
The Vars
In the example above you can see the “vars” method. This method provides a place for variables on a per-request basis for later use. In this case, the data is stored so that we can access it in the views. It could just as easily be used to share information between middlewares. This is a Hashie::Mash which is like a Hash with indifferent access, that can access keys via method calls. The vars method is also aliased as “v”.
This is a way to separate the concerns of Controller and View. These contexts are supposed to be separate. Breaking the encapsulation of instance variables does not need to be done in a short stack.
Templates
Templates are searched for in (stack root)/views/
Templates are separate in each sub-application and will not clash. They’re able to be looked up in parent stacks, however. Meaning, if you inherit from AStack, all of AStacks views will be used unless you replace them in the child stacks views directory.
URL generation
When you’re in a stack, you can call
url(named_url) to generate a url. This will generate the url, taking into account the mount path for the stack.
You can generate urls for other stacks also by using the global Pancake.url helper.
Pancake.url(SomeStack, :named_url)
There’s a whole bunch more to Pancake than I can go over here. But hopefully I’ve been able to provide a taste of what it is and why I think it’s pretty awesome in the upcoming Rack landscape.
Community
IRC: #pancake Google Group Twitter: #pancakestacks (hashtag) Blog / News
Bugs
If you find a bug please report it at our tails account, create a fork, and publish your fix in a topic branch..