ruil 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +6 -2
- data/VERSION +1 -1
- data/lib/ruil/html_responder.rb +88 -0
- data/lib/ruil/json_responder.rb +35 -0
- data/lib/ruil/mongo_resource.rb +128 -0
- data/lib/ruil/path_info_parser.rb +50 -0
- data/lib/ruil/redirector.rb +33 -0
- data/lib/ruil/register.rb +45 -0
- data/lib/ruil/request.rb +49 -0
- data/lib/ruil/resource.rb +109 -68
- data/lib/ruil/session_widget.rb +43 -0
- data/lib/ruil/static_resource.rb +63 -0
- data/lib/ruil/widget.rb +39 -0
- data/lib/ruil.rb +5 -0
- data/rakefile +25 -28
- data/spec/html_responder_spec.rb +50 -0
- data/spec/json_responder_spec.rb +30 -0
- data/spec/path_info_parser_spec.rb +15 -0
- data/spec/redirector_spec.rb +22 -0
- data/spec/register_spec.rb +43 -0
- data/spec/request_spec.rb +26 -0
- data/spec/resource_spec.rb +31 -0
- data/spec/static_resource_spec.rb +38 -0
- metadata +40 -19
- data/.gitignore +0 -5
- data/lib/ruil/delegator.rb +0 -42
- data/lib/ruil/tenjin_template.rb +0 -20
- data/test/delegator_test.rb +0 -50
- data/test/resource_test.rb +0 -23
- data/test/templates/a.desktop.html +0 -1
- data/test/templates/a.mobile.html +0 -1
- data/test/templates/b.desktop.html +0 -1
- data/test/templates/b.mobile.html +0 -1
data/README.rdoc
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
= ruil
|
2
2
|
|
3
|
-
Basic tools for build web
|
3
|
+
Basic tools for build web applications on top of rack
|
4
4
|
|
5
5
|
== Install
|
6
6
|
|
7
|
+
!!!sh
|
7
8
|
$ gem install ruil
|
8
9
|
|
9
10
|
== Usage
|
@@ -14,15 +15,18 @@ Basic tools for build web appplications on top of rack
|
|
14
15
|
|
15
16
|
First download the code from the repository:
|
16
17
|
|
17
|
-
|
18
|
+
!!!sh
|
19
|
+
$ git clone git://github.com/danielhz/ruil.git
|
18
20
|
|
19
21
|
This project uses jeweler to build the gem, so you can use this commands:
|
20
22
|
|
23
|
+
!!!sh
|
21
24
|
$ rake build # to build the gem
|
22
25
|
$ rake install # to build and install the gem in one step
|
23
26
|
|
24
27
|
Also, if you want test the gem you can use the spec task:
|
25
28
|
|
29
|
+
!!!sh
|
26
30
|
$ rake spec
|
27
31
|
|
28
32
|
This project uses rcov so you can check the coverage opening the HTML
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.1.0
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'tenjin'
|
3
|
+
require 'uagent_rack'
|
4
|
+
|
5
|
+
module Ruil
|
6
|
+
|
7
|
+
# {Ruil::HTMLResponder} objects implement receive requests and respond
|
8
|
+
# with Rack::Response where bodies are HTML documents and the media
|
9
|
+
# type is "text/html" or "application/xhtml+xml".
|
10
|
+
class HTMLResponder
|
11
|
+
|
12
|
+
# Initialize a new template object using the template file name
|
13
|
+
# prefix. Also a layout could be defined with the template.
|
14
|
+
#
|
15
|
+
# Template files name must follow the pattern "foo.key.tenjin.html",
|
16
|
+
# where foo is the name of the template, tipically related with
|
17
|
+
# the resource, key is the key that indentify different
|
18
|
+
# templates for the same resource and html is the indentifier
|
19
|
+
# for the template media type.
|
20
|
+
#
|
21
|
+
# We use distinct templates for the same resource to send
|
22
|
+
# different representations of the resource depending the client
|
23
|
+
# user agent and preferences.
|
24
|
+
def initialize(file_prefix, layout = nil)
|
25
|
+
@templates = []
|
26
|
+
Dir[file_prefix + ".*.*.*"].select{ |f| ! (/\.cache$/ === f) }.each do |file|
|
27
|
+
a = File.basename(file).split('.')
|
28
|
+
# Mode
|
29
|
+
mode = a[1].to_sym
|
30
|
+
# Media type
|
31
|
+
media_type = case a[3]
|
32
|
+
when "html"
|
33
|
+
"text/html"
|
34
|
+
when "xhtml"
|
35
|
+
"application/xhtml+xml"
|
36
|
+
else
|
37
|
+
next
|
38
|
+
end
|
39
|
+
# Add template
|
40
|
+
engine = case a[2]
|
41
|
+
when "tenjin"
|
42
|
+
require "tenjin"
|
43
|
+
Tenjin::Engine.new({:layout => layout})
|
44
|
+
else
|
45
|
+
raise "Template engine unknown #{a[2]}"
|
46
|
+
end
|
47
|
+
@templates << {
|
48
|
+
:mode => mode.to_sym,
|
49
|
+
:engine => engine,
|
50
|
+
:file => file,
|
51
|
+
:media_type => media_type,
|
52
|
+
:suffix => a[3].to_sym
|
53
|
+
}
|
54
|
+
end
|
55
|
+
@uagent_parser = UAgent::Parser.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# Creates a resource representation using a template for the
|
59
|
+
# data contained in the request.
|
60
|
+
#
|
61
|
+
# @param [Ruil::Request] request the request to respond.
|
62
|
+
# @return [Rack::Response] the response.
|
63
|
+
def call(request)
|
64
|
+
path_info = request.rack_request.path_info
|
65
|
+
suffix = path_info.sub(/^.*\./, '')
|
66
|
+
suffix = 'html' if suffix == path_info
|
67
|
+
template = @templates.select{
|
68
|
+
|t| t[:suffix] == suffix.to_sym and t[:mode] == mode(request)
|
69
|
+
}.map.first
|
70
|
+
|
71
|
+
unless template.nil?
|
72
|
+
body = template[:engine].render(template[:file], request.generated_data)
|
73
|
+
Rack::Response.new(body, 200, {'Content-Type' => template[:media_type]})
|
74
|
+
else
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def mode(request)
|
80
|
+
r = request.rack_request
|
81
|
+
r.session[:mode] = r.params['mode'].to_sym unless r.params['mode'].nil?
|
82
|
+
r.session[:mode] = @uagent_parser.call(request.rack_request.env) if r.session[:mode].nil?
|
83
|
+
r.session[:mode]
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'json'
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module Ruil
|
6
|
+
|
7
|
+
# The {Ruil::JSONResponder} singleton object recive a request and gets
|
8
|
+
# a Rack::Response serializing the {Ruil::Request#generated_data} in the
|
9
|
+
# JSON format as the response body.
|
10
|
+
#
|
11
|
+
# === Usage
|
12
|
+
#
|
13
|
+
# request = Ruil::Request.new(Rack::Request.new({}))
|
14
|
+
# request.generated_data['some'] = ['data']
|
15
|
+
# responder = JSONResponder.instance
|
16
|
+
# response = responder.call(request)
|
17
|
+
# response.status # => 200
|
18
|
+
# response.header['Content-Type'] # => "application/json"
|
19
|
+
# response.body # => ["{'some':['data']}"]
|
20
|
+
#
|
21
|
+
class JSONResponder
|
22
|
+
include Singleton
|
23
|
+
|
24
|
+
# Responds a request.
|
25
|
+
# @param request[Ruil::Request] the request
|
26
|
+
# @return [Rack::Response] the response
|
27
|
+
def call(request)
|
28
|
+
return false unless /\.js$/ === request.rack_request.path_info
|
29
|
+
body = [request.generated_data.to_json]
|
30
|
+
Rack::Response.new(body, 200, {'Content-Type' => 'application/json'})
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mongo'
|
3
|
+
|
4
|
+
require 'ruil/resource'
|
5
|
+
|
6
|
+
module Ruil
|
7
|
+
|
8
|
+
# {Ruil::MongoResource} objects get a CRUD interface to resources stored in
|
9
|
+
# colletions of a Mongo database.
|
10
|
+
#
|
11
|
+
# db = Mongo::Connection.new.db('mydb')
|
12
|
+
# resource = Ruil::MongoResource.new(db, 'mycollection')
|
13
|
+
#
|
14
|
+
# You could generate a CRUD for all the collections in a database.
|
15
|
+
#
|
16
|
+
# db.collection_names.each do |collection_name|
|
17
|
+
# Ruil::MongoResource.new(db, 'mycollection')
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# You could access to initialized resources using the {Ruil::MongoResource.[]}
|
21
|
+
# method.
|
22
|
+
#
|
23
|
+
# resource = Ruil::MongoResource[:my_collection_name]
|
24
|
+
class MongoResource
|
25
|
+
|
26
|
+
@@resources = {}
|
27
|
+
|
28
|
+
# The items per page when listing resources.
|
29
|
+
# @return [Fixnum] the items per page
|
30
|
+
attr_accessor :page_size
|
31
|
+
|
32
|
+
# A procedure to map items when listing resources.
|
33
|
+
# @return [Proc] a mapping when listing resources
|
34
|
+
attr_accessor :map_list_item
|
35
|
+
|
36
|
+
# A procedure to map a resource to show.
|
37
|
+
# @return [Proc] a mapping when showing resources.
|
38
|
+
attr_accessor :map_show_item
|
39
|
+
|
40
|
+
# The directory where templates are.
|
41
|
+
# @return [String] the templates directory
|
42
|
+
attr_accessor :templates_dir
|
43
|
+
|
44
|
+
# The actions associated with the mongo resource.
|
45
|
+
# This attribute allows you to redefine the behavior of actions
|
46
|
+
# for any {Ruil::MongoResource}.
|
47
|
+
# @return [Hash<Symbol><Ruil::MongoResource] the actions.
|
48
|
+
attr_accessor :actions
|
49
|
+
|
50
|
+
# Creates a new {Ruil::MongoResource}.
|
51
|
+
#
|
52
|
+
# @param [Mongo::DB] db
|
53
|
+
# the resource database
|
54
|
+
#
|
55
|
+
# @param [String] collection_name
|
56
|
+
# the name of the collection that stores the resources
|
57
|
+
def initialize(db, collection_name)
|
58
|
+
@db = db
|
59
|
+
@collection_name = collection_name
|
60
|
+
@collection = @db[@collection_name]
|
61
|
+
@page_size = 10
|
62
|
+
@map_list_item = Proc.new { |item| item }
|
63
|
+
@map_show_item = Proc.new { |item| item }
|
64
|
+
@templates_dir = File.join("dynamic", "templates")
|
65
|
+
@actions = {}
|
66
|
+
@@resources[@collection_name.to_sym] = self
|
67
|
+
yield self if block_given?
|
68
|
+
# Procedure to load resource templates
|
69
|
+
@load_templates = Proc.new do |resource, action|
|
70
|
+
Dir[File.join(@templates_dir, @collection_name, "#{action}.*.*.*")].each do |t|
|
71
|
+
if /\.(html|xhtml)$/ === t
|
72
|
+
resource << Ruil::Template.new(t)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# List resources
|
77
|
+
@actions[:list] = Ruil::Resource.new("GET", "/#{@collection_name}/list") do |r|
|
78
|
+
r.content_generator = Proc.new do |e|
|
79
|
+
e[:page] = ( e[:request].params['page'] || 0 )
|
80
|
+
items = @collection.find().skip(e[:page]).limit(@page_size)
|
81
|
+
{ :items => items.map { |item| @map_list_item.call(item) } }
|
82
|
+
end
|
83
|
+
@load_templates.call r, 'list'
|
84
|
+
end
|
85
|
+
# Show a resource
|
86
|
+
@actions[:show] = Ruil::Resource.new("GET", "/#{@collection_name}/:_id") do |r|
|
87
|
+
r.content_generator = Proc.new do |e|
|
88
|
+
id = BSON::ObjectId.from_string(e[:path_info_params][:_id])
|
89
|
+
item = @collection.find(:_id => id).first
|
90
|
+
{ :item => @map_show_item.call(item) }
|
91
|
+
end
|
92
|
+
@load_templates.call r, 'show'
|
93
|
+
end
|
94
|
+
# Create a new resource
|
95
|
+
@actions[:post] = Ruil::Resource.new("POST", "/#{@collection_name}") do |r|
|
96
|
+
# TODO
|
97
|
+
end
|
98
|
+
# Update a resource
|
99
|
+
@actions[:put] = Ruil::Resource.new("PUT", "/#{@collection_name}/:_id") do |r|
|
100
|
+
# TODO
|
101
|
+
end
|
102
|
+
# Delete a resource
|
103
|
+
@actions[:delete] = Ruil::Resource.new("DELETE", "/#{@collection_name}/:_id") do |r|
|
104
|
+
# TODO'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Get the resource names.
|
109
|
+
#
|
110
|
+
# @return [Array<Symbol>] the resource names.
|
111
|
+
def self.resource_names
|
112
|
+
@@resources.keys
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get the resource for a collection.
|
116
|
+
#
|
117
|
+
# @param [Symbo] collection_name
|
118
|
+
# the collection name
|
119
|
+
#
|
120
|
+
# @return [Ruil::MongoResource]
|
121
|
+
# the resource for the collection
|
122
|
+
def self.[](collection_name)
|
123
|
+
@@resources[collection_name]
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Ruil
|
2
|
+
|
3
|
+
# Each instance of {Ruil::PathInfoParser} matches path info strings
|
4
|
+
# with a pattern and return variables matching it if
|
5
|
+
# the pattern is matched. Else, it returns false.
|
6
|
+
#
|
7
|
+
# === Usage
|
8
|
+
#
|
9
|
+
# parser = PathInfoParser.new('/foo/:type/:id')
|
10
|
+
# parser === '/foo/bar/1' # => {:type => 'bar', :id => '1'}
|
11
|
+
# parser === '/foo/bar/3.js' # => {:type => 'bar', :id => '3'}
|
12
|
+
# parser === '/foo' # => false
|
13
|
+
# parser === '/bar' # => false
|
14
|
+
#
|
15
|
+
class PathInfoParser
|
16
|
+
|
17
|
+
# Initialize a new parser
|
18
|
+
#
|
19
|
+
# @param pattern[String] the pattern to match
|
20
|
+
def initialize(pattern)
|
21
|
+
@pattern = pattern.split('/').map do |s|
|
22
|
+
( s[0,1] == ':' ) ? eval(s) : s
|
23
|
+
end
|
24
|
+
@pattern = ['', ''] if @pattern.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Match a path info.
|
28
|
+
#
|
29
|
+
# @param path_info[String] the path info to match.
|
30
|
+
# @return [Hash,false] a hash with variables matched with the
|
31
|
+
# pattern or false if the path info doesn't match the pattern.
|
32
|
+
def ===(path_info)
|
33
|
+
s = path_info.split('/')
|
34
|
+
s = ['', ''] if s.empty?
|
35
|
+
s.last.gsub!(/\..*$/, '')
|
36
|
+
return false unless s.size == @pattern.size
|
37
|
+
matchs = {}
|
38
|
+
s.each_index do |i|
|
39
|
+
if Symbol === @pattern[i]
|
40
|
+
matchs[@pattern[i]] = s[i]
|
41
|
+
else
|
42
|
+
return false unless @pattern[i] == s[i]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
matchs
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Ruil
|
2
|
+
|
3
|
+
# {Ruil::Redirector} instances redirects requests to other resources.
|
4
|
+
#
|
5
|
+
# === Usage
|
6
|
+
#
|
7
|
+
# Tipically it could be used when access is unauthorized.
|
8
|
+
#
|
9
|
+
# rack_request = Rack::Request.new({})
|
10
|
+
# rack_request.path_info = '/unauthorized_url'
|
11
|
+
# ruil_request = Ruil::Request.new(rack_request)
|
12
|
+
# response = Ruil::Redirector.new('/login').call(ruil_request)
|
13
|
+
# response.status # => 302
|
14
|
+
# response.header['Location'] # => "/login?redirected_from=/unauthorized_url"
|
15
|
+
#
|
16
|
+
class Redirector
|
17
|
+
|
18
|
+
# Initialize a {Ruil::Redirector}.
|
19
|
+
def initialize(redirect_to)
|
20
|
+
@redirect_to = redirect_to
|
21
|
+
end
|
22
|
+
|
23
|
+
# Responds a request with a redirection.
|
24
|
+
# @param request[Ruil::Request] the request.
|
25
|
+
# @return [Rack::Response] the response.
|
26
|
+
def call(request)
|
27
|
+
headers = {'Location'=> @redirect_to + "?redirected_from=" + request.rack_request.path_info}
|
28
|
+
Rack::Response.new([], 302, headers)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Ruil
|
2
|
+
|
3
|
+
# Ruil::Register module allow us to register resources. When a Ruil::Resource
|
4
|
+
# object is created it is automatically registered using the +register+ method.
|
5
|
+
# The +call+ method answer a request with the first registered resource that
|
6
|
+
# match the request. Matches are checked using the order of resource registrations.
|
7
|
+
module Register
|
8
|
+
|
9
|
+
@@resources = {
|
10
|
+
'GET' => [],
|
11
|
+
'POST' => [],
|
12
|
+
'PUT' => [],
|
13
|
+
'DELETE' => []
|
14
|
+
}
|
15
|
+
|
16
|
+
# Register a resource.
|
17
|
+
def self.<<(resource)
|
18
|
+
resource.request_methods.each do |request_method|
|
19
|
+
@@resources[request_method] << resource
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Answer a request with the response of the matched resource for that request.
|
24
|
+
#
|
25
|
+
# @param request_or_env[Rack::Request, Hash] the request.
|
26
|
+
# @return [Rack::Response] the response to the request.
|
27
|
+
def self.call(request_or_env)
|
28
|
+
case request_or_env
|
29
|
+
when Rack::Request
|
30
|
+
request = request_or_env
|
31
|
+
when Hash
|
32
|
+
request = Rack::Request.new(request_or_env)
|
33
|
+
else
|
34
|
+
raise "Invalid request: #{request_or_env.inspect}"
|
35
|
+
end
|
36
|
+
@@resources[request.request_method].each do |resource|
|
37
|
+
response = resource.call(request)
|
38
|
+
return response.finish if response
|
39
|
+
end
|
40
|
+
raise "No resource matching the request"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/lib/ruil/request.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Ruil
|
2
|
+
|
3
|
+
# Instances of {Ruil::Request} encapsulate Rack::Requests
|
4
|
+
# and extend it with data generated when processing the request.
|
5
|
+
#
|
6
|
+
# === Usage
|
7
|
+
#
|
8
|
+
# rack_request = Rack::Request.new
|
9
|
+
# ruil_request = Ruil::Request.new(rack_request)
|
10
|
+
# ruil_request.rack_request # => rack_request
|
11
|
+
# ruil_request.generated_data # => {}
|
12
|
+
# ruil_request.generated_data[:x] = "y"
|
13
|
+
# ruil_request.generated_data # => {:x => "y"}
|
14
|
+
# ruil_request.html_mode # => :desktop
|
15
|
+
#
|
16
|
+
class Request
|
17
|
+
|
18
|
+
# The generated data.
|
19
|
+
# @return [Hash]
|
20
|
+
attr_accessor :generated_data
|
21
|
+
|
22
|
+
# The selected responder.
|
23
|
+
# @return [Object]
|
24
|
+
attr_accessor :responder
|
25
|
+
|
26
|
+
# The rack request
|
27
|
+
# @return [Rack::Request]
|
28
|
+
attr_accessor :rack_request
|
29
|
+
|
30
|
+
# Initialize a new Ruil::Request unsing a Rack::Request.
|
31
|
+
# @param request[Rack::Request] the request to be encapsulated.
|
32
|
+
def initialize(request)
|
33
|
+
@rack_request = request
|
34
|
+
@generated_data = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return the mode for the view (:desktop, :mobile,...).
|
38
|
+
# @return [Symbol]
|
39
|
+
def html_mode
|
40
|
+
if @html_mode.nil?
|
41
|
+
# TODO: Get it from session
|
42
|
+
# TODO: Get it from device parser
|
43
|
+
@html_mode = :desktop
|
44
|
+
end
|
45
|
+
@html_mode
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/lib/ruil/resource.rb
CHANGED
@@ -1,82 +1,123 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
require 'rack'
|
3
|
+
require 'ruil/path_info_parser'
|
4
|
+
require 'ruil/register'
|
2
5
|
|
3
6
|
module Ruil
|
4
7
|
|
8
|
+
# {Ruil::Resource} objects answer requests (see the method {#call}) with
|
9
|
+
# an array following the Rack interface. If the request not match the
|
10
|
+
# resource, +false+ is returned.
|
11
|
+
# Also, a resource includes a set of templates to delegate the action of
|
12
|
+
# rendering a resource.
|
13
|
+
#
|
14
|
+
# === Use example
|
15
|
+
#
|
16
|
+
# The next example shows how to create and use a resource.
|
17
|
+
#
|
18
|
+
# resource = Resource.new('GET', "/index")
|
19
|
+
# puts resource.call(request) # => the response to the request
|
20
|
+
#
|
21
|
+
# Every resource is automatically regitered into {Ruil::Register} when it
|
22
|
+
# is created. Thus, you may use {Ruil::Register.call} to call resource
|
23
|
+
# instead using {#call} directly.
|
24
|
+
#
|
25
|
+
# resource = Resource.new('GET', "/index")
|
26
|
+
# puts Ruil::Register.call(request) # => the response using the register
|
27
|
+
#
|
28
|
+
# === Templates
|
29
|
+
#
|
30
|
+
# The interface of templates consists in the +new+, +key+ and +call+ methods.
|
31
|
+
# Classes that satisfy that interface are {Ruil::Template} and
|
32
|
+
# {Ruil::JSONTemplate}. Every resource have a {Ruil::JSONTemplate} as a
|
33
|
+
# default template.
|
34
|
+
#
|
35
|
+
# resource = Resource.new('GET', "/index") do |res|
|
36
|
+
# Dir['path_to_templates/*'].each do |file|
|
37
|
+
# res << Ruil::Template.new(file)
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# === Path patterns
|
42
|
+
#
|
43
|
+
# Path patterns are strings that are matched with the request path info.
|
44
|
+
# Patterns may include named parameters accessibles via the hash that
|
45
|
+
# the {Ruil::PathInfoParser#===} method returns after a match check.
|
46
|
+
#
|
47
|
+
# resource = Ruil::Resource.new(:get, "/users/:user_id/pictures/:picture_id")
|
48
|
+
# env['PATH_INFO'] = "/users/23/pictures/56"
|
49
|
+
# resource.call(env) # matches are { :user_id => "232, :picture_id => "56" }
|
50
|
+
# env['PATH_INFO'] = "/users/23/pictures"
|
51
|
+
# resource.call(env) # match is false
|
5
52
|
class Resource
|
6
|
-
|
7
|
-
# Initialize a new resource.
|
8
|
-
#
|
9
|
-
# Parameters:
|
10
|
-
# - templates: a hash with procedures or objects with method call(options),
|
11
|
-
# used to generate the resource.
|
12
|
-
# - user_agent_parser: is an object with a method call that analize the
|
13
|
-
# request to get the key for the template to use.
|
14
|
-
def initialize(user_agent_parser, &block)
|
15
|
-
@templates = {}
|
16
|
-
@user_agent_parser = user_agent_parser
|
17
|
-
yield self
|
18
|
-
end
|
19
|
-
|
20
|
-
def add_template(key, template)
|
21
|
-
@templates[key] = template
|
22
|
-
end
|
23
|
-
|
24
|
-
# The regular expression for the url of this resource.
|
25
|
-
def path_pattern
|
26
|
-
'/'
|
27
|
-
end
|
28
53
|
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
# Authorize the access to this resource.
|
35
|
-
def authorize(env)
|
36
|
-
true
|
37
|
-
end
|
38
|
-
|
39
|
-
# Build options to render the resource.
|
40
|
-
def options(env)
|
41
|
-
{
|
42
|
-
:env => env,
|
43
|
-
:template_key => template_key(env)
|
44
|
-
}
|
45
|
-
end
|
46
|
-
|
47
|
-
# Selects the template key
|
48
|
-
def template_key(env)
|
49
|
-
@user_agent_parser.call(env) || @templates.keys.sort.first
|
50
|
-
end
|
54
|
+
# Methods that a resource responds.
|
55
|
+
#
|
56
|
+
# @return [Array<String>]
|
57
|
+
attr_reader :request_methods
|
51
58
|
|
52
|
-
#
|
53
|
-
|
54
|
-
|
59
|
+
# Initialize a new resource.
|
60
|
+
#
|
61
|
+
# @param request_methods [String, Array<String>]
|
62
|
+
# indentify the request methods that this resource responds. Valid
|
63
|
+
# methods are: <tt>"GET"</tt>, <tt>"POST"</tt>, <tt>"PUT"</tt> and
|
64
|
+
# <tt>"DELETE"</tt>.
|
65
|
+
#
|
66
|
+
# @param path_pattern [String]
|
67
|
+
# defines a pattern to match paths.
|
68
|
+
# Patterns may include named parameters accessibles via the hash that
|
69
|
+
# the {Ruil::PathInfoParser#===} method returns after a match check.
|
70
|
+
#
|
71
|
+
# @param authorizer [lambda(Ruil::Request)]
|
72
|
+
# A procedure that checks if the user is allowed to access the resource.
|
73
|
+
#
|
74
|
+
# @param responders [Array<Responder>] an array with responders.
|
75
|
+
def initialize(request_methods, path_pattern, authorizer = nil, responders = [], &block)
|
76
|
+
# Set request methods
|
77
|
+
@request_methods =
|
78
|
+
case request_methods
|
79
|
+
when String
|
80
|
+
[request_methods]
|
81
|
+
when Array
|
82
|
+
request_methods
|
83
|
+
else
|
84
|
+
raise "Invalid value for request_methods: #{request_methods.inspect}"
|
85
|
+
end
|
86
|
+
# Set the path info parser
|
87
|
+
@path_info_parser = Ruil::PathInfoParser.new(path_pattern)
|
88
|
+
# Set the authorizer method
|
89
|
+
@authorizer = authorizer
|
90
|
+
# Set responders
|
91
|
+
@responders = [Ruil::JSONResponder.instance] + responders
|
92
|
+
# Block that generates data
|
93
|
+
@block = Proc.new { |request| yield request } if block_given?
|
94
|
+
# Register
|
95
|
+
Ruil::Register << self
|
55
96
|
end
|
56
97
|
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
98
|
+
# Respond a request
|
99
|
+
#
|
100
|
+
# @param request [Rack::Request] a request to the resource.
|
101
|
+
# @return [Rack::Response] a response for the request.
|
102
|
+
def call(rack_request)
|
103
|
+
path_info = rack_request.path_info
|
104
|
+
path_info_params = ( @path_info_parser === path_info )
|
105
|
+
return false unless path_info_params
|
106
|
+
unless @authorizer.nil?
|
107
|
+
return @autorizer.unauthorized unless @authorizer.authorize(request)
|
108
|
+
end
|
109
|
+
request = Ruil::Request.new(rack_request)
|
110
|
+
request.generated_data[:path_info_params] = path_info_params
|
111
|
+
@block.call(request) unless @block.nil?
|
112
|
+
if request.responder.nil?
|
113
|
+
@responders.each do |responder|
|
114
|
+
response = responder.call(request)
|
115
|
+
return response if response
|
116
|
+
end
|
61
117
|
else
|
62
|
-
|
118
|
+
return request.responder.call(request)
|
63
119
|
end
|
64
|
-
|
65
|
-
|
66
|
-
# Action if the resource is unauthorized
|
67
|
-
def unauthorized(env)
|
68
|
-
redirect("/unauthorized")
|
69
|
-
end
|
70
|
-
|
71
|
-
# Render
|
72
|
-
def render_html(env)
|
73
|
-
content = template(env).call(options(env))
|
74
|
-
[200, {"Content-Type" => "text/html"}, [content]]
|
75
|
-
end
|
76
|
-
|
77
|
-
# Redirect
|
78
|
-
def redirect(url)
|
79
|
-
[ 302, {"Content-Type" => "text/html", 'Location'=> url }, [] ]
|
120
|
+
raise "Responder not found for #{request.inspect}"
|
80
121
|
end
|
81
122
|
|
82
123
|
end
|