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 CHANGED
@@ -1,9 +1,10 @@
1
1
  = ruil
2
2
 
3
- Basic tools for build web appplications on top of rack
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
- $ git clone git@github.com:danielhz/ruil.git
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
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
@@ -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
- # The regular expression for the url of this resource.
30
- def template_pattern
31
- '*.*.html'
32
- end
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
- # Selectes a template to render the resource
53
- def template(env)
54
- @templates[template_key(env)]
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
- # Call the resource
58
- def call(env)
59
- if authorize(env)
60
- render_html(env)
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
- unauthorized(env)
118
+ return request.responder.call(request)
63
119
  end
64
- end
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