merb 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README +71 -14
  2. data/Rakefile +20 -31
  3. data/examples/skeleton.tar +0 -0
  4. data/examples/skeleton/dist/schema/migrations/001_add_sessions_table.rb +1 -1
  5. data/lib/merb.rb +3 -2
  6. data/lib/merb/caching.rb +5 -0
  7. data/lib/merb/caching/action_cache.rb +56 -0
  8. data/lib/merb/caching/fragment_cache.rb +38 -0
  9. data/lib/merb/caching/store/file_cache.rb +82 -0
  10. data/lib/merb/caching/store/memory_cache.rb +67 -0
  11. data/lib/merb/core_ext.rb +1 -1
  12. data/lib/merb/core_ext/merb_hash.rb +35 -0
  13. data/lib/merb/core_ext/merb_object.rb +88 -2
  14. data/lib/merb/merb_controller.rb +71 -69
  15. data/lib/merb/merb_dispatcher.rb +72 -0
  16. data/lib/merb/merb_exceptions.rb +6 -1
  17. data/lib/merb/merb_handler.rb +19 -47
  18. data/lib/merb/merb_mailer.rb +1 -1
  19. data/lib/merb/merb_request.rb +11 -3
  20. data/lib/merb/merb_router.rb +113 -8
  21. data/lib/merb/merb_server.rb +71 -12
  22. data/lib/merb/merb_upload_handler.rb +8 -6
  23. data/lib/merb/merb_upload_progress.rb +1 -1
  24. data/lib/merb/merb_view_context.rb +13 -3
  25. data/lib/merb/mixins/basic_authentication_mixin.rb +1 -3
  26. data/lib/merb/mixins/controller_mixin.rb +149 -17
  27. data/lib/merb/mixins/form_control_mixin.rb +1 -0
  28. data/lib/merb/mixins/render_mixin.rb +148 -151
  29. data/lib/merb/mixins/responder_mixin.rb +133 -18
  30. data/lib/merb/mixins/view_context_mixin.rb +24 -0
  31. data/lib/merb/session/merb_ar_session.rb +4 -4
  32. data/lib/merb/session/merb_memory_session.rb +6 -5
  33. data/lib/merb/template.rb +10 -0
  34. data/lib/merb/template/erubis.rb +52 -0
  35. data/lib/merb/template/haml.rb +77 -0
  36. data/lib/merb/template/markaby.rb +48 -0
  37. data/lib/merb/template/xml_builder.rb +34 -0
  38. metadata +19 -17
  39. data/lib/merb/mixins/merb_status_codes.rb +0 -59
@@ -19,6 +19,7 @@ module Merb
19
19
  # <%= control_for @post, :created_at, :time %>
20
20
  # <%= control_for @post, :published_at, :date %>
21
21
  #
22
+ # TODO : is this useful enough? Needs some love
22
23
  def control_for(obj, meth, type, opts={})
23
24
  instance = obj
24
25
  obj = obj.class
@@ -1,32 +1,94 @@
1
1
  module Merb
2
2
 
3
3
  module RenderMixin
4
- @@erbs = {}
5
- @@mtimes = {}
6
4
 
7
- def self.erbs
8
- @@erbs
9
- end
10
-
11
- # shortcut to a template path based on name.
12
- def template_dir(loc)
13
- File.expand_path(MERB_ROOT / "/dist/app/views/#{loc}")
14
- end
15
-
16
- # returns the current method name. Used for
17
- # auto discovery of which template to render
18
- # based on the action name.
19
- def current_method_name(depth=0)
20
- caller[depth] =~ /`(.*)'$/; $1
21
- end
22
-
23
- # given html, js and xml this method returns the template
24
- # extension from the :template_ext map froom your app's
25
- # configuration. defaults to .herb, .jerb & .xerb
26
- def template_extension_for(ext)
27
- (@tmpl_ext_cache ||= Merb::Server.template_ext)[ext]
5
+ # universal render method. Template handlers are registered
6
+ # by template extension. So you can use the same render method
7
+ # for any kind of template that implements an adapter module.
8
+ # out of the box Merb support Erubis, Markaby and Builder templates
9
+ #
10
+ # Erubis template ext: .herb .jerb .erb
11
+ # Markaby template ext: .mab
12
+ # Builder template ext: .rxml .builder .xerb
13
+ #
14
+ # Examples:
15
+ #
16
+ # render
17
+ # looks for views/controllername/actionname.* and renders
18
+ # the template with the proper engine based on its file extension.
19
+ #
20
+ # render :layout => :none
21
+ # renders the current template with no layout. XMl Builder templates
22
+ # are exempt from layout by default.
23
+ #
24
+ # render :action => 'foo'
25
+ # renders views/controllername/foo.*
26
+ #
27
+ # render :nothing => 200
28
+ # renders nothing with a status of 200
29
+ #
30
+ # render :js => "$('some-div').toggle();"
31
+ # if the right hand side of :js => is a string then the proper
32
+ # javascript headers will be set and the string will be returned
33
+ # verbatim as js.
34
+ #
35
+ # render :js => :spinner
36
+ # when the rhs of :js => is a Symbol, it will be used as the
37
+ # action/template name so: views/controllername/spinner.jerb
38
+ # will be rendered as javascript
39
+ #
40
+ # render :js => true
41
+ # this will just look for the current controller/action tenmplate
42
+ # with the .jerb extension and render it as javascript
43
+ #
44
+ # render :xml => @posts.to_xml
45
+ # render :xml => "<foo><bar>Hi!</bar></foo>"
46
+ # this will set the appropriate xml headers and render the rhs
47
+ # of :xml => as a string. SO you can pass any xml string to this
48
+ # to be rendered.
49
+ #
50
+ def render(opts={}, &blk)
51
+ action = opts[:action] || params[:action]
52
+ opts[:layout] ||= ancestral_trait[:layout]
53
+
54
+ case
55
+ when status = opts[:nothing]
56
+ return render_nothing(status)
57
+ when partial = opts[:partial]
58
+ template = find_partial(partial, opts)
59
+ opts[:layout] = :none
60
+ when js = opts[:js]
61
+ headers['Content-Type'] = "text/javascript"
62
+ opts[:layout] = :none
63
+ if String === js
64
+ return js
65
+ elsif Symbol === js
66
+ template = find_template(:action => js, :ext => 'jerb')
67
+ else
68
+ template = find_template(:action => action, :ext => 'jerb')
69
+ end
70
+ when xml = opts[:xml]
71
+ headers['Content-Type'] = 'application/xml'
72
+ headers['Encoding'] = 'UTF-8'
73
+ return xml
74
+ else
75
+ template = find_template(:action => action)
76
+ end
77
+
78
+ engine = engine_for(template)
79
+ options = {
80
+ :file => template,
81
+ :view_context => (opts[:clean_context] ? clean_view_context : _view_context),
82
+ :opts => opts
83
+ }
84
+ content = engine.transform(options)
85
+ if engine.exempt_from_layout? || opts[:layout] == :none
86
+ content
87
+ else
88
+ wrap_layout(content, opts)
89
+ end
28
90
  end
29
-
91
+
30
92
  # this returns a ViewContext object populated with all
31
93
  # the instance variables in your controller. This is used
32
94
  # as the view context object for the Erubis templates.
@@ -34,36 +96,32 @@ module Merb
34
96
  @_view_context_cache ||= ViewContext.new(self)
35
97
  end
36
98
 
99
+ def clean_view_context
100
+ ViewContext.new(self)
101
+ end
102
+
37
103
  # does a render with no layout. Also sets the
38
104
  # content type header to text/javascript. Use
39
105
  # this when you want to render a template with
40
106
  # .jerb extension.
41
- def render_js(template=current_method_name(1))
42
- headers['Content-Type'] = "text/javascript"
43
- template = new_eruby_obj(template_dir(self.class.name.snake_case) / "/#{template}.#{template_extension_for(:js)}")
44
- template.evaluate(_view_context)
107
+ def render_js(template=nil)
108
+ render :js => true, :action => (template || params[:action])
45
109
  end
46
-
110
+
47
111
  # renders nothing but sets the status, defaults
48
- # to 200. does send one \n newline char, tyhis is for
112
+ # to 200. does send one ' ' space char, this is for
49
113
  # safari and flash uploaders to work.
50
114
  def render_nothing(status=200)
51
115
  @status = status
52
- return "\n"
116
+ return " "
53
117
  end
54
-
55
- # renders the action without wrapping it in a layout.
56
- # call it without arguments if your template matches
57
- # the name of the running action. Otherwise you can
58
- # explicitely set the template name excluding the file
59
- # extension
60
- def render_no_layout(template=current_method_name(1))
61
- template = new_eruby_obj(template_dir(self.class.name.snake_case) / "/#{template}.#{template_extension_for(:html)}")
62
- template.evaluate(_view_context)
118
+
119
+ def render_no_layout(opts={})
120
+ render opts.update({:layout => :none})
63
121
  end
64
122
 
65
123
  # This is merb's partial render method. You name your
66
- # partials _partialname.herb, and then call it like
124
+ # partials _partialname.* , and then call it like
67
125
  # partial(:partialname). If there is no '/' character
68
126
  # in the argument passed in it will look for the partial
69
127
  # in the view directory that corresponds to the current
@@ -72,120 +130,59 @@ module Merb
72
130
  # if you create a views/shared directory then you can call
73
131
  # partials that live there like partial('shared/foo')
74
132
  def partial(template)
75
- if template =~ /\//
76
- t = template.split('/')
77
- template = t.pop
78
- tmpl = new_eruby_obj(template_dir(t.join('/')) / "/_#{template}.#{template_extension_for(:html)}")
79
- else
80
- tmpl = new_eruby_obj(template_dir(self.class.name.snake_case) / "/_#{template}.#{template_extension_for(:html)}")
81
- end
82
- tmpl.evaluate(_view_context)
133
+ render :partial => template
83
134
  end
84
135
 
85
- # This creates and returns a new Erubis object populated
86
- # with the template from path. If there is no matching
87
- # template then we rescue the Errno::ENOENT exception
88
- # and raise a no template found message
89
- def new_eruby_obj(path)
90
- if @@erbs[path] && !cache_template?(path)
91
- return @@erbs[path]
92
- else
93
- begin
94
- returning Erubis::MEruby.new(IO.read(path)) do |eruby|
95
- eruby.init_evaluator :filename => path
96
- if cache_template?(path)
97
- @@erbs[path] = eruby
98
- @@mtimes[path] = Time.now
99
- end
100
- end
101
- rescue Errno::ENOENT
102
- raise "No template found at path: #{path}"
103
- end
104
- end
105
- end
106
-
107
- def cache_template?(path)
108
- return false unless Merb::Server.config[:cache_templates]
109
- return true unless @@erbs[path]
110
- @@mtimes[path] < File.mtime(path) ||
111
- (File.symlink?(path) && (@@mtimes[path] < File.lstat(path).mtime))
112
- end
136
+ private
113
137
 
114
- # this is the xml builder render method. This method
115
- # builds the Builder::XmlMarkup object for you and adds
116
- # the xml headers and encoding. Then it evals your template
117
- # in the context of the xml object. So your .xerb templates
118
- # will look like this:
119
- # xml.foo {|xml|
120
- # xml.bar "baz"
121
- # }
122
- def render_xml(template=current_method_name(1))
123
- _xml_body = IO.read( template_dir(self.class.name.snake_case) + "/#{template}.#{template_extension_for(:xml)}" )
124
- headers['Content-Type'] = 'application/xml'
125
- headers['Encoding'] = 'UTF-8'
126
- _view_context.instance_eval %{
127
- xml = Builder::XmlMarkup.new :indent => 2
128
- xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
129
- #{_xml_body}
130
- return xml.target!
131
- }
132
- end
133
-
134
- # This is the main render method that handles layouts.
135
- # render will use layout/application.rhtml unless
136
- # there is a layout named after the current controller
137
- # or if self.layout= has been set to another value in
138
- # the current controller. You can over-ride this setting
139
- # by passing an options hash with a :layout => 'layoutname'.
140
- # if you with to not render a layout then pass :layout => :none
141
- # the first argument to render is the template name. if you do
142
- # not pass a template name, it will set the template to
143
- # views/controller/action automatically.
144
- # examples:
145
- # class Test < Merb::Controller
146
- # # renders views/test/foo.herb
147
- # # in layout application.herb
148
- # def foo
149
- # # code
150
- # render
151
- # end
152
- #
153
- # # renders views/test/foo.herb
154
- # # in layout application.herb
155
- # def bar
156
- # # code
157
- # render :foo
158
- # end
159
- #
160
- # # renders views/test/baz.herb
161
- # # with no layout
162
- # def baz
163
- # # code
164
- # render :layout => :none
165
- # end
166
- def render(opts={})
167
- template = opts.is_a?(Symbol) ? opts : (opts[:action] || params[:action])
168
- tmpl_ext = template_extension_for(:html)
169
- MERB_LOGGER.info("Rendering template: #{template}.#{tmpl_ext}")
170
- name = self.class.name.snake_case
171
- template = new_eruby_obj(template_dir(name) / "/#{template}.#{tmpl_ext}")
172
- layout_content = template.evaluate(_view_context)
173
- self.layout = opts[:layout].to_sym if opts.has_key?(:layout)
174
- return layout_content if (layout == :none)
175
- if layout != :application
176
- layout_choice = layout
177
- else
178
- if File.exist?(template_dir("layout/#{name}.#{tmpl_ext}"))
179
- layout_choice = name
138
+ def wrap_layout(content, opts={})
139
+ if opts[:layout] != :application
140
+ layout_choice = find_template(:layout => opts[:layout])
180
141
  else
181
- layout_choice = layout
142
+ if name = find_template(:layout => self.class.name.snake_case)
143
+ layout_choice = name
144
+ else
145
+ layout_choice = find_template(:layout => :application)
146
+ end
147
+ end
148
+
149
+ _view_context.instance_variable_set('@_layout_content', content)
150
+ engine = engine_for(layout_choice)
151
+ options = {
152
+ :file => layout_choice,
153
+ :view_context => _view_context,
154
+ :opts => opts
155
+ }
156
+ engine.transform(options)
157
+ end
158
+
159
+ # OPTIMIZE : combine find_template and find_partial ?
160
+ def find_template(opts={})
161
+ if action = opts[:action]
162
+ path =
163
+ File.expand_path(MERB_ROOT / "dist/app/views" / self.class.name.snake_case / action)
164
+ elsif layout = opts[:layout]
165
+ path = ancestral_trait[:layout_root] / layout
166
+ else
167
+ raise "called find_template without an :action or :layout"
182
168
  end
183
- end
184
- MERB_LOGGER.info("With Layout: #{layout_choice}.#{tmpl_ext}")
185
- _view_context.instance_variable_set('@_layout_content', layout_content)
186
- layout_tmpl = new_eruby_obj("#{template_dir('layout')}/#{layout_choice}.#{tmpl_ext}")
187
- layout_tmpl.evaluate(_view_context)
188
- end
169
+ extensions = [ancestral_trait[:template_extensions].keys].flatten.uniq
170
+ glob = "#{path}.{#{opts[:ext] || extensions.join(',')}}"
171
+ Dir[glob].first
172
+ end
173
+
174
+ def find_partial(template, opts={})
175
+ if template =~ /\//
176
+ t = template.split('/')
177
+ template = t.pop
178
+ path = ancestral_trait[:template_root] / t.join('/') / "_#{template}"
179
+ else
180
+ path = ancestral_trait[:template_root] / self.class.name.snake_case / "_#{template}"
181
+ end
182
+ extensions = [ancestral_trait[:template_extensions].keys].flatten.uniq
183
+ glob = "#{path}.{#{opts[:ext] || extensions.join(',')}}"
184
+ Dir[glob].first
185
+ end
189
186
 
190
187
  end
191
- end
188
+ end
@@ -9,31 +9,146 @@ module Merb
9
9
  # type.yaml { @foo.to_yaml }
10
10
  # end
11
11
 
12
+ # TODO : revisit this whole patern. Can we improve on this?
12
13
  module ResponderMixin
13
- def respond_to
14
- yield response = Response.new(@env['HTTP_ACCEPT'])
15
- @headers['Content-Type'] = response.content_type
16
- response.body
14
+
15
+ def respond_to(&block)
16
+ responder = Rest::Responder.new(@env['HTTP_ACCEPT'], params)
17
+ block.call(responder)
18
+ responder.respond(headers)
19
+ @status = responder.status
20
+ responder.body
17
21
  end
18
22
 
19
- class Response
20
- attr_reader :body, :content_type
21
- def initialize(accept) @accept = accept end
22
-
23
- TYPES = {
24
- :yaml => %w[application/yaml text/yaml],
23
+ module Rest
24
+
25
+ TYPES = {
26
+ :all => %w[*/*],
27
+ :yaml => %w[application/x-yaml text/yaml],
25
28
  :text => %w[text/plain],
26
- :html => %w[text/html */* application/html],
27
- :xml => %w[application/xml],
28
- :js => %w[application/json text/x-json]
29
+ :html => %w[text/html application/xhtml+xml application/html],
30
+ :xml => %w[application/xml text/xml application/x-xml],
31
+ :js => %w[application/json text/x-json text/javascript application/javascript application/x-javascript]
29
32
  }
33
+
34
+ class Responder
35
+
36
+ attr_reader :body, :type, :status
37
+
38
+ def initialize(accept_header, params={})
39
+ MERB_LOGGER.info accept_header
40
+ @accepts = Responder.parse(accept_header)
41
+ @params = params
42
+ @stack = {}
43
+ end
44
+
45
+ def method_missing(symbol, &block)
46
+ raise "respond_to expects a block" unless block_given?
47
+ # the first method we encounter here will be used for the catch all mime-type */*
48
+ @stack[:all] = block unless @stack[:all]
49
+ @stack[symbol] = block
50
+ end
51
+
52
+ def respond(headers)
53
+ unless @stack.keys.all?{|k| TYPES.has_key?(k) }
54
+ raise "unrecognized mime type in respond_to block"
55
+ end
56
+ mime_type = negotiate_content
57
+ if mime_type
58
+ headers['Content-Type'] = mime_type.super_range
59
+ @status = 200
60
+ @body = @stack[mime_type.to_sym].call
61
+ else
62
+ headers['Content-Type'] = nil
63
+ @status = 406
64
+ @body = nil
65
+ end
66
+ end
67
+
68
+ protected
69
+
70
+ def self.parse(accept_header)
71
+ index = 0
72
+ list = accept_header.split(/,/).map! do |entry|
73
+ AcceptType.new(entry,index += 1)
74
+ end.sort!.uniq
75
+ end
76
+
77
+ private
78
+
79
+ def negotiate_content
80
+ if @params['format']
81
+ negotiate_by_format
82
+ elsif (@stack.keys & @accepts.map(&:to_sym)).size > 0
83
+ negotiate_by_accept_header
84
+ end
85
+ end
86
+
87
+ def negotiate_by_format
88
+ format = @params['format'].to_sym
89
+ if @stack[format]
90
+ if @accepts.map(&:to_sym).include?(format)
91
+ @accepts.select{|a| a.to_sym == format }.first
92
+ else
93
+ AcceptType.new(TYPES[format].first,0)
94
+ end
95
+ end
96
+ end
97
+
98
+ def negotiate_by_accept_header
99
+ @accepts.each do |accept|
100
+ return accept if @stack[accept.to_sym] || accept.to_sym == :all
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ class AcceptType
30
107
 
31
- def method_missing(method, *args)
32
- if TYPES[method] && @accept =~ Regexp.union(*TYPES[method])
33
- @content_type = TYPES[method].first
34
- @body = yield if block_given?
108
+ attr_reader :media_range, :quality, :index, :type, :sub_type
109
+
110
+ def initialize(entry,index)
111
+ @index = index
112
+ @media_range, quality = entry.split(/;\s*q=/).map(&:strip)
113
+ @type, @sub_type = @media_range.split(/\//)
114
+ quality ||= 0.0 if @media_range == '*/*'
115
+ @quality = ((quality || 1.0).to_f * 100).to_i
116
+ end
117
+
118
+ def <=>(entry)
119
+ returning (entry.quality <=> quality).to_s do |c|
120
+ c.replace((index <=> entry.index).to_s) if c == '0'
121
+ end.to_i
35
122
  end
123
+
124
+ def eql?(entry)
125
+ synonyms.include?(entry.media_range)
126
+ end
127
+
128
+ def ==(entry); eql?(entry); end
129
+
130
+ def hash; super_range.hash; end
131
+
132
+ def synonyms
133
+ TYPES.values.select{|e| e.include?(@media_range)}.flatten
134
+ end
135
+
136
+ def super_range
137
+ synonyms.first || @media_range
138
+ end
139
+
140
+ def to_sym
141
+ TYPES.select{|k,v| v == synonyms }.flatten.first
142
+ end
143
+
144
+ def to_s
145
+ @media_range
146
+ end
147
+
36
148
  end
149
+
37
150
  end
151
+
38
152
  end
39
- end
153
+
154
+ end