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 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