Building Your Framework

Checkout the finished framework at eldr-rb/eldr

First let's create a Gemfile:

source 'https://rubygems.org'
ruby '2.1.5'
gemspec

This sets a source, a ruby version, and tells bundler to look in the gemspec file for dependencies:

In the real world you would start a gem by running:

$ bundle gem

And then bundler would generate a bunch of files for you. But this is a book and not the real world. Lookout Atreyu!

Now create a gemspec called eldr.gemspec:

# -*- encoding: utf-8 -*-
require File.expand_path('../lib/eldr/version', __FILE__)

Gem::Specification.new do |s|
  s.name        = 'eldr'
  s.version     = Eldr::VERSION
  s.platform    = Gem::Platform::RUBY
  s.authors     = ['K-2052']
  s.email       = ['k@2052.me']
  s.homepage    = 'https://github.com/eldr-rb/eldr'
  s.summary     = 'A minimal framework that lights a fire in your development'
  s.description = 'A minimal framework that lights a fire in your development.
                    Built close to Rack and the metal.'
  s.licenses    = ['MIT']

  s.required_rubygems_version = '~> 2.0'
  s.required_ruby_version     = '~> 2.0'
  s.rubyforge_project         = 'eldr'

  s.add_dependency 'rack',       '~> 1.5'
  s.add_dependency 'mustermann', '0.4.0'
  s.add_dependency 'fast_blank', '0.0.2'

  s.add_development_dependency 'bundler',   '~> 1.7'
  s.add_development_dependency 'rake',      '10.4.2'
  s.add_development_dependency 'rspec',     '3.1.0'
  s.add_development_dependency 'rubocop',   '0.28.0'
  s.add_development_dependency 'rack-test', '~> 0.6'

  s.files        = `git ls-files`.split("\n")
  s.executables  = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
  s.require_path = 'lib'
end

We depend on; Mustermann for route matching and fast_blank for blank? support. We will be using rspec and rack-test for testing. Rubocop provides style guideline checking.

Eldr is Norse for fire and not a web 2.0 version of elder.

Our gemspec loads a /lib/eldr/version file, let's add one:

module Eldr
  VERSION = '0.0.1'
end

Then add a .rspec file:

--color
--format progress

Now run:

$ bundle install

And then:

$ bundle exec rspec

If nothing explodes then you are good to go!

Basic Architecture

The first step in creating a Rack framework is defining a call method for Rack to hit. Let's create a new file lib/eldr/app.rb with a call method:

module Eldr
  class App
    def self.call(env)
    end
  end
end

We want our controllers to be instances but we don't want a user to have to initialize them. To do this, we can have .call() return a new instance and then call #call() on that instance:

def self.call(env)
  self.new.call(env)
end

Then we need a #call() method:

def call(env)
end

This has an unforeseen problem, if we were to set an instance variable and then use it again, we could potentially return data from a previous instance. We might end up giving one user another's data!

The solution for this is to duplicate the instance for every request:

def call(env)
  dup.call!(env)
end

# The real call method
def call!(env)
end

This might seem like it would have a performance hit but dup is rather efficient; the stray objects are garbage collected long before they become a memory issue.

We need to load our framework from our specs. Let's add a file called lib/eldr.rb:

require_relative 'eldr/version'
require_relative 'eldr/app'

And then a spec/spec_helper.rb that loads it:

require 'rack/test'
require 'rack'
require_relative '../lib/eldr'

# Hardcode an instance into a global because rack-test likes to get too clever
RSpec.configure do |config|
  config.include Rack::Test::Methods
  config.pattern = '**{,/*/**}/*_spec.rb'
end

Finally add --require ./spec/spec_helper to .rspec

Then test that is working by running:

$ bundle exec rspec

Rack can interface with this app class but what we really want is for our app class to be a Rack interface. We want to be able to add middleware, mix other apps into our app, inherit from other apps etc. We can get all this by turning our app class into a Rack::Builder stack.

We will add an instance of Rack::Builder to our class, delegate its methods to our app class, then in the call method we will pass an instance of the app into the builder instance.

Turning our app class into a Rack Stack

First let's add an accessor for builder and delegate the map and use methods to it:

require 'forwardable'
module Eldr
  class App
    class << self
      extend Forwardable
      attr_accessor :builder
      def_delegators :builder, :map, :use

      def builder
        @builder ||= Rack::Builder.new
      end
    end
  end
end

Now in our .new() method let's pass an instance of the app's class to the builder stack, then return the builder stack:

alias_method :_new, :new
def new(*args, &block)
  builder.run _new(*args, &block)
  builder
end

We can now treat our app like we would a Rack::Builder stack. Let's test this out; create a new file called examples/stack_app_example.ru:

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 OtherApp < Eldr::App
  def call!(env)
    [200, {}, ['Other App']]
  end
end

class StackAppExample < Eldr::App
  use SimpleCounterMiddleware
  map '/other' do
    run OtherApp
  end

  def call!(env)
    [200, {}, [env['eldr.simple_counter']]]
  end
end

run StackAppExample

Let's add a spec (spec/examples/stack_app_example_spec.rb) to test that this works:

describe 'StackAppExample' do
  let(:app) do
    path = File.expand_path('../../examples/stack_app_example.ru', File.dirname(__FILE__))
    Rack::Builder.parse_file(path).first
  end

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

  describe 'GET /other' do
    it 'returns Other App' do
      response = rt.get('/other')
      expect(response.body).to eq 'Other App'
    end
  end

  describe 'GET /' do
    it 'returns the counter' do
      response = rt.get('/')
      expect(response.body).to eq '2'
    end
  end
end

This spec was our first encounter with Rack::Builder.parse_file. If you want to test a rack app defined in a rackup file, this is how you get at it. parse_file loads the file, wraps it Rack::Builder, evals it in ruby and then returns an array of our defined apps.

We need to make sure we carry any middleware with us during inheritance. Rack::Builder doesn't take an array of existing middleware, so we will need to extend it.

Create a new file lib/eldr/builder.rb with the following:

    require 'rack/builder'

    module Eldr
      class Builder < Rack::Builder
        attr_accessor :middleware
        def initialize(default_app = nil, &block)
          @middleware, @map, @run, @warmup = [], nil, default_app, nil
          instance_eval(&block) if block_given?
        end

        def self.app(default_app = nil, &block)
          new(default_app, &block).to_app
        end

        def use(middleware, *args, &block)
          if middleware.is_a? Array
            @middleware.push(*middleware)
          else
            if @map
              mapping, @map = @map, nil
              @middleware << proc { |app| generate_map app, mapping }
            end
            @middleware << proc { |app| middleware.new(app, *args, &block) }
      end
    end

    def run(app) # rubocop:disable Style/TrivialAccessors
      @run = app
    end

    def warmup(prc = nil, &block)
      @warmup = prc || block
    end

    def map(path, &block)
      @map ||= {}
      @map[path] = block
    end

    def to_app
      app = @map ? generate_map(@run, @map) : @run
      fail 'missing run or map statement' unless app
      app = @middleware.reverse.inject(app) { |a, e| e[a] }
      @warmup.call(app) if @warmup
      app
    end

    def call(env)
      to_app.call(env)
    end

    private

    def generate_map(default_app, mapping)
      mapped = default_app ? { '/' => default_app } : {}
      mapping.each { |r, b| mapped[r] = self.class.new(default_app, &b).to_app }
      Rack::URLMap.new(mapped)
    end
  end
end

Let's add a spec (spec/builder_spec.rb) for this new Builder class:

describe Eldr::Builder do
  describe '#use' do
    it 'supports passing an array of middleware' do
      builder = Eldr::Builder.new
      builder.use Class.new
      builder_2 = Eldr::Builder.new
      builder_2.use builder.middleware
      expect(builder_2.middleware).to eq(builder.middleware)
    end
  end
end

Now in app.rb we need to require it and use it in place of Rack::Builder:

require_relative 'builder'

def builder
  @builder ||= Builder.new
end

To add middleware to an inherited app class we can use the inherited callback:

def inherited(base)
  base.builder.use builder.middleware
end

An inherited callback is part of ruby's core.

We can use it (examples/inheritance.ru) like this:

class SimpleMiddleware
  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 TestInheritanceApp < Eldr::App
  use SimpleMiddleware
end

class InheritedApp < TestInheritanceApp
  def call!(env)
    [200, {}, [env['eldr.simple_counter']]]
  end
end

run InheritedApp

Let's add a spec (specs/examples/inheritance_spec.rb) for this:

describe 'InheritedApp' do
  let(:app) do
    path = File.expand_path("../../examples/inheritance.ru", File.dirname(__FILE__))
    Rack::Builder.parse_file(path).first
  end

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

  describe 'GET /' do
    it 'counts using counter middleware' do
      response = rt.get '/'
      expect(response.body).to eq('1')
    end
  end
end

That does it for our stack!

Routing

A router consists of two parts; a matcher and a recognizer. The matcher returns true or false when a path matches a route. The recognizer finds all the routes that match. A matcher is typically part of the Route object. The recognizer is often part of the App's class. For clarity and simplicity, our matcher and recognizer will be independent objects.

Matcher

A matcher at its core is a regular expression. To generate regular expressions from our route paths we will make use of Mustermann. Mustermann is a string matching library for route strings. It supports a wide range of styles; Sinatra, Rails, CakePHP and more.

An api for a matcher object is very simple. It takes a path and supplies a match method:

require 'mustermann'

module Eldr
  class Matcher
    def initialize(path, options = {})
      @path    = path
      @capture = options.delete(:capture)
    end

    def match(pattern)
      handler.match(pattern)
    end

    def handler
      @handler ||=
      case @path
      when String
        Mustermann.new(@path, capture: @capture)
      when Regexp
        /^(?:#{@path})$/
      end
    end
  end
end

Our matcher takes a path and a capture hash for parameters. When we want to match a path we simply call match against the object. Like this:

matcher = Eldr::Matcher.new('/cats/:id')
matcher.match('/cats/bob')

Let's add a spec for our matcher to spec/matcher_spec.rb:

describe Eldr::Matcher do
  describe '#match' do
    let(:matcher) { Eldr::Matcher.new('/cats/:id') }

    context 'when pattern matches' do
      it 'returns MatchData' do
        expect(matcher.match('/cats/bob')).to be_instance_of MatchData
      end

      it 'returns the splats' do
        expect(matcher.match('/cats/bob')[:id]).to eq('bob')
      end
    end

    context 'when pattern does not match' do
      it 'returns nil' do
        expect(matcher.match('/dogs/bob')).to be_nil
      end
    end
  end
end

Route

Before we build something to recognize routes we have to build the route object itself. Our route will need a path, a http verb, and a handler:

class Route
  def initialize(verb: 'GET', path: '/', name: nil, handler: nil)
    @path, @verb, @name, @handler = path, verb.to_s.upcase, name, handler
  end

  def matcher
    @matcher ||= Matcher.new(@path, capture: @capture)
  end

  def match(pattern)
    matcher.match(pattern)
  end
end

Let's add a spec (spec/route_spec.rb) to make sure this works:

describe Eldr::Route do
  describe '.new' do
    it 'returns a new instance' do
      expect(Eldr::Route.new).to be_instance_of Eldr::Route
    end
  end

  describe '#match' do
    let(:route) { Eldr::Route.new(path: '/cats/:id') }

    context 'when the path matches' do
      it 'returns MatchData' do
        expect(route.match('/cats/bob')).to be_instance_of MatchData
      end

      it 'returns the splats' do
        expect(route.match('/cats/bob')[:id]).to eq('bob')
      end
    end

    context 'when the path does not match' do
      it 'returns nil' do
        expect(route.match('/dogs/bob')).to be_nil
      end
    end
  end
end

Recognizing

A recognizer is simple, it loops over the routes and returns the ones that match a given path. Let's build one:

    require 'fast_blank'

    module Eldr
  class Recognizer
    class NotFound < StandardError
      def call(_env)
        [404, {}, ['']]
      end
    end

    attr_accessor :env, :routes, :pattern

    def initialize(routes)
      @routes = routes
    end

    def call(env)
      @env = env

      ret_routes = routes[verb].select { |route| !route.match(pattern).nil? }

      ## If no routes are found then raise an eror
      raise NotFound if ret_routes.empty?

      ret_routes
    end

    def pattern
      @pattern = env['PATH_INFO']
      @pattern = '/' if @pattern.blank?
      @pattern
    end

    def verb
      env['REQUEST_METHOD'].downcase.to_sym
    end
  end
end

The recongizer takes its details from an env hash, so it can be used directly with a rack env. The pattern we match routes against and the http verb both come from the env hash.

We can use the recognizer like this:

routes = { 'GET' => [Eldr::Route.new(path: '/cats/:id'), Eldr::Route.new(path: '/dogs/:id')] }
recognizer = Eldr::Recognizer.new(routes)

env = { 'PATH_INFO' => '/cats/bob', 'REQUEST_METHOD' => 'GET' }
matched_routes = recognizer.call(env)

Let's add a spec for the recognizer to spec/recognizer_spec.rb:

describe Eldr::Recognizer do
  describe '#call' do
    let(:routes) { {get: [Eldr::Route.new(path: '/cats')] } }
    let(:recognizer) { Eldr::Recognizer.new(routes) }

    context 'when there are matching routes' do
      let(:env) do
        Rack::MockRequest.env_for('/cats', {:method => :get})
      end

      it 'returns matching routes' do
        expect(recognizer.call(env).length).to eq(1)
      end
    end

    context 'when there are no matching routes' do
      let(:env) do
        Rack::MockRequest.env_for('/no-route-for-me', {:method => :get})
      end

      it 'raises a NotFound error' do
        expect { recognizer.call(env) }.to raise_error
      end
    end
  end
end

Route Handling

Now we need to build our route handler. Route handlers are simple, they have a call method that takes an env hash and responds with a valid rack response. We will wrap the route's handler in a route object. The app class will call the route and then the route will call the handler:

class Route
  def call(env)
    @env = env

    @handler.call(env)
  end
end

Simple and effective...

Let's add a spec for #call to spec/route_spec.rb:

describe Eldr::Route do
  describe '#call' do
    let(:route) do
      Eldr::Route.new(handler: proc { 'cats' })
    end

    it 'returns cats' do
      expect(route.call({})).to eq('cats')
    end
  end
end

Rails Style Handlers

Rails uses handlers that consist of a controller name and a method name. They look like this:

to: 'Cats#show'

This is easy to support. If the handler is a string then we split it into a controller and method name. initialize an instance of the controller, and then call the method:

def initialize
  # ... other code here
  @handler = create_handler(handler)
end

def create_handler(handler)
  return handler if handler.respond_to? :call

  if handler.is_a? String
    controller, method = handler.split('#')

    proc { |env|
      obj = Object.const_get(controller).new
      obj.send(method.to_sym, env)
    }
  end
end

Let's add a spec for this:

describe Eldr::Route do
  describe '#call' do
    class Cats
      def show(env)
        'cats'
      end
    end

    let(:route) { Eldr::Route.new(handler: 'Cats#show') }

    it 'returns cats' do
      expect(route.call({})).to eq('cats')
    end
  end
end

Putting it Together

Now that we have a Matcher, a Recognizer, and a Route; we can add routing methods to the App class. The first thing we need is an .add() method for pushing a new Route onto .routes:

module Eldr
  class App
    class << self
      attr_accessor :routes
      def routes
        @routes ||= { delete: [], get: [], head: [], options: [], patch: [], post: [], put: [] }
      end

      def add(verb: :get, path: '/', handler: nil)
        handler = Proc.new if block_given?
        route =  Route.new(verb: verb, path: path, handler: handler)
        routes[verb] << route
        route
      end
      alias_method :<<, :add
    end
  end
end

Then we can use define_method for the get, post, delete methods etc:

class << self
  HTTP_VERBS = %w(DELETE GET HEAD OPTIONS PATCH POST PUT)
  HTTP_VERBS.each do |verb|
    define_method verb.downcase.to_sym do |path, *args, &block|
      handler = Proc.new(&block) if block
      handler ||= args.pop if args.last.respond_to?(:call)
      options ||= args.pop if args.last.is_a?(Hash)
      options ||= {}
      add({verb: verb.downcase.to_sym, path: path, handler: handler}.merge!(options))
    end
  end
end

Let's add a recognize method for recognizing routes:

def self.recognizer
  @recognizer ||= Recognizer.new(routes)
end

def recognize(env)
  self.class.recognizer.call(env)
end

Now modify our call! method to run recognize and call the handlers on the recognized routes:

def call!(env)
  @env = env

  recognize(env).each do |route|
    env['eldr.route'] = route
    catch(:pass) { return route.call(env) }
  end
rescue => error
  if error.respond_to? :call
    error.call(env)
  else
    raise error
  end
end

We use it like this:

class HelloWorld < Eldr::App
  get '/' do
    [200, {}, ['Hello World!']]
  end
end

run HelloWorld

Let's test that all is working by adding a spec to spec/examples/hello_world_spec.rb:

describe 'HelloWorldExample' do
  let(:app) do
    path = File.expand_path("../../examples/hello_world.ru", File.dirname(__FILE__))
    Rack::Builder.parse_file(path).first
  end

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

  describe 'GET /' do
    it 'returns hello world' do
      response = rt.get '/'
      expect(response.body).to eq('Hello World!')
    end
  end
end

Filters

Almost every framework has a concept of filters; code run before or after a route's handler. They look like this:

before :index do
  # do stuff before
end

get '/', name: :index, do
end

The best place for filters is on the route itself. The we can call them in the same #call method that calls the handler.

First let's add an array for holding them:

class Route
  attr_accessor :before_filters, :after_filters

  def initialize
    @before_filters, @after_filters = [], []
  end
end

Then modify the #call method to run the filters:

class Route
  def call(env)
    @env = env

    before_filters.each { |filter| filter.call(env) }
    response = @handler.call(env)
    after_filters.each { |filter| filter.call(env) }
    response
  end
end

Let's add a spec for this to spec/route_spec.rb:

describe '#call' do
  context 'when there are before filters' do
    let(:route) do
      Eldr::Route.new(handler: proc { 'cats' })
    end

    it 'calls the before filters' do
      called = false
      route.before_filters << proc { called = true }
      route.call({})
      expect(called).to eq(true)
    end
  end

  context 'when there are after filters' do
    let(:route) do
      Eldr::Route.new(handler: proc { 'cats' })
    end

    it 'calls the before filters' do
      called = false
      route.after_filters << proc { called = true }
      route.call({})
      expect(called).to eq(true)
    end
  end
end

Now we can add before/after methods to our app class:

class App
  class << self
    attr_accessor :before_filters, :after_filters

    def before_filters
      @before_filters ||= { all: [] }
    end

    def after_filters
      @after_filters ||= { all: [] }
    end

    def before(*route_names, &block)
      *route_names = [:all] if route_names.empty?
      route_names.each do |route_name|
        before_filters[route_name] ||= []
        before_filters[route_name] << block
      end
    end

    def after(*route_names, &block)
      *route_names = [:all] if route_names.empty?
      route_names.each do |route_name|
        after_filters[route_name] ||= []
        after_filters[route_name] << block
      end
    end
  end
end

These methods take a series of route names and then a block. If no route names are given then we default to :all. When we create a route we only need to pass these filters to it:

def add(verb: :get, path: '/', name: '', handler: nil)
  handler = Proc.new if block_given?
  route =  Route.new(verb: verb, path: path, name: name, handler: handler)

  route.before_filters = before_filters[name] if before_filters.include? name
  route.after_filters  = before_filters[name] if before_filters.include? name

  route.before_filters.push(*before_filters[:all])
  route.after_filters.push(*before_filters[:all])

  routes[verb] << route
  route
end
alias_method :<<, :add

We can use it like this:

class FiltersApp < Eldr::App
  before :bob do
    puts 'before'
  end

  after :bob do
    puts 'after'
  end

  get '/', name: :bob, do
    [200, {}, ['Hello World']]
  end
end

Now modify our app spec (spec/app_spec.rb) to make sure are before/after methods work:

describe '.before' do
  it 'adds a before filter' do
    bob = Class.new(Eldr::App)
    bob.before {}
    bob.before(:cats) { }
    expect(bob.before_filters[:all].length).to eq(1)
    expect(bob.before_filters[:cats].length).to eq(1)
  end
end

describe '.after' do
  it 'adds an before filter' do
    bob = Class.new(Eldr::App)
    bob.after {}
    bob.after(:cats) { }
    expect(bob.after_filters[:all].length).to eq(1)
    expect(bob.after_filters[:cats].length).to eq(1)
  end
end

Instance Variables

Right now our handlers are called in their own context. They have no access to instance variables or methods from the app's instance. This makes it impossible do something like:

class Posts < Eldr::App
  before :show do
    @post = Post.find params['id']
  end

  get '/posts/:id', name: :show, do
    respond(@post)
  end
end

We can resolve this by passing an instance of the app to the route; if the app exists we will call filters and the handler in the context of the app. Let's do that:

    class Route
      attr_accessor :app, :handler

      def call(env, app: nil)
        @app ||= app

        call_before_filters(env)
        resp = call_handler(env)
        call_after_filters(env)

    resp
  end

  def call_before_filters(env)
    if app
      app.class.before_filters[:all].each { |filter| app.instance_exec(env, &filter) }
      before_filters.each                 { |filter| app.instance_exec(env, &filter) }
    else
      before_filters.each { |filter| filter.call(env) }
    end
  end

  def call_after_filters(env)
    if app
      app.class.after_filters[:all].each { |filter| app.instance_exec(env, &filter) }
      after_filters.each                 { |filter| app.instance_exec(env, &filter) }
    else
      after_filters.each { |filter| filter.call(env) }
    end
  end

  def call_handler(env)
    if app and handler.is_a? Proc
      app.instance_exec(env, &handler)
    else
      handler.call(env)
    end
  end
end

Now modify App#call! to pass the app instance:

def call!(env)
  @env = env

  recognize(env).each do |route|
    env['eldr.route'] = route
    catch(:pass) { return route.call(env, app: self) }
  end
  rescue => error
    if error.respond_to? :call
      error.call(env)
    else
      raise error
    end
end

Add an example app to examples/instance_vars.ru:

class InstanceVarsExample < Eldr::App
  before do
    @instance_var = 'cats'
  end

  get '/instance-var' do
    [200, {}, [@instance_var]]
  end
end

run InstanceVarsExample

And then a spec to spec/examples/instance_vars_spec.rb:

describe 'InstanceVarsExample' do
  let(:app) do
    path = File.expand_path('../../examples/instance_vars.ru', File.dirname(__FILE__))
    Rack::Builder.parse_file(path).first
  end

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

  describe 'GET /instance-var' do
    it 'returns cats' do
      response = rt.get('/instance-var')
      expect(response.body).to eq('cats')
    end
  end
end

Configuration

Every framework needs a way to set configuration values. A place for extensions to go and grab the values they need. Every config needs a home.

We need a configuration system that is flexible enough to take defaults and powerful enough to set values on the fly. It will look like this:

class App < Eldr::App
  set :session_id, 'cats'
end
expect(App.configuration.session_id).to eq 'cats'
expect(App.configuration.doesnt_exist).to eq nil

This flexibility is commonly accomplished with Hashie::Mash or OpenStruct, but both are rather slow. We can get everything we need by manually implementing it. It won't be as flexible and will explode under edge cases, but it will be adequate for all our purposes.

First let's define a class with a hash for storing the config:

module Eldr
  class Configuration
    attr_accessor :table

    def initialize
      defaults = { lock: false }
      table.merge!(defaults)
    end

    def merge!(hash)
      hash = hash.table unless hash.is_a? Hash
      @table.merge!(hash)
    end

    def set(key, value)
      @table[key] = value
    end

    def table
      @table ||= {}
    end
  end
end

Accessors and defaulting to nil can be accomplished by overriding method_missing:

def method_missing(method, *args)
  if !args.empty? # we assume we are setting something
    set(method.to_s.gsub(/\=$/, '').to_sym, args.pop)
  else
    # A hash accessor returns nil when there is existing element
    # No special code necessary for returing nil!
    @table[method.to_s.gsub(/\?$/, '').to_sym]
  end
end

Let's add a spec for this (spec/configuration_spec.rb):

describe Eldr::Configuration do
  describe '#initialize' do
    it 'sets defaults' do
      expect(Eldr::Configuration.new().lock).to eq(false)
    end
  end

  describe '#set' do
    let(:config) { Eldr::Configuration.new }

    it 'sets a configuration value in #table' do
      config.set(:bob, 'what about him?')
      expect(config.bob).to eq('what about him?')
    end
  end

  describe '#method_missing' do
    let(:config) { Eldr::Configuration.new }

    it 'returns a value from #table' do
      config.set(:bob, 'what about him?')
      expect(config.bob).to eq('what about him?')
    end

    it 'returns nil as a default' do
      expect(config.franklin).to eq(nil)
    end
  end
end

Now we need to add configuration to our app class:

class App
  attr_accessor :configuration

  class << self
    def configuration
      @configuration ||= Configuration.new
    end
    alias_method :config, :configuration

    def set(key, value)
      configuration.set(key, value)
    end

    def inherited(base)
      base.builder.use builder.middleware
      base.configuration.merge!(configuration)
    end
  end

  def initialize(configuration = nil)
    configuration ||= self.class.configuration
    @configuration = configuration
  end
end

Now let's add some spec (spec/app_spec.rb) to make sure all is working:

describe '.inherited' do
  it 'inherits configuration' do
    bob = Class.new(Eldr::App)
    bob.set(:bob, 'what about him?')

    inherited = Class.new(bob)
    expect(inherited.configuration.bob).to eq('what about him?')
  end
end

Congratulations you have built a basic framework! But what about helpers?!

Checkout the finished framework's repo at eldr-rb/eldr