merb 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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