Ben Scofield gave a talk at Rubyconf about building his own web framework called Athena from scratch. He discussed how starting small with a "Hello World" program and building up from there helped him learn about aspects of web development like RESTful design, routing, ORM/database integration, and exception handling in Ruby. He concluded by noting that there is always more to learn, and shared his GitHub page where the framework code can be found.
10. main() {
printf(quot;hello, worldnquot;);
}
-module(hello).
-export([hello/0]).
hello() ->
io:format(quot;Hello World!~nquot;, []).
PROGRAM HELLO
PRINT*, 'Hello World!'
END
11. main = putStrLn quot;Hello Worldquot;
Imports System.Console
Class HelloWorld
Public Shared Sub Main()
WriteLine(quot;Hello, world!quot;)
End Sub
End Class
!greeting.
+!greeting : true <- .print(quot;Hello Worldquot;).
51. puts quot;Starting Athena applicationquot;
require 'active_record'
require File.expand_path(File.join('./vendor/athena/lib/athena'))
puts quot;... Framework loadedquot;
Athena.require_all_libs_relative_to('resources')
puts quot;... Resources loadedquot;
use Rack::Static, :urls => ['/images', '/stylesheets'], :root => 'public'
run Athena::Application.new
puts quot;... Application startednnquot;
puts quot;^C to stop the applicationquot;
52. 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
53. 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 get; raise Athena::MethodNotAllowed; end
def put; raise Athena::MethodNotAllowed; end
def post; raise Athena::MethodNotAllowed; end
def delete; raise Athena::MethodNotAllowed; end
# ...
end
end
54. 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
55. 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
56. 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 quot;set_table_name '#{self.name}'.tableizequot;
eval quot;Persistent#{self.name} = pklassquot;
@@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
# ...
63. 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
Form Parameters
end
64. 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 quot;set_table_name '#{self.name}'.tableizequot;
eval quot;Persistent#{self.name} = pklassquot;
@@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
66. 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