All I Really Need to Know* I Learned by Writing My Own Web Framework

Ben Scofield
* about Ruby and the web 7 November 2008 Rubyconf

DSL

DSLs Intimidate and Frighten

flickr: cwsteeds

Rails

Rails

your custom framework

Starter Projects

Hello World

main() { printf("hello, world\n"); }

-module(hello). -export([hello/0]). hello() -> io:format("Hello World!~n", []).

PROGRAM HELLO PRINT*, 'Hello World!' END

main = putStrLn "Hello World" Imports System.Console Class HelloWorld Public Shared Sub Main() WriteLine("Hello, world!") End Sub End Class !greeting. +!greeting : true <- .print("Hello World").

Applications

To-do lists

DIY Blog

Frameworks?

PHP Framework

(based on Rails)

NOT FOR PRODUCTION!

well, maybe for production

NOT FOR PRODUCTION!

but really:

Frameworks

ActionMailer ActionPack ActiveRecord Ruby on Rails ActiveResource ActiveSupport Railties

request => response

Sequel ActiveRecord persistence layer DataMapper Og

ERB Liquid Amrita2 templating layer Erubis Markaby HAML

Ramaze Waves ActionPack the middle layer Merb Core Sinatra Camping

ActionMailer Merb Helpers utilities ActiveSupport Railties

Tools

Rack

CGI SCGI LSWS Mongrel WEBrick FastCGI Fuzed Thin Ebb

Mack Coset Camping Halcyon Maveric Sinatra Vintage Ramaze Waves Merb

mod_rack

Rack::Lint Rack::URLMap Rack::Deflater Rack::CommonLogger middlewares Rack::ShowExceptions Rack::Reloader Rack::Static Rack::Cache

Rack::Request utility Rack::Response

Other Layers

Sequel ActiveRecord persistence layer DataMapper Og

ERB Liquid Amrita2 templating layer Erubis Markaby HAML

Start at the End

Vision

REST and Resources

fully-formed web applications built on resources

fully-formed

web applications

built on resources

Birth of Athena

Dionysus
ask me after if you don’t get the joke

Project

Extraction
flickr: 95579828@N00

class Habit < Athena::Resource def get @habit = Habit.find(@id) end # ... end

RouteMap = { :get => { /\/habit\/new/ => {:resource => :habit, :template => :new}, /\/habit\/(\d+)/ => {:resource => :habit}, /\/habit\/(\d+)\/edit/ => {:resource => :habit, :template => :edit} }, :put => { /\/habit\/(\d+)/ => {:resource => :habit} }, :delete => { /\/habit\/(\d+)/ => {:resource => :habit} }, :post => { /\/habit\// => {:resource => :habit} } }

Results

puts "Starting Athena application" require 'active_record' require File.expand_path(File.join('./vendor/athena/lib/athena')) puts "... Framework loaded" Athena.require_all_libs_relative_to('resources') puts "... Resources loaded" use Rack::Static, :urls => ['/images', '/stylesheets'], :root => 'public' run Athena::Application.new puts "... Application started\n\n" puts "^C to stop the application"

module Athena class Application def self.root File.join(File.dirname(__FILE__), '..', '..', '..', '..') end def self.route_map @route_map ||= { :get => {}, :post => {}, :put => {}, :delete => {} } @route_map end def call(environment) request = Rack::Request.new(environment) request_method = request.params['_method'] ? # ... matching_route = Athena::Application.route_map # ... if matching_route resource = matching_route.last[:class].new(request, request_method) resource.template = matching_route.last[:template] return resource.output else raise Athena::BadRequest end end end end

module Athena class Resource extend Athena::Persistence attr_accessor :template def self.inherited(f) unless f == Athena::SingletonResource url_name = f.name.downcase routing = url_name + id_pattern url_pattern = /^\/#{routing}$/ Athena::Application.route_map[:get][/^\/#{routing}\/edit$/] = # ... Athena::Application.route_map[:post][/^\/#{url_name}$/] = # ... # ... end end def self.default_resource Athena::Application.route_map[:get][/^\/$/] = {:class => # ... end def self.id_pattern '\/(\d+)' end def def def def get; put; post; delete; raise raise raise raise Athena::MethodNotAllowed; Athena::MethodNotAllowed; Athena::MethodNotAllowed; Athena::MethodNotAllowed; end end end end

# ... end end

class Habit < Athena::Resource persist(ActiveRecord::Base) do validates_presence_of :name end def get @habit = Habit.find_by_id(@id) || Habit.new_record end def post @habit = Habit.new_record(@params['habit']) @habit.save! end def put @habit = Habit.find(@id) @habit.update_attributes!(@params['habit']) end def delete Habit.find(@id).destroy end end

class Habits < Athena::SingletonResource default_resource def get @habits = Habit.all end def post @results = @params['habits'].map do |hash| Habit.new_record(hash).save end end def put @results = @params['habits'].map do |id, hash| Habit.find(id).update_attributes(hash) end end def delete Habit.find(:all, :conditions => ['id IN (?)', @params['habit_ids']] ).map(&:destroy) end end

module Athena module Persistence def self.included(base) base.class_eval do @@persistence_class = nil end end def persist(klass, &block) pklass = Class.new(klass) pklass.class_eval(&block) pklass.class_eval "set_table_name '#{self.name}'.tableize" eval "Persistent#{self.name} = pklass" @@persistence_class = pklass @@persistence_class.establish_connection( YAML::load(IO.read(File.join(Athena::Application.root, # ... ) end def new_record(*args) self.persistence_class.new(*args) end def persistence_class @@persistence_class end # ...

Lessons About the Web

HTTP status codes
http://thoughtpad.net/alan-dean/http-headers-status.gif

207
Multi-Status

207 sucks
Multi-Status

rack::cache
http://tomayko.com/src/rack-cache/

Lessons About Ruby

module Athena class Application # ... def process_params(params) nested_pattern = /^(.+?)\[(.*\])/ processed = {} params.each do |k, v| if k =~ nested_pattern scanned = k.scan(nested_pattern).flatten first = scanned.first last = scanned.last.sub(/\]/, '') if last == '' processed[first] ||= [] processed[first] << v else processed[first] ||= {} processed[first][last] = v processed[first] = process_params(processed[first]) end else processed[k] = v end end processed.delete('_method') processed end # ... end end

Form Parameters

module Athena module Persistence def self.included(base) base.class_eval do @@persistence_class = nil end end def persist(klass, &block) pklass = Class.new(klass) pklass.class_eval(&block) pklass.class_eval "set_table_name '#{self.name}'.tableize" eval "Persistent#{self.name} = pklass" @@persistence_class = pklass @@persistence_class.establish_connection( YAML::load(IO.read(File.join(Athena::Application.root, # ... ) end def new_record(*args) self.persistence_class.new(*args) end def persistence_class @@persistence_class end

Dynamic class creation

# ...

Tangled classes
flickr: randomurl

module Athena module Persistence # ... def method_missing(name, *args) return self.persistence_class.send(name, *args) rescue ActiveRecord::ActiveRecordError => e raise e rescue super end end end

Exception Propagation

this session has been a LIE

More to learn
flickr: glynnis

... always
flickr: mointrigue

http://github.com/bscofield/athena
(under construction)

Thank you!
Questions?
Ben Scofield
Development Director, Viget Labs http://www.viget.com/extend | http://www.culann.com ben.scofield@viget.com | bscofield@gmail.com

Sign up to vote on this title
UsefulNot useful