merb 0.0.7 → 0.0.8

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 (62) hide show
  1. data/README +66 -31
  2. data/Rakefile +3 -1
  3. data/bin/merb +47 -13
  4. data/examples/app_skeleton/Rakefile +4 -3
  5. data/examples/app_skeleton/dist/app/helpers/global_helper.rb +6 -0
  6. data/examples/app_skeleton/dist/conf/merb.yml +11 -0
  7. data/examples/app_skeleton/dist/conf/mup.conf +5 -0
  8. data/examples/app_skeleton/dist/conf/router.rb +1 -3
  9. data/examples/app_skeleton/scripts/merb_stop +10 -2
  10. data/examples/sample_app/Rakefile +3 -3
  11. data/examples/sample_app/dist/app/controllers/files.rb +3 -3
  12. data/examples/sample_app/dist/app/controllers/posts.rb +25 -23
  13. data/examples/sample_app/dist/app/controllers/test.rb +7 -3
  14. data/examples/sample_app/dist/app/helpers/global_helper.rb +7 -0
  15. data/examples/sample_app/dist/app/helpers/posts_helper.rb +4 -0
  16. data/examples/sample_app/dist/app/views/layout/application.herb +5 -4
  17. data/examples/sample_app/dist/app/views/layout/foo.herb +1 -1
  18. data/examples/sample_app/dist/app/views/posts/new.herb +9 -2
  19. data/examples/sample_app/dist/app/views/shared/_test.herb +1 -0
  20. data/examples/sample_app/dist/conf/merb.yml +7 -7
  21. data/examples/sample_app/dist/conf/merb_init.rb +8 -1
  22. data/examples/sample_app/dist/conf/mup.conf +5 -11
  23. data/examples/sample_app/dist/conf/router.rb +1 -1
  24. data/examples/sample_app/dist/public/test.html +5 -0
  25. data/examples/sample_app/dist/schema/migrations/002_add_sessions_table.rb +1 -1
  26. data/examples/sample_app/dist/schema/schema.rb +1 -1
  27. data/examples/sample_app/log/merb.4000.pid +1 -0
  28. data/lib/merb.rb +35 -17
  29. data/lib/merb/core_ext.rb +2 -0
  30. data/lib/merb/{merb_class_extensions.rb → core_ext/merb_class.rb} +42 -0
  31. data/lib/merb/core_ext/merb_enumerable.rb +7 -0
  32. data/lib/merb/{merb_utils.rb → core_ext/merb_hash.rb} +1 -78
  33. data/lib/merb/core_ext/merb_kernel.rb +16 -0
  34. data/lib/merb/core_ext/merb_module.rb +10 -0
  35. data/lib/merb/core_ext/merb_numeric.rb +20 -0
  36. data/lib/merb/core_ext/merb_object.rb +6 -0
  37. data/lib/merb/core_ext/merb_string.rb +40 -0
  38. data/lib/merb/core_ext/merb_symbol.rb +12 -0
  39. data/lib/merb/merb_constants.rb +18 -0
  40. data/lib/merb/merb_controller.rb +150 -76
  41. data/lib/merb/{session/merb_drb_server.rb → merb_drb_server.rb} +13 -46
  42. data/lib/merb/merb_exceptions.rb +4 -0
  43. data/lib/merb/merb_handler.rb +29 -17
  44. data/lib/merb/merb_request.rb +95 -0
  45. data/lib/merb/merb_upload_handler.rb +46 -0
  46. data/lib/merb/merb_upload_progress.rb +48 -0
  47. data/lib/merb/merb_view_context.rb +46 -0
  48. data/lib/merb/merb_yaml_store.rb +31 -0
  49. data/lib/merb/mixins/basic_authentication_mixin.rb +2 -2
  50. data/lib/merb/mixins/controller_mixin.rb +24 -75
  51. data/lib/merb/mixins/erubis_capture_mixin.rb +84 -0
  52. data/lib/merb/mixins/javascript_mixin.rb +103 -19
  53. data/lib/merb/mixins/merb_status_codes.rb +59 -0
  54. data/lib/merb/mixins/render_mixin.rb +114 -40
  55. data/lib/merb/mixins/responder_mixin.rb +2 -1
  56. data/lib/merb/session/merb_ar_session.rb +120 -0
  57. data/lib/merb/session/merb_drb_session.rb +0 -6
  58. data/lib/merb/vendor/paginator/paginator.rb +102 -99
  59. metadata +44 -8
  60. data/examples/sample_app/script/startdrb +0 -8
  61. data/lib/merb/session/merb_session.rb +0 -64
  62. data/lib/mutex_hotfix.rb +0 -34
@@ -0,0 +1,6 @@
1
+ class Object
2
+ def returning(value)
3
+ yield(value)
4
+ value
5
+ end
6
+ end
@@ -0,0 +1,40 @@
1
+ class String
2
+
3
+ # reloads controller classes on each request if
4
+ # :allow_reloading is set to true in the config
5
+ # file or command line options.
6
+ def import
7
+ if Merb::Server.config[:allow_reloading]
8
+ Object.send(:remove_const, self.camel_case.intern) rescue nil
9
+ load(self.snake_case + '.rb')
10
+ else
11
+ require(self.snake_case)
12
+ end
13
+ end
14
+
15
+ # "FooBar".snake_case #=> "foo_bar"
16
+ def snake_case
17
+ return self unless self =~ %r/[A-Z]/
18
+ self.reverse.scan(%r/[A-Z]+|[^A-Z]*[A-Z]+?/).reverse.map{|word| word.reverse.downcase}.join '_'
19
+ end
20
+
21
+ # "foo_bar".camel_case #=> "FooBar"
22
+ def camel_case
23
+ return self if self =~ %r/[A-Z]/ and self !~ %r/_/
24
+ words = self.strip.split %r/\s*_+\s*/
25
+ words.map!{|w| w.downcase.sub(%r/^./){|c| c.upcase}}
26
+ words.join
27
+ end
28
+
29
+ # Concatenates a path
30
+ def /(o)
31
+ File.join(self, o.to_s)
32
+ end
33
+
34
+ def to_const
35
+ const = const.to_s.dup
36
+ base = const.sub!(/^::/, '') ? Object : ( self.kind_of?(Module) ? self : self.class )
37
+ const.split(/::/).inject(base){ |mod, name| mod.const_get(name) }
38
+ end
39
+
40
+ end
@@ -0,0 +1,12 @@
1
+ class Symbol
2
+
3
+ # faster Symbol#to_s to speed up routing.
4
+ def to_s
5
+ @str_rep ||= id2name.freeze
6
+ end
7
+
8
+ # ["foo", "bar"].map &:reverse #=> ['oof', 'rab']
9
+ def to_proc
10
+ Proc.new{|*args| args.shift.__send__(self, *args)}
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module Merb
2
+ module Const
3
+
4
+ ESCAPE_TABLE = {
5
+ '&' => '&',
6
+ '<' => '&lt;',
7
+ '>' => '&gt;',
8
+ '"' => '&quot;',
9
+ "'" => '&#039;',
10
+ }.freeze
11
+
12
+ DEFAULT_SEND_FILE_OPTIONS = {
13
+ :type => 'application/octet-stream'.freeze,
14
+ :disposition => 'attachment'.freeze
15
+ }.freeze
16
+
17
+ end
18
+ end
@@ -1,7 +1,7 @@
1
1
  require File.dirname(__FILE__)+'/mixins/controller_mixin'
2
2
  require File.dirname(__FILE__)+'/mixins/render_mixin'
3
- require File.dirname(__FILE__)+'/mixins/javascript_mixin'
4
3
  require File.dirname(__FILE__)+'/mixins/responder_mixin'
4
+ require File.dirname(__FILE__)+'/merb_request'
5
5
 
6
6
  module Merb
7
7
 
@@ -12,90 +12,99 @@ module Merb
12
12
  # to your controller via params. It also parses the ?query=string and
13
13
  # puts that into params as well.
14
14
  class Controller
15
+
16
+ meta_accessor :layout
17
+
15
18
  include Merb::ControllerMixin
16
19
  include Merb::RenderMixin
17
- include Merb::JavascriptMixin
18
20
  include Merb::ResponderMixin
19
21
 
20
- if Merb::Server.config[:session]
21
- require "drb"
22
- DRb.start_service('druby://localhost:0')
23
- Merb.const_set :DRbSession, DRbObject.new(nil, "druby://#{Merb::Server.config[:host]}:#{Merb::Server.config[:session]}")
24
- require File.dirname(__FILE__)+"/session/merb_drb_session"
25
- include ::Merb::SessionMixin
26
- puts "drb session mixed in"
27
- end
28
-
29
22
  attr_accessor :status, :body
30
23
 
24
+ MULTIPART_REGEXP = /\Amultipart\/form-data.*boundary=\"?([^\";,]+)/n.freeze
25
+ CONTENT_DISPOSITION_REGEXP = /^Content-Disposition: form-data;/.freeze
26
+ FIELD_ATTRIBUTE_REGEXP = /(?:\s(\w+)="([^"]+)")/.freeze
27
+ CONTENT_TYPE_REGEXP = /^Content-Type: (.+?)(\r$|\Z)/m.freeze
28
+
31
29
  # parses the http request into params, headers and cookies
32
30
  # that you can use in your controller classes. Also handles
33
31
  # file uploads by writing a tempfile and passing a reference
34
32
  # in params.
35
33
  def initialize(req, env, args, method=(env['REQUEST_METHOD']||'GET'))
36
- env = MerbHash[env.to_hash]
37
- @layout = :application
34
+ env = ::MerbHash[env.to_hash]
38
35
  @status, @method, @env, @headers, @root = 200, method.downcase.to_sym, env,
39
36
  {'Content-Type' =>'text/html'}, env['SCRIPT_NAME'].sub(/\/$/,'')
40
- @k = query_parse(env['HTTP_COOKIE'], ';,')
41
- qs = query_parse(env['QUERY_STRING'])
42
- #puts req.read; req.rewind
37
+ cookies = query_parse(env['HTTP_COOKIE'], ';,')
38
+ querystring = query_parse(env['QUERY_STRING'])
39
+ self.layout ||= :application
43
40
  @in = req
44
- if %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)|n =~ (env['CONTENT_TYPE'])
45
- b = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/
41
+ if MULTIPART_REGEXP =~ env['CONTENT_TYPE']
42
+ boundary_regexp = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/
46
43
  until @in.eof?
47
- fh=MerbHash[]
48
- for l in @in
49
- case l
44
+ attrs=MerbHash[]
45
+ for line in @in
46
+ case line
50
47
  when "\r\n" : break
51
- when /^Content-Disposition: form-data;/
52
- fh.update MerbHash[*$'.scan(/(?:\s(\w+)="([^"]+)")/).flatten]
53
- when /^Content-Type: (.+?)(\r$|\Z)/m
54
- puts "=> fh[type] = #$1"
55
- fh[:type] = $1
48
+ when CONTENT_DISPOSITION_REGEXP
49
+ attrs.update ::MerbHash[*$'.scan(FIELD_ATTRIBUTE_REGEXP).flatten]
50
+ when CONTENT_TYPE_REGEXP
51
+ attrs[:type] = $1
56
52
  end
57
53
  end
58
- fn=fh[:name]
59
- o=if fh[:filename]
60
- o=fh[:tempfile]=Tempfile.new(:Merb)
61
- o.binmode
54
+ name=attrs[:name]
55
+ io_buffer=if attrs[:filename]
56
+ io_buffer=attrs[:tempfile]=Tempfile.new(:Merb)
57
+ io_buffer.binmode
62
58
  else
63
- fh=""
59
+ attrs=""
64
60
  end
65
- while l=@in.read(16384)
66
- if l=~b
67
- o<<$`.chomp
68
- @in.seek(-$'.size,IO::SEEK_CUR)
61
+ while chunk=@in.read(16384)
62
+ if chunk =~ boundary_regexp
63
+ io_buffer << $`.chomp
64
+ @in.seek(-$'.size, IO::SEEK_CUR)
69
65
  break
70
66
  end
71
- o<<l
67
+ io_buffer << chunk
72
68
  end
73
- qs[fn]=fh if fn
74
- fh[:tempfile].rewind if fh.is_a?MerbHash
69
+ querystring[name]=attrs if name
70
+ attrs[:tempfile].rewind if attrs.is_a?MerbHash
75
71
  end
76
- elsif @method == :post
72
+ elsif @method == :post
77
73
  if ['application/json', 'text/x-json'].include?(env['CONTENT_TYPE'])
78
74
  MERB_LOGGER.info("JSON Request")
79
75
  json = JSON.parse(@in.read || "") || {}
80
- json = MerbHash.new(json) if json.is_a? Hash
81
- qs.merge!(json)
76
+ json = ::MerbHash.new(json) if json.is_a? Hash
77
+ querystring.merge!(json)
82
78
  else
83
- qs.merge!(query_parse(@in.read))
79
+ querystring.merge!(query_parse(@in.read))
84
80
  end
85
81
  end
86
- @cookies, @params = @k.dup, qs.dup.merge(args)
87
- @cookies.merge!(:sess_id => @params.delete(:sess_id)) if @params.has_key?:sess_id
82
+ @cookies, @params = cookies.dup, querystring.dup.merge(args)
83
+ @cookies.merge!(:session_id => @params[:session_id]) if @params.has_key?(:session_id)
84
+ @method = @params.delete(:_method).downcase.to_sym if @params.has_key?(:_method)
85
+ @request = Request.new(@env, @method)
88
86
  MERB_LOGGER.info("Params: #{params.inspect}")
89
87
  end
90
88
 
91
89
  def dispatch(action=:to_s)
92
90
  start = Time.now
93
91
  setup_session if respond_to?:setup_session
94
- if catch(:halt) { call_filters(before_filters) }
92
+ cought = catch(:halt) { call_filters(before_filters) }
93
+ case cought
94
+ when :filter_chain_completed
95
95
  @body = send(action)
96
- else
96
+ when String
97
+ @body = cought
98
+ when nil
97
99
  @body = filters_halted
98
- end
100
+ when Symbol
101
+ @body = send(cought)
102
+ when Proc
103
+ @body = cought.call(self)
104
+ else
105
+ raise MerbControllerError, "The before filter chain is broken dude."
106
+ end
107
+ call_filters(after_filters)
99
108
  finalize_session if respond_to?:finalize_session
100
109
  MERB_LOGGER.info("Time spent in #{action} action: #{Time.now - start} seconds")
101
110
  end
@@ -105,7 +114,13 @@ module Merb
105
114
  def filters_halted
106
115
  "<html><body><h1>Filter Chain Halted!</h1></body></html>"
107
116
  end
108
-
117
+
118
+ # accessor for @request. Please use request and
119
+ # never @request directly.
120
+ def request
121
+ @request
122
+ end
123
+
109
124
  # accessor for @params. Please use params and
110
125
  # never @params directly.
111
126
  def params
@@ -123,18 +138,27 @@ module Merb
123
138
  def headers
124
139
  @headers
125
140
  end
126
-
141
+
142
+ # accessor for @session. Please use session and
143
+ # never @session directly.
144
+ def session
145
+ @session
146
+ end
147
+
127
148
  # meta_accessor sets up a class instance variable that can
128
149
  # be unique for each class but also inherits the meta attrs
129
150
  # from its superclasses. Since @@class variables are almost
130
- # global vars within an inheritance tree
151
+ # global vars within an inheritance tree, we use
152
+ # @class_instance_variables instead
131
153
  meta_accessor :before_filters
154
+ meta_accessor :after_filters
132
155
 
156
+ # calls a filter chain according to rules.
133
157
  def call_filters(filter_set)
134
158
  (filter_set || []).each do |(filter, rule)|
135
159
  ok = false
136
- if rule.has_key?(:include)
137
- if rule[:include].include?(params[:action].intern)
160
+ if rule.has_key?(:only)
161
+ if rule[:only].include?(params[:action].intern)
138
162
  ok = true
139
163
  end
140
164
  elsif rule.has_key?(:exclude)
@@ -150,23 +174,63 @@ module Merb
150
174
  when Proc
151
175
  filter.call(self) if ok
152
176
  end
153
- end
177
+ end
178
+ return :filter_chain_completed
154
179
  end
155
180
 
156
181
  # #before is a class method that allows you to specify before
157
- # filters in your controllers. Filters can either before a symbol
158
- # or string that corresponds to a method name or a proc object.
159
- # if it is a method name that method will be called and if it
160
- # is a proc it will be called with an argument of self. When
182
+ # filters in your controllers. Filters can either be a symbol
183
+ # or string that corresponds to a method name to call, or a
184
+ # proc object. if it is a method name that method will be
185
+ # called and if it is a proc it will be called with an argument
186
+ # of self where self is the current controller object. When
161
187
  # you use a proc as a filter it needs to take one parameter.
188
+ #
189
+ # examples:
190
+ # before :some_filter
191
+ # before :authenticate, :exclude => [:login, :signup]
192
+ # before Proc.new {|c| c.some_method }, :only => :foo
193
+ #
194
+ # You can use either :only => :actionname or :exclude => [:this, :that]
195
+ # but not both at once. :only will only run before the listed actions
196
+ # and :exclude will run for every action that is not listed.
197
+ #
198
+ # Merb's before filter chain is very flixible. To halt the
199
+ # filter chain you use throw :halt . If throw is called with
200
+ # only one argument of :halt the return of the method filters_halted
201
+ # will be what is rendered to the view. You can overide filters_halted
202
+ # in your own controllers to control what it outputs. But the throw
203
+ # construct is much more powerful then just that. throw :halt can
204
+ # also take a second argument. Here is what that second arg can be
205
+ # and the behavior each type can have:
206
+ #
207
+ # when the second arg is a string then that string will be what
208
+ # is rendered to the browser. Since merb's render method returns
209
+ # a string you can render a template or just use a plain string:
210
+ #
211
+ # String:
212
+ # throw :halt, "You don't have permissions to do that!"
213
+ # throw :halt, render(:action => :access_denied)
214
+ #
215
+ # if the second arg is a symbol then the method named after that
216
+ # symbol will be called
217
+ # Symbol:
218
+ # throw :halt, :must_click_disclaimer
219
+ #
220
+ # If the second arg is a Proc, it will be called and its return
221
+ # value will be what is rendered to the browser:
222
+ # Proc:
223
+ # throw :halt, Proc.new {|c| c.access_denied }
224
+ # throw :halt, Proc.new {|c| Tidy.new(c.index) }
225
+ #
162
226
  def self.before(filter, opts={})
163
227
  raise(ArgumentError,
164
- "You can specify either :include or :exclude but
228
+ "You can specify either :only or :exclude but
165
229
  not both at the same time for the same filter."
166
- ) if opts.has_key?(:include) && opts.has_key?(:exclude)
230
+ ) if opts.has_key?(:only) && opts.has_key?(:exclude)
167
231
 
168
- if opts[:include] && opts[:include].is_a?(Symbol)
169
- opts[:include] = [opts[:include]]
232
+ if opts[:only] && opts[:only].is_a?(Symbol)
233
+ opts[:only] = [opts[:only]]
170
234
  end
171
235
  if opts[:exclude] && opts[:exclude].is_a?(Symbol)
172
236
  opts[:exclude] = [opts[:exclude]]
@@ -181,23 +245,33 @@ module Merb
181
245
  )
182
246
  end
183
247
  end
184
-
185
- if Merb::Server.config[:basic_auth]
186
- require File.dirname(__FILE__)+"/mixins/basic_authentication_mixin"
187
- include ::Merb::Authentication
188
- puts "Basic Authentication mixed in"
248
+
249
+ # #after is a class method that allows you to specify after
250
+ # filters in your controllers. Filters can either be a symbol
251
+ # or string that corresponds to a method name or a proc object.
252
+ # if it is a method name that method will be called and if it
253
+ # is a proc it will be called with an argument of self. When
254
+ # you use a proc as a filter it needs to take one parameter.
255
+ # you can gain access to the response body like so:
256
+ # after Proc.new {|c| Tidy.new(c.body) }, :only => :index
257
+ #
258
+ def self.after(filter, opts={})
259
+ raise(ArgumentError,
260
+ "You can specify either :only or :exclude but
261
+ not both at the same time for the same filter."
262
+ ) if opts.has_key?(:only) && opts.has_key?(:exclude)
263
+ raise(ArgumentError,
264
+ 'after filters can only be a Proc object'
265
+ ) unless Proc === filter
266
+ if opts[:only] && opts[:only].is_a?(Symbol)
267
+ opts[:only] = [opts[:only]]
268
+ end
269
+ if opts[:exclude] && opts[:exclude].is_a?(Symbol)
270
+ opts[:exclude] = [opts[:exclude]]
271
+ end
272
+ (self.after_filters ||= []) << [filter, opts]
189
273
  end
190
274
 
191
275
  end
192
276
 
193
277
  end
194
-
195
- class Noroutefound < Merb::Controller
196
- # This is the class that handles requests that don't
197
- # match any defined routes.
198
- def method_missing
199
- @status = 404
200
- "<html><body><h1>No Matching Route!</h1></body></html>"
201
- end
202
-
203
- end
@@ -1,5 +1,5 @@
1
1
  require 'drb'
2
- require 'thread'
2
+ require File.dirname(__FILE__)+'/merb_upload_progress'
3
3
 
4
4
  module Merb
5
5
 
@@ -15,6 +15,7 @@ module Merb
15
15
  @timestamps = Hash.new
16
16
  @mutex = Mutex.new
17
17
  @session_ttl = opts.fetch(:session_ttl, 15*60) # default 15 minutes
18
+ start_timer
18
19
  self
19
20
  end
20
21
 
@@ -51,57 +52,23 @@ module Merb
51
52
  GC.start
52
53
  end
53
54
 
55
+ def start_timer
56
+ Thread.new do
57
+ sleep @session_ttl
58
+ reap_old_sessions
59
+ end
60
+ end
61
+
54
62
  def sessions
55
63
  @sessions
56
64
  end
57
65
 
66
+ def upload_progress
67
+ ::Merb::UploadProgress.new
68
+ end
69
+
58
70
  end # end singleton class
59
71
 
60
72
  end # end DRbSession
61
73
 
62
- # Keeps track of the status of all currently processing uploads
63
- class UploadProgress
64
- attr_accessor :debug
65
- def initialize
66
- @guard = Mutex.new
67
- @counters = {}
68
- end
69
-
70
- def check(upid)
71
- @counters[upid].last rescue nil
72
- end
73
-
74
- def last_checked(upid)
75
- @counters[upid].first rescue nil
76
- end
77
-
78
- def update_checked_time(upid)
79
- @guard.synchronize { @counters[upid][0] = Time.now }
80
- end
81
-
82
- def add(upid, size)
83
- @guard.synchronize do
84
- @counters[upid] = [Time.now, {:size => size, :received => 0}]
85
- puts "#{upid}: Added" if @debug
86
- end
87
- end
88
-
89
- def mark(upid, len)
90
- return unless status = check(upid)
91
- puts "#{upid}: Marking" if @debug
92
- @guard.synchronize { status[:received] = status[:size] - len }
93
- end
94
-
95
- def finish(upid)
96
- @guard.synchronize do
97
- puts "#{upid}: Finished" if @debug
98
- @counters.delete(upid)
99
- end
100
- end
101
-
102
- def list
103
- @counters.keys.sort
104
- end
105
- end
106
-
107
74
  end