merb 0.1.0 → 0.2.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.
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