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.
- data/README +71 -14
- data/Rakefile +20 -31
- data/examples/skeleton.tar +0 -0
- data/examples/skeleton/dist/schema/migrations/001_add_sessions_table.rb +1 -1
- data/lib/merb.rb +3 -2
- data/lib/merb/caching.rb +5 -0
- data/lib/merb/caching/action_cache.rb +56 -0
- data/lib/merb/caching/fragment_cache.rb +38 -0
- data/lib/merb/caching/store/file_cache.rb +82 -0
- data/lib/merb/caching/store/memory_cache.rb +67 -0
- data/lib/merb/core_ext.rb +1 -1
- data/lib/merb/core_ext/merb_hash.rb +35 -0
- data/lib/merb/core_ext/merb_object.rb +88 -2
- data/lib/merb/merb_controller.rb +71 -69
- data/lib/merb/merb_dispatcher.rb +72 -0
- data/lib/merb/merb_exceptions.rb +6 -1
- data/lib/merb/merb_handler.rb +19 -47
- data/lib/merb/merb_mailer.rb +1 -1
- data/lib/merb/merb_request.rb +11 -3
- data/lib/merb/merb_router.rb +113 -8
- data/lib/merb/merb_server.rb +71 -12
- data/lib/merb/merb_upload_handler.rb +8 -6
- data/lib/merb/merb_upload_progress.rb +1 -1
- data/lib/merb/merb_view_context.rb +13 -3
- data/lib/merb/mixins/basic_authentication_mixin.rb +1 -3
- data/lib/merb/mixins/controller_mixin.rb +149 -17
- data/lib/merb/mixins/form_control_mixin.rb +1 -0
- data/lib/merb/mixins/render_mixin.rb +148 -151
- data/lib/merb/mixins/responder_mixin.rb +133 -18
- data/lib/merb/mixins/view_context_mixin.rb +24 -0
- data/lib/merb/session/merb_ar_session.rb +4 -4
- data/lib/merb/session/merb_memory_session.rb +6 -5
- data/lib/merb/template.rb +10 -0
- data/lib/merb/template/erubis.rb +52 -0
- data/lib/merb/template/haml.rb +77 -0
- data/lib/merb/template/markaby.rb +48 -0
- data/lib/merb/template/xml_builder.rb +34 -0
- metadata +19 -17
- data/lib/merb/mixins/merb_status_codes.rb +0 -59
@@ -1,32 +1,94 @@
|
|
1
1
|
module Merb
|
2
2
|
|
3
3
|
module RenderMixin
|
4
|
-
@@erbs = {}
|
5
|
-
@@mtimes = {}
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
|
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=
|
42
|
-
|
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
|
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 "
|
116
|
+
return " "
|
53
117
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
14
|
-
|
15
|
-
@
|
16
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@
|
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
|
-
|
153
|
+
|
154
|
+
end
|