Skip to content

hassox/pancake

Repository files navigation

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. It provides very useful helpers on top of Rack that assist in constructing Rack stacks as mixins. Almost all key aspects of web frameworks are covered in Pancake as mixins to help you create your own re-usable Rack Stacks without worrying about the really low level plumbing.

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             |
|                                 |
-----------------------------------

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.

(See here for examples )

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.

Get Started

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/.. and rendered.

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.

Note on Patches/Pull Requests

  • Fork the project.
  • Make your feature addition or bug fix.
  • Add tests for it. This is important so I don’t break it in a
    future version unintentionally.
  • Commit, do not mess with rakefile, version, or history.
    (if you want to have your own version, that is fine but
    bump version in a commit by itself I can ignore when I pull)
  • send me a pull request. Bonus points for topic branches.

Copyright

Copyright © 2009 Daniel Neighman. See LICENSE for details.