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
@@ -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