ruil 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|