How to Build Sinatra on Rails 3

本文介绍如何使用Rails框架实现Sinatra DSL的一个子集,包括路由、会话支持及回调等功能。通过具体示例展示了如何定义路由、处理请求及设置会话。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

n Ruby, we have the great fortune to have one major framework (Rails) and a number of minor frameworks that drive innovation forward. One of the great minor frameworks which has been getting a lot of traction recently is Sinatra, primarily because it exposes a great DSL for writing small, single-purpose apps.

Here’s an example of a simple Sinatra application.

class MyApp < Sinatra::Base
set :views, File.dirname(__FILE__)
enable :sessions

before do
halt if session[:fail] == true
end

get "/hello" do
"Hello world"
end

get "/world" do
@name = "Carl"
erb :awesomesauce
end

get "/fail" do
session[:fail] = true
"You failed"
end
end
There’s a lot of functionality packed into this little package. You can declare some code to be run before all actions, declare actions and the URL they should be routed from, use rendering semantics, and even use sessions.

We’ve been saying that Rails 3 is flexible enough to use as a framework toolkit–let’s prove it by using Rails to build the subset of the Sinatra DSL described above.

Let’s start with a very tiny subset of the DSL:

class MyApp < Sinatra::Base
get "/hello" do
"HELLO World"
end

post "/world" do
"Hello WORLD"
end
end
The first step is to declare the Sinatra base class:

module Sinatra
class Base < ActionController::Metal
include ActionController::RackConvenience
end
end
We start off by making Sinatra::Base a subclass of the bare metal ActionController implementation, which provides just enough infrastructure to get going. We also include the RackConvenience module, which provides request and response and handles some basic Rack tasks for us.

Next, let’s add support for the GET and POST method:

class Sinatra::Base
def self.inherited(klass)
klass.class_eval { @_routes = [] }
end

class << self
def get(uri, options = {}, &block) route(:get, uri, options, &block) end
def post(uri, options = {}, &block) route(:post, uri, options, &block) end

def route(http_method, uri, options, &block)
action_name = "[#{http_method}] #{uri}"
@_routes << {:method => http_method.to_s.upcase, :uri => uri,
:action => action_name, :options => options}
define_method(action_name, &block)
end
end
end
We’ve simply defined some class methods on the Sinatra::Base to store off routing details for the get and post methods, and creating a new method named [GET] /hello. This is a bit of an interesting Ruby trick; while the def keyword has strict semantics for method names, define_method allows any string.

Now we need to wire up the actual routing. There are a number of options, including the Rails router (rack-mount, rack-router, and usher are all new, working Rails-like routers). We’ll use Usher, a fast Rails-like router written by Josh Hull.

class << Sinatra::Base
def to_app
routes, controller = @_routes, self

Usher::Interface.for(:rack) do
routes.each do |route|
add(route[:uri], :conditions => {:method => route[:method]}.merge(route[:options])).
to(controller.action(route[:action]))
end
end
end
end
Here, we define to_app, which is used by Rack to convert a parameter to run into a valid Rack application. We create a new Usher interface, and add a route for each route created by Sinatra. Because Usher::Interface.for uses instance_eval for its DSL, we store off the routes and controller in local variables that will still be available in the closure.

One little detail here: In Rails 3, each action in a controller is a valid rack endpoint. You get the endpoint by doing ControllerName.action(method_name). Here, we’re simply pulling out the action named “[GET] /hello” that we created in route.

The final piece of the puzzle is covering the action processing in the controller itself. For this, we will mostly reuse the default action processing, with a small change:

class Sinatra::Base
def process_action(*)
self.response_body = super
end
end
What’s happening here is that Rails does not treat the return value of the action as significant, instead expecting it to be set using render, but Sinatra treats the returned string as significant. As a result, we set the response_body to the return value of the action.

Next, let’s add session support.

class << Sinatra::Base
def set(name, value)
send("_set_#{name}", value)
end

def enable(name)
set(name, true)
end

def _set_sessions(value)
@_sessions = value
include ActionController::Session if value
end

def to_app
routes, controller = @_routes, self

app = Usher::Interface.for(:rack) do
routes.each do |route|
add(route[:uri], :conditions => {:method => route[:method]}.merge(route[:options])).
to(controller.action(route[:action]))
end
end

if @_sessions
app = ActionDispatch::Session::CookieStore.new(app, {:key => "_secret_key",
:secret => Digest::SHA2.hexdigest(Time.now.to_s + rand(100).to_s)})
end

app
end
end
There’s a few things going on here. First, Sinatra provides an API for setting options: set :option, :value. In Sinatra, enable :option is equivalent to set :option, true. To simplify adding new options, we just delegate set :whatever, value to a call to _set_whatever(value).

We then implement _set_sessions(value) to include ActionController::Session, which provides the session helper. In to_app, we wrap the original application in an ActionDispatch::Session::CookieStore if sessions were set.

Next, we want to add in support for callbacks (before do). It’s only a few lines:

class Sinatra::Base
include AbstractController::Callbacks
end

class << Sinatra::Base
alias before before_filter
end
Basically, we pull in the normal Rails callback code, and then rename before_filter to before and we’re good to go.

Finally, let’s dig into rendering.

class Sinatra::Base
include ActionController::RenderingController

def sinatra_render_file(name)
render :template => name.to_s
end

def sinatra_render_inline(string, type)
render :inline => string, :type => type
end

%w(haml erb builder).each do |type|
define_method(type) do |thing|
return sinatra_render_inline(thing, type) if thing.is_a?(String)
return sinatra_render_file(thing)
end
end
end

class << Sinatra::Base
alias _set_views append_view_path
end
We include the RenderController module, which provides rendering support. Sinatra supports a few different syntaxes for rendering. It supports erb :template_name which renders the ERB template named template_name. It also supports erb "Some String", which renders the string uses the ERB engine.

Rails supports both of those via render :template and render :inline, so we simply defer to that functionality in each case. We also handle Sinatra’s set :views, view_path by delegating to append_view_path.

You can check out the full repository at https://github.com/wycats/railsnatra/

So there you have it, a large subset of the Sinatra DSL written in Rails in under 100 lines of code. And if you want to add in more advanced Rails features, like layouts, flash, respond_to, file streaming, or conditional get support, it’s just a simple module inclusion away.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值