Home

Awesome

Eldr Build Status Code Climate Coverage Status Dependency Status Inline docs Gratipay

Eldr is a minimal ruby framework that doesn't hide the rack. It aims to be lightweight, simple, modular and above all, clear. Eldr is a ruby framework without all the magic.

Eldr apps are rack apps like this:

class App < Eldr::App
  get '/posts' do
    Rack::Response.new "posts", 200
  end
end

Since they are rack apps you can boot them like this:

run App

And when you want to combine them you can do this:

class Posts < Eldr::App
  get '/'
    Rack::Response.new "posts", 200
  end
end

class Tasks < Eldr::App
  get '/'
    Rack::Response.new "tasks", 200
  end
end

map '/posts' do
  Posts.new
end

map '/tasks' do
  run Tasks.new
end
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

Table of Contents

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Features

Installation and Usage

To install Eldr add the following to your gemfile:

gem 'eldr'
# or if you want to use the master branch:
# gem 'eldr', github: 'eldr-rb/eldr'

Then run bundler:

$ bundle

To use it you need to create a rackup file. Add the following to a config.ru file:

class HelloWorld < Edlr::App
  get '/', proc { [200, {'Content-Type' => 'txt'}, ['Hello World!']]}
end

run HelloWorld

Route handlers are anything that respond to call and return a valid Rack response. All the http verbs you expect are available:

get '/',  proc { [200, {'Content-Type' => 'txt'}, ['Hello World!']]}
post '/', proc { [201, {'Content-Type' => 'txt'}, ['Hello World!']]}

etc...

For further usage examples checkout the examples folder

I have already built and released extensions for many common tasks:

Quickstart Guides

Hello World

Start by adding the following to your gemfile:

gem 'edlr'

Then run bundler:

$ bundle install

Now create a new file called app.rb with the following contents:

class App < Eldr::App
  get '/' do
    Rack::Response.new "Hello World!", 200
  end
end

This defines a new App with a root route that returns "Hello World!". To make use of this app we need to add a rackup file. In the root of your project create a new file called config.ru:

require_relative 'app'
run App

Now boot it using rackup:

$ rackup

When you visit http:///localhost:9292 in your browser you should see "Hello World!"

Rendering a Template

Eldr provides no render helper in its core but it is easy to define your own. One can use Tilt as the templating library (with your engine of choice) and then create some helper methods to handle finding the template and rendering it.

Create a module with the following:

module RenderingHelpers
  def render(path, resp_code=200)
    Rack::Response.new Tilt.new(find_template(path)).render(self), resp_code
  end

  def find_template(path)
    template = File.join('views', path)
    raise NotFoundError unless File.exists? template
    template
  end
end

This takes a template, finds the template and then passes it off to tilt for handling. If the template does not exist it raises a NotFoundError.

We can use these helpers by including them in our App:

class App
  include RenderingHelpers

  get '/cats' do
    render 'cats.slim'
  end
end

Using it, is as simple as including it!

Checkout: eldr-rendering for some pre-made rendering helpers.

Before/After Filters

Eldr comes with support for before/after filters. You can add a filter like this:

class App < Eldr::App
  before do
    @message = 'Hello World!'
  end

  after do
    puts 'after'
  end

  get '/', proc { [200, {}, [@message]] }
end

Filters can be limited to specific routes by giving your route a name:

class App < Eldr::App
  before(:bob) do
    @message = 'Hello World!'
  end

  after(:bob) do
    puts 'after'
  end

  get '/', name: :bob, proc { [200, {}, [@message]] }
end

Helpers

In Eldr, helpers are not mystical elements, they are plain ruby modules:

module Helpers
end

If you need registration callbacks then you can make use of ruby's own included callback:

module Helpers
  def included(klass)
  end
end

If you need to define these methods directly on the app class, something like helpers do, you can do the following:

class App
  def helper_method
  end
end

This will put your helper method on every instance of your app's class and make it available to any templates.

Rails Style Routing

In the rails world routes are methods on controller instances:

class Cats
  def index
    # stuff here
  end
end

Then in a separate router object we map our routes to these action methods.

get '/cats', to: 'cats#index'

This is easy to do with Eldr; because Eldr apps are Rack apps, we can use one as a dispatcher/router and then another as our controller.

class Cats
  def index
    Rack::Response.new "Hello From all the Cats!"
  end
end

Then run it:

run Router.new do
  get '/cats', to: 'cats#index'
end

We could add a lot more abstraction but this is simple enough and accomplishes our goals. Eldr encourages you to build up abstractions as you go.

Pre-building flexibility and accounting for things you don't need to do yet is unnecessary work. You should build apps that you can see into and actually understand, no magic.

Rails Style Responses

The Rails world has a tremendous amount of conventions and abstraction for dealing with responses. They range from complex to simple, from format helpers to full fledged REST abstractions. At their core, they all take information from Rack::Request and turn it into a valid Rack response -- and a valid rack response is just an array.

Once we realize this, replicating rails becomes just a matter of patterns.

Eldr routes must return a valid rack response, which is an array -- actually its just something that can act like an array. We can return an object that responds to :to_a and Rack will be just as happy.

Let's create a object that holds a response string, response headers, and a status.

class Response
  attr_accessor :status, :headers, :body

  def initialize(body, status=200, headers={})
    @body, @status, @headers = body, status, headers
  end
end

Now add a to_a method:

def to_a
  [@status, @headers, [@body]]
end
alias_method :to_ary, :to_a # this makes ruby aware the object can act like an array

To use this we just return a new instance of it as our response:

Eldr::App.new do
  get '/' do
    Response.new("Hello World!")
  end
end

This simple pattern is enough to build all sorts of powerful abstractions.

Rails Style Requests

Rails provides all sorts of ways to digest the data passed to it by a client. At their core they all operate on Rack's env object. They take Rack's env object and pass it off to a wrapper.

The wrapper modifies the request only on the env object, this means at any point we can duplicate the state of the wrapper by passing it the env; and any objects that need parsed request data (e.g params) will know to find it in the env object.

The most typical way rails helps us with requests is through parameter parsing and validation. If we of params a just a hash, then validating, error handling on them etc is simple, we can use all the tools we are already used to.

Lets start by getting our parameters into a hash. To do this we pass env into Rack::Request; a wrapper object that parses the query string and injects the results into @env["rack.request.query_hash"]:

class App < Eldr::App
  post '/cats' do |env|
    request = Rack::Request.new(env)
    params  = request.GET # we could also access it from env["rack.request.query_hash"]
  end
end

Now we will need an object to validate the parameters. We can use Virtus and ActiveModel::Validations for this:

class CatParams
  include Virtus.model
  include ActiveModel::Validations

  attribute :name,       String
  attribute :age,        Integer
  attribute :human_kils, Integer

  validates :name, :age, :human_kills, presence: true
end

To validate a request we need to instantiate CatParams:

class InvalidParams < Edlr::ResponseError
  def status
    400
  end
end

class App < Eldr::App
  post '/cats' do |env|
    request = Rack::Request.new(env)
    params  = CatParams.new(request.GET)

    raise InvalidParams.new(errors) unless params.valid?

    cat = Cat.create(params.attributes)
    Rack::Response.new(cat.to_json)
  end
end

If our parameters are invalid we raise an InvalidParams response (Eldr has error handling). If they are valid, then we create our Cat and return it as JSON.

Errors

You can raise an error with any class that inherits from StandardError and responds to call. This is useful in before filters, when you want to halt a route from executing.

An error class looks like this:

class ErrorResponse < StandardErrro
  def call(env)
    Rack::Response.new message, 500
  end
end

And you can use it like a standard ruby error class:

app = Edlr::App.new do
  get '/' do
    raise ErrorResponse, "Bad Data"
  end
end

run app

Error Catching

Eldr will NOT catch all errors. In a production setting you will need to use middleware to make certain nothing ever explodes at your user. Something like rack-robustness will work fine:

class App < Edlr::App
  use Rack::Robustness do |g|
    g.on(ArgumentError){|ex| 400 }
    g.on(SecurityError){|ex| 403 }

    g.content_type 'text/plain'

    g.body{|ex|
      ex.message
    }

    g.ensure(true){|ex|
      env['rack.errors'].write(ex.message)
    }
  end
end

run App

Conditions

Most microframeworks provide a DSL for route conditions. This needlesly complicates a framework with multiple ways to do things. Do we check for x in our conditions or do we check for it in the route's handler?

Eldr's strives for clarity. There is only one place to place logic for a route, in the route's handler.

If you want a route to only be executed under certain conditions you check those conditions in the handler and then throw :pass if conditions match/do not match:

  get '/' do
    throw :pass if params['agent'] == 'secret'
    # respond here
  end

  # Executed only if we pass from the first route
  get '/' do
  end

Access Control

Originally Eldr had an access control DSL that looked like this:

access_control do
  allow :create, :registered, :admin
end

The DSL pulled a route's name and its roles into a before filter, then checked them against the current_user's roles. The filter itself was five lines.

I soon realized that this was redundant abstraction. The DSL didn't save me any coding, it merely gave the code pretty words. I was sacrificing clarity for poetry.

To protect a route you need a before filter that checks the user for roles. It can look like this:

before :create, :edit do
  unless current_user.has_role? :admin
    raise NotAuthorized, "You are not authorized to do this!"
  end
end

It is just as short as any DSL and ambiguity free. Any ruby developer can guess what it means without having to know your framework's secret language.

Inheritance

In many frameworks you are encouraged to wrap your controllers in a central app. You build a main app that handles routing, scoping, sharing configuration and middleware etc; often controllers are merely blocks executed in the context of this app. This allows you to build DRY controllers but at the cost of a large central app.

In Eldr we reverse this model and encourage you to build up your controllers through inheritance. Eldr apps can inherit middleware and configuration from parent apps. This allows you to define a base app and allow all your controllers to share the same configuration.

For example:

class SimpleCounterMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    env['eldr.simple_counter'] ||= 0
    env['eldr.simple_counter'] += 1
    @app.call(env)
  end
end

class BaseApp < Eldr::App
  use SimpleCounterMiddleware
  set :bob, 'what about him?'
end

class InheritedApp < BaseApp
  get '/', proc { [200, {}, [env['eldr.simple_counter']]]}
end

run InheritedApp

Extensions

Extending a framework is a mystical process full of esoteric patterns and confusing APIs. Engines, helper blocks, plugin apis, and if you are lucky yaml configuration.

You can extend Eldr in two ways;

  1. Through standard Ruby patterns
  2. Through standard Rack patterns

Extending using Ruby patterns

Ruby patterns are something you are already intimately familiar with. Include, extend, ducks, blocks, procs etc are something you know like the front of your MacBook (btw you should really clean that smudge). Eldr is a DSL you already know how to speak.

I have designed Edlr to work well with common ruby patterns; you can include it, extend it, pass it blocks etc, and it wont blow up. If it does blow up its a bug.

You can add methods to your app (i.e helpers) by including them:

class App < Eldr::App
  include Helpers
end

If you want to use an app as a block you can:

run App.new do
  get '/' { Rack::Response.new "Hello World" }
end

Flexibility in Eldr is not accomplished through abstraction but through clarity. You can make Eldr do what you want it to because you will understand it not because it provides every feature.

Extending using Rack

Rails has a powerful code reuse model. You can generalize routes, controllers, even assets and views, then share them. Authentication/Authorization, shopping carts, social engines, admin panels, all abound in the rails world. This reuse of code allows one to build complex large apps that once took months, in a matter of minutes.

In Eldr you re-use applications the Rack way. You will architect your app so it can be mounted as a Rack app, then an end-developer can mount it or extend it using Rack tools.

For example, imagine a user registration/authentication app, something like Devise.

First we create our app class:

class EldrWise
  ###
  # Registrations
  ###

  # Register a new user
  post '/user' do
    # ... user creation stuff
    redirect_to "/users/#{user.id}"
  end

  # ...  more code

  ###
  # Users
  ###
  get '/users/:id' do
    render 'users/show'
  end
end

We would probably want to break this up into separate controllers:

class EldrWise::Registrations
  # ... registration routes
end

class EldrWise::Users
  # ... user routes
end

module EldrWise
  map '/registration' do
    Registrations
  end

  map '/users' do
    Users
  end
end

Then an end user can either mount the entire thing:

App.extend EldrWise
run App

Or just a part:

map '/users' do
  EldrWise::Users
end
run App

To customize it they can either extend it:

class UsersController < EldrWise::Users
end

map '/users' do
  UsersController
end

If we want to define all our controllers on the root we can use Eldr::Cascade. We define the routes we want to override, Cascade will get a 404 on the ones we didn't, then call the next app until it gets a response:

require 'eldr'
require 'eldr/cascade'
class App
  # override the users post route but nothing else
  post '/users' do
  end
end
run Eldr::Cascade.new[App, EldrWise]

Thinking about Rack when you construct your Eldr apps will make them flexible and easy to reuse.

Using Rack inside Eldr

Eldr apps are an instance of Rack::Builder; this means we can use any middleware or other Rack apps (including other Eldr apps) inside an app. To use Rack::Builder features you call the methods on the class.

For example, to use Rack session cookies we do:

class App < Eldr::App
  use Rack::Session::Cookie
end

If we wanted to mount other Eldr apps/controllers in our app we would do the following:

class App < Eldr::App
  map '/users' do
    UsersController
  end
end

Because rack builder itself can take rack builder instances we can run our App in config.ru just like we expect:

class App < Eldr::App
  use Rack::Session::Cookie
end

run App

Redirecting Things

Redirects are an enormously complex subject involving thousands of lines of helper code. Just kidding, they are just a status code and a new path. We can create a redirection helper in 5 lines:

module Helpers
  def redirect(path, message)
    [302, {'location' => path}, [message]]
  end
end

Route Handlers: The Power of Action Objects

In Edlr (just like in Rack) route handlers are things that respond to call. This means we can use everthing from procs, to blocks, to instances of a class.

If we wanted to we could do the following:

class Handler
  def call(env)
    Rack::Response.new "Hi there Dave!"
  end
end

class App
  get '/', Handler.new
end

This isn't as silly as it might seem. Its a common and powerful pattern that can clean up complex controllers.

Imagine we have a complex show action that takes up hundreds of lines. Our first inclination will be to split this up into different methods on our controller. This would make our show action a lot shorter; but we would have logic for our show action spread across our controller. Our other actions don't need to be able to access the show action's logic. It would be better to have all the logic for the show action in one object.

We can do this by writing our show action as an Action Object. Our action's logic will be contained entirely in this object.

Action objects give us the freedom to instantiatie them, inject things, pass them around, test and generally just engage in whatever sociopathic whimsys we might want to do to them.

Let's take a look at that show action:

class Show
  attr_accessor :env

  def helper_logic
    # do things here
  end

  def params
    env['eldr.params']
  end

  def call(env)
    @env = env

    helper_logic
    # @cat = Cat.find params[:id]
    Rack::Response.new "Found cat named #{params['name'].capitalize}!"
  end
end

class CatsController
  get '/cats/:name', Show.new
end

run CatsController

Once you start using this pattern you cant stop. Like nutella, you start putting it on everything, trying it on absurd things you know are wrong -- just in case it might work well.

Don't overuse action objects, but remember they are there when you want deliciousness.

Testing Eldr Apps

You are ready to set out on your own but you are afraid of breaking all the things. You need some safety checks to keep you from hurting yourself or your apps. Testing Eldr apps is the same as testing rack apps.

In your spec helper include rack-test. With rspec that would look like this:

RSpec.configure do |config|
  config.include Rack::Test::Methods
end

Now you need to define your app method so rack-test can mount your app. Eldr apps are rack apps so we can simply return a new instance of it:

def app
  YourEldrApp.new
end

If you want to test each of your apps individually you can use let to create a new Rack::Test session thingy:

let(:rt) do
  Rack::Test::Session.new YourEldrApp.new
end

Then you'll be able to access the rack-test methods from rt. For example:

response = rt.get '/'
response.status.should == 200

Rack apps sure are easy to work with!

See the spec/ in this repo for some specific rspec examples.

Performance

Eldr has been built with perfomance in mind. Right now it performs in the middle of the pack and more performance improvements are forthcoming:

FrameworkRequests/sec% from best
rack10282.20100.0%
hobbit8853.8186.11%
roda8830.0085.88%
cuba8517.0082.83%
lotus-router8464.8182.32%
rack-response7939.6777.22%
brooklyn7639.8874.3%
eldr7170.1569.73%
rambutan6954.7567.64%
nancy6516.8463.38%
gin3850.0037.44%
nyny3745.9636.43%
sinatra2740.5026.65%
rails2475.5024.08%
scorched1692.1816.46%
ramaze1490.4214.5%

See the bench-micro repo to run your own perfomance benchmarks

Help/Resources

You can get help from the following places:

Contributing

  1. Fork. it
  2. Create. your feature branch (git checkout -b cat-evolver)
  3. Commit. your changes (git commit -am 'Add Cat Evolution')
  4. Test. your changes (always be testing)
  5. Push. to the branch (git push origin cat-evolver)
  6. Pull. Request. (for extra points include funny gif and or pun in comments)

To remember this you can use the easy to remember and totally not tongue-in-check initialism: FCCTPP.

I don't want any of these steps to scare you off. If you don't know how to do something or are struggle getting it to work feel free to create a pull request or issue anyway. I'll be happy to help you get your contributions up to code and into the repo!

License

Licensed under MIT by K-2052.