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
@@ -9,4 +9,4 @@ corelib = __DIR__+'/merb/core_ext'
9
9
  merb_hash
10
10
  merb_numeric
11
11
  merb_symbol
12
- ].each {|fn| require File.join(corelib, fn)}
12
+ ].each {|fn| require File.join(corelib, fn)}
@@ -2,6 +2,41 @@ class Hash
2
2
  def with_indifferent_access
3
3
  MerbHash.new(self)
4
4
  end
5
+ def to_params()
6
+ result = ''
7
+ stack = []
8
+
9
+ each do |key, value|
10
+ Hash === value ? stack << [key, value] : result << "#{key}=#{value}&"
11
+ end
12
+
13
+ stack.each do |parent, hash|
14
+ hash.each do |key, value|
15
+ if Hash === value
16
+ stack << ["#{parent}[#{key}]", value]
17
+ else
18
+ result << "#{parent}[#{key}]=#{value}&"
19
+ end
20
+ end
21
+ end
22
+ result.chop
23
+ end
24
+
25
+ # lets through the keys in the argument
26
+ # >> {:one => 1, :two => 2, :three => 3}.pass(:one)
27
+ # => {:one=>1}
28
+ def pass(*keys)
29
+ self.reject { |k,v| ! keys.include?(k) }
30
+ end
31
+ alias only pass
32
+
33
+ # blocks the keys in the arguments
34
+ # >> {:one => 1, :two => 2, :three => 3}.block(:one)
35
+ # => {:two=>2, :three=>3}
36
+ def block(*keys)
37
+ self.reject { |k,v| keys.include?(k) }
38
+ end
39
+ alias except block
5
40
  end
6
41
 
7
42
  # like HashWithIndifferentAccess from ActiveSupport.
@@ -1,15 +1,72 @@
1
+ Traits = Hash.new{|h,k| h[k] = {}}
2
+
1
3
  class Object
4
+ # traits thanks to Michael Fellinger m.fellinger@gmail.com
5
+ # Adds a method to Object to annotate your objects with certain traits.
6
+ # It's basically a simple Hash that takes the current object as key
7
+ #
8
+ # Example:
9
+ #
10
+ # class Foo
11
+ # trait :instance => false
12
+ #
13
+ # def initialize
14
+ # trait :instance => true
15
+ # end
16
+ # end
17
+ #
18
+ # Foo.trait[:instance]
19
+ # # false
20
+ #
21
+ # foo = Foo.new
22
+ # foo.trait[:instance]
23
+ # # true
24
+
25
+ def trait hash = nil
26
+ if hash
27
+ Traits[self].merge! hash
28
+ else
29
+ Traits[self]
30
+ end
31
+ end
32
+
33
+ # builds a trait from all the ancestors, closer ancestors
34
+ # overwrite distant ancestors
35
+ #
36
+ # class Foo
37
+ # trait :one => :eins
38
+ # trait :first => :erstes
39
+ # end
40
+ #
41
+ # class Bar < Foo
42
+ # trait :two => :zwei
43
+ # end
44
+ #
45
+ # class Foobar < Bar
46
+ # trait :three => :drei
47
+ # trait :first => :overwritten
48
+ # end
49
+ #
50
+ # Foobar.ancestral_trait
51
+ # {:three=>:drei, :two=>:zwei, :one=>:eins, :first=>:overwritten}
52
+
53
+ def ancestral_trait
54
+ ancs = (ancestors rescue self.class.ancestors)
55
+ ancs.reverse.inject({}){|s,v| s.merge(v.trait)}.merge(trait)
56
+ end
57
+
2
58
  def returning(value)
3
59
  yield(value)
4
60
  value
5
61
  end
6
62
 
7
- def __meta() class << self; self end end
8
- def meta_eval(&blk) __meta.instance_eval( &blk ) end
63
+ def meta_class() class << self; self end end
64
+ def meta_eval(&blk) meta_class.instance_eval( &blk ) end
9
65
  def meta_def(name, &blk) meta_eval { define_method name, &blk } end
10
66
  def class_def name, &blk
11
67
  self.class.class_eval { define_method name, &blk }
12
68
  end
69
+
13
70
  def blank?
14
71
  if respond_to? :empty? then empty?
15
72
  elsif respond_to? :zero? then zero?
@@ -17,4 +74,33 @@ class Object
17
74
  end
18
75
  end
19
76
 
77
+ def with_options(options)
78
+ yield Merb::OptionMerger.new(self, options)
79
+ end
20
80
  end
81
+
82
+ module Merb
83
+ class OptionMerger #:nodoc:
84
+ instance_methods.each do |method|
85
+ undef_method(method) if method !~ /^(__|instance_eval|class)/
86
+ end
87
+
88
+ def initialize(context, options)
89
+ @context, @options = context, options
90
+ end
91
+
92
+ private
93
+ def method_missing(method, *arguments, &block)
94
+ merge_argument_options! arguments
95
+ @context.send(method, *arguments, &block)
96
+ end
97
+
98
+ def merge_argument_options!(arguments)
99
+ arguments << if arguments.last.respond_to? :to_hash
100
+ @options.merge(arguments.pop)
101
+ else
102
+ @options.dup
103
+ end
104
+ end
105
+ end
106
+ end
@@ -12,85 +12,55 @@ 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
- shared_accessor :layout, :session_id_key, :cache_templates
15
+
16
+ trait :layout => :application
17
+ trait :session_id_key => :_session_id
17
18
 
18
19
  include Merb::ControllerMixin
19
20
  include Merb::RenderMixin
20
21
  include Merb::ResponderMixin
21
22
 
22
- attr_accessor :status, :body
23
-
23
+ attr_accessor :status, :body, :request
24
+
24
25
  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
-
26
+
29
27
  # parses the http request into params, headers and cookies
30
28
  # that you can use in your controller classes. Also handles
31
29
  # file uploads by writing a tempfile and passing a reference
32
30
  # in params.
33
- def initialize(req, env, args, method=(env['REQUEST_METHOD']||'GET'))
34
- env = ::MerbHash[env.to_hash]
35
- @status, @method, @env, @headers, @root = 200, method.downcase.to_sym, env,
36
- {'Content-Type' =>'text/html'}, env['SCRIPT_NAME'].sub(/\/$/,'')
37
- cookies = query_parse(env['HTTP_COOKIE'], ';,')
38
- querystring = query_parse(env['QUERY_STRING'])
39
- self.layout ||= :application
40
- self.session_id_key ||= :_session_id
41
- @in = req
42
- if MULTIPART_REGEXP =~ env['CONTENT_TYPE']
43
- boundary_regexp = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/
44
- until @in.eof?
45
- attrs=MerbHash[]
46
- for line in @in
47
- case line
48
- when "\r\n" : break
49
- when CONTENT_DISPOSITION_REGEXP
50
- attrs.update ::MerbHash[*$'.scan(FIELD_ATTRIBUTE_REGEXP).flatten]
51
- when CONTENT_TYPE_REGEXP
52
- attrs[:type] = $1
53
- end
54
- end
55
- name=attrs[:name]
56
- io_buffer=if attrs[:filename]
57
- io_buffer=attrs[:tempfile]=Tempfile.new(:Merb)
58
- io_buffer.binmode
59
- else
60
- attrs=""
61
- end
62
- while chunk=@in.read(16384)
63
- if chunk =~ boundary_regexp
64
- io_buffer << $`.chomp
65
- @in.seek(-$'.size, IO::SEEK_CUR)
66
- break
67
- end
68
- io_buffer << chunk
69
- end
70
- if name =~ /(.*)?\[\]/
71
- (querystring[$1] ||= []) << attrs
72
- else
73
- querystring[name]=attrs if name
74
- end
75
- attrs[:tempfile].rewind if attrs.is_a?MerbHash
76
- end
31
+ def initialize(request, env, args, response)
32
+ @env = MerbHash[env.to_hash]
33
+ @status, @method, @response, @headers = 200, (env['REQUEST_METHOD']||'GET').downcase.to_sym, response,
34
+ {'Content-Type' =>'text/html'}
35
+ cookies = query_parse(@env['HTTP_COOKIE'], ';,')
36
+ querystring = query_parse(@env['QUERY_STRING'])
37
+
38
+ if MULTIPART_REGEXP =~ @env['CONTENT_TYPE'] && @method == :post
39
+ querystring.update(parse_multipart(request, $1))
77
40
  elsif @method == :post
78
- if ['application/json', 'text/x-json'].include?(env['CONTENT_TYPE'])
41
+ if ['application/json', 'text/x-json'].include?(@env['CONTENT_TYPE'])
79
42
  MERB_LOGGER.info("JSON Request")
80
- json = JSON.parse(@in.read || "") || {}
81
- json = ::MerbHash.new(json) if json.is_a? Hash
82
- querystring.merge!(json)
43
+ json = JSON.parse(request.read || "") || {}
44
+ json = MerbHash.new(json) if json.is_a? Hash
45
+ querystring.update(json)
83
46
  else
84
- querystring.merge!(query_parse(@in.read))
47
+ querystring.update(query_parse(request.read))
85
48
  end
86
49
  end
87
- @cookies, @params = cookies.dup, querystring.dup.merge(args)
88
- @cookies.merge!(:_session_id => @params[:_session_id]) if @params.has_key?(:_session_id)
89
- @method = @params.delete(:_method).downcase.to_sym if @params.has_key?(:_method)
50
+
51
+ @cookies, @params = cookies, querystring.update(args)
52
+ @cookies[ancestral_trait[:session_id_key]] = @params[ancestral_trait[:session_id_key]] if @params.key?(ancestral_trait[:session_id_key])
53
+
54
+ allow = [:post, :put, :delete]
55
+ allow << :get if MERB_ENV == 'development'
56
+ if @params.key?(:_method) && allow.include?(@method)
57
+ @method = @params.delete(:_method).downcase.intern
58
+ end
90
59
  @request = Request.new(@env, @method)
91
- MERB_LOGGER.info("Params: #{params.inspect}")
60
+
61
+ MERB_LOGGER.info("Params: #{params.inspect}\nCookies: #{cookies.inspect}")
92
62
  end
93
-
63
+
94
64
  def dispatch(action=:to_s)
95
65
  start = Time.now
96
66
  setup_session if respond_to?:setup_session
@@ -150,8 +120,37 @@ module Merb
150
120
  @session
151
121
  end
152
122
 
123
+ # accessor for @response. Please use response and
124
+ # never @response directly.
125
+ def response
126
+ @response
127
+ end
128
+
129
+ trait :template_extensions => { }
130
+ trait :template_root => File.expand_path(MERB_ROOT / "dist/app/views")
131
+ trait :layout_root => File.expand_path(MERB_ROOT / "dist/app/views/layout")
132
+ # lookup the trait[:template_extensions] for the extname of the filename
133
+ # you pass. Answers with the engine that matches the extension, Template::Erubis
134
+ # is used if none matches.
135
+ def engine_for(file)
136
+ extension = File.extname(file)[1..-1]
137
+ ancestral_trait[:template_extensions][extension]
138
+ rescue
139
+ ::Merb::Template::Erubis
140
+ end
141
+
142
+ # This method is called by templating-engines to register themselves with
143
+ # a list of extensions that will be looked up on #render of an action.
144
+ def self.register_engine(engine, *extensions)
145
+ [extensions].flatten.uniq.each do |ext|
146
+ trait[:template_extensions][ext] = engine
147
+ end
148
+ end
149
+
150
+
151
+
153
152
  # shared_accessor sets up a class instance variable that can
154
- # be unique for each class but also inherits the meta attrs
153
+ # be unique for each class but also inherits the shared attrs
155
154
  # from its superclasses. Since @@class variables are almost
156
155
  # global vars within an inheritance tree, we use
157
156
  # @class_instance_variables instead
@@ -260,14 +259,17 @@ module Merb
260
259
  "You can specify either :only or :exclude but
261
260
  not both at the same time for the same filter."
262
261
  ) if opts.has_key?(:only) && opts.has_key?(:exclude)
263
-
264
- raise(ArgumentError,
265
- 'after filters can only be a Proc object'
266
- ) unless Proc === filter
267
-
262
+
268
263
  opts = shuffle_filters!(opts)
269
264
 
270
- (self.after_filters ||= []) << [filter, opts]
265
+ case filter
266
+ when Symbol, Proc, String
267
+ (self.after_filters ||= []) << [filter, opts]
268
+ else
269
+ raise(ArgumentError,
270
+ 'After filters need to be either a Symbol, String or a Proc'
271
+ )
272
+ end
271
273
  end
272
274
 
273
275
  def self.shuffle_filters!(opts={})
@@ -0,0 +1,72 @@
1
+ module Merb
2
+
3
+ class Dispatcher
4
+ class << self
5
+
6
+ attr_accessor :path_prefix
7
+
8
+ @@mutex = Mutex.new
9
+ @@use_mutex = ::Merb::Server.use_mutex
10
+ # This is where we grab the incoming request PATH_INFO
11
+ # and use that in the merb routematcher to determine
12
+ # which controller and method to run.
13
+ # returns a 2 element tuple of:
14
+ # [controller, action]
15
+ def handle(request, response)
16
+ request_uri = request.params[Mongrel::Const::REQUEST_URI]
17
+ request_uri.sub!(path_prefix, '') if path_prefix
18
+ route = route_path(request_uri)
19
+
20
+ allowed = route.delete(:allowed)
21
+ rest = route.delete(:rest)
22
+
23
+ controller = instantiate_controller(route[:controller], request.body, request.params, route, response)
24
+
25
+ if rest
26
+ method = controller.request.method
27
+ if allowed.keys.include?(method) && action = allowed[method]
28
+ controller.params[:action] = action
29
+ else
30
+ raise Merb::RestfulMethodNotAllowed.new(method, allowed)
31
+ end
32
+ else
33
+ action = route[:action]
34
+ end
35
+ if @@use_mutex
36
+ @@mutex.synchronize {
37
+ controller.dispatch(action)
38
+ }
39
+ else
40
+ controller.dispatch(action)
41
+ end
42
+ [controller, action]
43
+ end
44
+
45
+ def route_path(path)
46
+ path = path.sub(/\/+/, '/').sub(/\?.*$/, '')
47
+ path = path[0..-2] if (path[-1] == ?/) && path.size > 1
48
+ Merb::RouteMatcher.new.route_request(path)
49
+ end
50
+
51
+ # take a controller class name string and reload or require
52
+ # the right controller file then CamelCase it and turn it
53
+ # into a new object passing in the request and response.
54
+ # this is where your Merb::Controller is instantiated.
55
+ def instantiate_controller(controller_name, req, env, params, res)
56
+ if !File.exist?(DIST_ROOT+"/app/controllers/#{controller_name.snake_case}.rb")
57
+ raise "Bad controller! #{controller_name.snake_case}"
58
+ end unless $TESTING
59
+ begin
60
+ controller_name.import
61
+ return Object.const_get( controller_name.camel_case ).new(req, env, params, res)
62
+ rescue RuntimeError
63
+ warn "Error getting instance of '#{controller_name.camel_case}': #{$!}"
64
+ raise $!
65
+ end
66
+ end
67
+
68
+ end # end class << self
69
+
70
+ end
71
+
72
+ end
@@ -8,7 +8,12 @@ module Merb
8
8
  class Noroutefound < MerbError; end
9
9
  class MissingControllerFile < MerbError; end
10
10
  class MerbControllerError < MerbError; end
11
-
11
+ class RestfulMethodNotAllowed < MerbError
12
+ def initialize(method, allowed)
13
+ super("RestfulMethodNotAllowed: #{method}\n"+ "Allowed: #{allowed.keys.join(' ')})")
14
+ end
15
+
16
+ end
12
17
  # format exception message for browser display
13
18
  def self.html_exception(e)
14
19
  ::Merb::Server.show_error ? ErrorResponse.new(e).out : "500 Internal Server Error!"
@@ -1,3 +1,16 @@
1
+ class Mongrel::HttpResponse
2
+ NO_CLOSE_STATUS_FORMAT = "HTTP/1.1 %d %s\r\n".freeze
3
+ def send_status_no_connection_close(content_length=@body.length)
4
+ if not @status_sent
5
+ @header['Content-Length'] = content_length unless @status == 304
6
+ write(NO_CLOSE_STATUS_FORMAT % [@status, Mongrel::HTTP_STATUS_CODES[@status]])
7
+ @status_sent = true
8
+ end
9
+ end
10
+ end
11
+
12
+
13
+
1
14
  class MerbHandler < Mongrel::HttpHandler
2
15
  @@file_only_methods = ["GET","HEAD"]
3
16
 
@@ -6,7 +19,6 @@ class MerbHandler < Mongrel::HttpHandler
6
19
  # by default.
7
20
  def initialize(dir, opts = {})
8
21
  @files = Mongrel::DirHandler.new(dir,false)
9
- @guard = Mutex.new
10
22
  end
11
23
 
12
24
  # process incoming http requests and do a number of things
@@ -30,7 +42,7 @@ class MerbHandler < Mongrel::HttpHandler
30
42
  return
31
43
  end
32
44
 
33
- MERB_LOGGER.info("\nRequest: PATH_INFO: #{request.params[Mongrel::Const::PATH_INFO]} (#{Time.now.strftime("%Y-%m-%d %H:%M:%S")})")
45
+ MERB_LOGGER.info("\nRequest: REQUEST_URI: #{request.params[Mongrel::Const::REQUEST_URI]} (#{Time.now.strftime("%Y-%m-%d %H:%M:%S")})")
34
46
 
35
47
  # Rails style page caching. Check the public dir first for
36
48
  # .html pages and serve directly. Otherwise fall back to Merb
@@ -50,23 +62,11 @@ class MerbHandler < Mongrel::HttpHandler
50
62
  @files.process(request,response)
51
63
  else
52
64
  begin
53
- # This handles parsing the query string and post/file upload
54
- # params and is outside of the synchronize call so that
55
- # multiple file uploads can be done at once.
65
+ # dLet Merb:Dispatcher find the route and call the filter chain and action
56
66
  controller = nil
57
- controller, action = handle(request)
58
- MERB_LOGGER.info("Routing to controller: #{controller.class} action: #{action}\nParsing HTTP Input took: #{Time.now - start} seconds")
67
+ controller, action = Merb::Dispatcher.handle(request, response)
59
68
 
60
- # We need a mutex here because ActiveRecord code can be run
61
- # in your controller actions. AR performs much better in single
62
- # threaded mode so we lock here for the shortest amount of time
63
- # possible. Route recognition and mime parsing has already occured
64
- # at this point because those processes are thread safe. This
65
- # gives us the best trade off for multi threaded performance
66
- # of thread safe, and a lock around calls to your controller actions.
67
- @guard.synchronize {
68
- controller.dispatch(action)
69
- }
69
+ MERB_LOGGER.info("Routing to controller: #{controller.class} action: #{action}\nRoute Recognition & Parsing HTTP Input took: #{Time.now - start} seconds")
70
70
  rescue Object => e
71
71
  response.start(500) do |head,out|
72
72
  head["Content-Type"] = "text/html"
@@ -113,6 +113,8 @@ class MerbHandler < Mongrel::HttpHandler
113
113
  if controller.body.respond_to? :close
114
114
  controller.body.close
115
115
  end
116
+ elsif Proc === controller.body
117
+ controller.body.call
116
118
  else
117
119
  MERB_LOGGER.info("Response status: #{response.status}\nComplete Request took: #{Time.now - start} seconds\n\n")
118
120
  # render response from successful controller
@@ -124,34 +126,4 @@ class MerbHandler < Mongrel::HttpHandler
124
126
  end
125
127
  end
126
128
 
127
- # This is where we grab the incoming request PATH_INFO
128
- # and use that in the merb routematcher to determine
129
- # which controller and method to run.
130
- # returns a 2 element tuple of:
131
- # [controller, action]
132
- def handle(request)
133
- path = request.params[Mongrel::Const::PATH_INFO].sub(/\/+/, '/')
134
- path = path[0..-2] if (path[-1] == ?/) && path.size > 1
135
- route = Merb::RouteMatcher.new.route_request(path)
136
- [ instantiate_controller(route[:controller], request.body, request.params, route),
137
- route[:action] ]
138
- end
139
-
140
- # take a controller class name string and reload or require
141
- # the right controller file then CamelCase it and turn it
142
- # into a new object passing in the request and response.
143
- # this is where your Merb::Controller is instantiated.
144
- def instantiate_controller(controller_name, req, env, params)
145
- if !File.exist?(DIST_ROOT+"/app/controllers/#{controller_name.snake_case}.rb")
146
- raise Merb::MissingControllerFile
147
- end
148
- begin
149
- controller_name.import
150
- return Object.const_get( controller_name.camel_case ).new(req, env, params)
151
- rescue RuntimeError
152
- warn "Error getting instance of '#{controller_name.camel_case}': #{$!}"
153
- raise $!
154
- end
155
- end
156
-
157
129
  end