diff --git a/bin/merb b/bin/merb old mode 100644 new mode 100755 diff --git a/lib/merb.rb b/lib/merb.rb index 76cb3e269e46fdf9b63cda7cb563c6cf40fdcb15..a2ab4ed47f9cb2ab942da5c46a2b561758a0d704 100644 --- a/lib/merb.rb +++ b/lib/merb.rb @@ -15,7 +15,7 @@ require 'merb_core/core_ext' require 'merb_core/gem_ext/erubis' require 'merb_core/logger' require 'merb_core/version' - +require 'merb_core/controller/mime' module Merb class << self @@ -23,6 +23,7 @@ module Merb def start(argv=ARGV) Merb::Config.parse_args(argv) BootLoader.run + case Merb::Config[:adapter] when "mongrel" adapter = Merb::Rack::Mongrel diff --git a/lib/merb_core/boot/bootloader.rb b/lib/merb_core/boot/bootloader.rb index d873924860bf4da06ac93db5c6a188f63dd1c3cc..57da75f05e28e8a256922bf345ccd3902e0a0b02 100644 --- a/lib/merb_core/boot/bootloader.rb +++ b/lib/merb_core/boot/bootloader.rb @@ -20,7 +20,7 @@ module Merb end def run - subclasses.each {|klass| Object.full_const_get(klass).new.run } + subclasses.each {|klass| Object.full_const_get(klass).run } end def after(klass) @@ -37,95 +37,128 @@ module Merb end -class Merb::BootLoader::BuildFramework < Merb::BootLoader - def run - build_framework +class Merb::BootLoader::LoadInit < Merb::BootLoader + def self.run + if Merb::Config[:init_file] + require Merb.root / Merb::Config[:init_file] + elsif File.exists?(Merb.root / "config" / "merb_init.rb") + require Merb.root / "config" / "merb_init" + elsif File.exists?(Merb.root / "merb_init.rb") + require Merb.root / "merb_init" + elsif File.exists?(Merb.root / "application.rb") + require Merb.root / "application" + end + end +end + +class Merb::BootLoader::Environment < Merb::BootLoader + def self.run + Merb.environment = Merb::Config[:environment] + end +end + +class Merb::BootLoader::Logger < Merb::BootLoader + def self.run + Merb.logger = Merb::Logger.new(Merb.dir_for(:log) / "test_log") + Merb.logger.level = Merb::Logger.const_get(Merb::Config[:log_level].upcase) rescue Merb::Logger::INFO end +end + +class Merb::BootLoader::BuildFramework < Merb::BootLoader + class << self + def run + build_framework + end - # This method should be overridden in merb_init.rb before Merb.start to set up a different - # framework structure - def build_framework - %[view model controller helper mailer part].each do |component| - Merb.push_path(component.to_sym, Merb.root_path("app/#{component}s")) + # This method should be overridden in merb_init.rb before Merb.start to set up a different + # framework structure + def build_framework + %w[view model controller helper mailer part].each do |component| + Merb.push_path(component.to_sym, Merb.root_path("app/#{component}s")) + end + Merb.push_path(:application, Merb.root_path("app/controllers/application.rb")) + Merb.push_path(:config, Merb.root_path("config/router.rb")) + Merb.push_path(:lib, Merb.root_path("lib")) end - Merb.push_path(:application, Merb.root_path("app/controllers/application.rb")) - Merb.push_path(:config, Merb.root_path("config/router.rb")) - Merb.push_path(:lib, Merb.root_path("lib")) end end class Merb::BootLoader::LoadPaths < Merb::BootLoader LOADED_CLASSES = {} - def run - # Add models, controllers, and lib to the load path - $LOAD_PATH.unshift Merb.load_paths[:model].first if Merb.load_paths[:model] - $LOAD_PATH.unshift Merb.load_paths[:controller].first if Merb.load_paths[:controller] - $LOAD_PATH.unshift Merb.load_paths[:lib].first if Merb.load_paths[:lib] + class << self + def run + # Add models, controllers, and lib to the load path + $LOAD_PATH.unshift Merb.load_paths[:model].first if Merb.load_paths[:model] + $LOAD_PATH.unshift Merb.load_paths[:controller].first if Merb.load_paths[:controller] + $LOAD_PATH.unshift Merb.load_paths[:lib].first if Merb.load_paths[:lib] - # Require all the files in the registered load paths - puts Merb.load_paths.inspect - Merb.load_paths.each do |name, path| - Dir[path.first / path.last].each do |file| - klasses = ObjectSpace.classes.dup - require f - LOADED_CLASSES[file] = ObjectSpace.classes - klasses + # Require all the files in the registered load paths + puts Merb.load_paths.inspect + Merb.load_paths.each do |name, path| + Dir[path.first / path.last].each do |file| + klasses = ObjectSpace.classes.dup + require file + LOADED_CLASSES[file] = ObjectSpace.classes - klasses + end end end - end - def reload(file) - if klasses = LOADED_CLASSES[file] - klasses.each do |klass| - remove_constant(klass) + def reload(file) + if klasses = LOADED_CLASSES[file] + klasses.each do |klass| + remove_constant(klass) + end end + load file end - load file - end - def remove_constant(const) - # This is to support superclasses (like AbstractController) that track - # their subclasses in a class variable. Classes that wish to use this - # functionality are required to alias it to _subclasses_list. Plugins - # for ORMs and other libraries should keep this in mind. - if klass.superclass.respond_to?(:_subclasses_list) - klass.superclass.send(:_subclasses_list).delete(klass) - klass.superclass.send(:_subclasses_list).delete(klass.to_s) - end + def remove_constant(const) + # This is to support superclasses (like AbstractController) that track + # their subclasses in a class variable. Classes that wish to use this + # functionality are required to alias it to _subclasses_list. Plugins + # for ORMs and other libraries should keep this in mind. + if klass.superclass.respond_to?(:_subclasses_list) + klass.superclass.send(:_subclasses_list).delete(klass) + klass.superclass.send(:_subclasses_list).delete(klass.to_s) + end - parts = const.to_s.split("::") - base = parts.size == 1 ? Object : Object.full_const_get(parts[0..-2].join("::")) - object = parts[-1].intern - Merb.logger.debugger("Removing constant #{object} from #{base}") - base.send(:remove_const, object) if object + parts = const.to_s.split("::") + base = parts.size == 1 ? Object : Object.full_const_get(parts[0..-2].join("::")) + object = parts[-1].intern + Merb.logger.debugger("Removing constant #{object} from #{base}") + base.send(:remove_const, object) if object + end end end class Merb::BootLoader::Templates < Merb::BootLoader - def run - template_paths.each do |path| - Merb::Template.inline_template(path) + class << self + def run + template_paths.each do |path| + Merb::Template.inline_template(path) + end end - end - def template_paths - extension_glob = "{#{Merb::Template::EXTENSIONS.keys.join(',')}}" + def template_paths + extension_glob = "{#{Merb::Template::EXTENSIONS.keys.join(',')}}" - # This gets all templates set in the controllers template roots - # We separate the two maps because most of controllers will have - # the same _template_root, so it's silly to be globbing the same - # path over and over. - template_paths = Merb::AbstractController._abstract_subclasses.map do |klass| - Object.full_const_get(klass)._template_root - end.uniq.map {|path| Dir["#{path}/**/*.#{extension_glob}"] } + # This gets all templates set in the controllers template roots + # We separate the two maps because most of controllers will have + # the same _template_root, so it's silly to be globbing the same + # path over and over. + template_paths = Merb::AbstractController._abstract_subclasses.map do |klass| + Object.full_const_get(klass)._template_root + end.uniq.compact.map {|path| Dir["#{path}/**/*.#{extension_glob}"] } - # This gets the templates that might be created outside controllers - # template roots. eg app/views/shared/* - template_paths << Dir["#{Merb.dir_for(:view)}/**/*.#{extension_glob}"] if Merb.dir_for(:view) + # This gets the templates that might be created outside controllers + # template roots. eg app/views/shared/* + template_paths << Dir["#{Merb.dir_for(:view)}/**/*.#{extension_glob}"] if Merb.dir_for(:view) - template_paths.flatten.compact.uniq - end + template_paths.flatten.compact.uniq + end + end end class Merb::BootLoader::Libraries < Merb::BootLoader @@ -145,18 +178,41 @@ class Merb::BootLoader::Libraries < Merb::BootLoader def self.add_libraries(hsh) @@libraries.merge!(hsh) end - - def run + + def self.run @@libraries.each do |exclude, choices| require_first_working(*choices) unless Merb::Config[exclude] end end - - def require_first_working(first, *rest) + + def self.require_first_working(first, *rest) p first, rest require first rescue LoadError raise LoadError if rest.empty? require_first_working rest.unshift, *rest end +end + +class Merb::BootLoader::MimeTypes < Merb::BootLoader + def self.run + # Sets the default mime-types + # + # By default, the mime-types include: + # :all:: no transform, */* + # :yaml:: to_yaml, application/x-yaml or text/yaml + # :text:: to_text, text/plain + # :html:: to_html, text/html or application/xhtml+xml or application/html + # :xml:: to_xml, application/xml or text/xml or application/x-xml, adds "Encoding: UTF-8" response header + # :js:: to_json, text/javascript ot application/javascript or application/x-javascript + # :json:: to_json, application/json or text/x-json + Merb.available_mime_types.clear + Merb.add_mime_type(:all, nil, %w[*/*]) + Merb.add_mime_type(:yaml, :to_yaml, %w[application/x-yaml text/yaml]) + Merb.add_mime_type(:text, :to_text, %w[text/plain]) + Merb.add_mime_type(:html, :to_html, %w[text/html application/xhtml+xml application/html]) + Merb.add_mime_type(:xml, :to_xml, %w[application/xml text/xml application/x-xml], :Encoding => "UTF-8") + Merb.add_mime_type(:js, :to_json, %w[text/javascript application/javascript application/x-javascript]) + Merb.add_mime_type(:json, :to_json, %w[application/json text/x-json]) + end end \ No newline at end of file diff --git a/lib/merb_core/config.rb b/lib/merb_core/config.rb index c92f2e6f071c234551ecb16a4716d47fa92f6c7b..ab0864e0174b54833c758f9f22a840d3b53c7653 100644 --- a/lib/merb_core/config.rb +++ b/lib/merb_core/config.rb @@ -92,6 +92,10 @@ module Merb options[:cluster] = nodes end + opts.on("-I", "--init-file FILE", "Name of the file to load first") do |init_file| + options[:init_file] = init_file + end + opts.on("-p", "--port PORTNUM", "Port to run merb on, defaults to 4000.") do |port| options[:port] = port end @@ -261,29 +265,29 @@ module Merb @configuration = Merb::Config.apply_configuration_from_file options, environment_merb_yml - case Merb::Config[:environment].to_s - when 'production' - Merb::Config[:reloader] = Merb::Config.fetch(:reloader, false) - Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, false) - Merb::Config[:cache_templates] = true - else - Merb::Config[:reloader] = Merb::Config.fetch(:reloader, true) - Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, true) - end - - Merb::Config[:reloader_time] ||= 0.5 if Merb::Config[:reloader] == true - - - if Merb::Config[:reloader] - Thread.abort_on_exception = true - Thread.new do - loop do - sleep( Merb::Config[:reloader_time] ) - ::Merb::BootLoader.reload if ::Merb::BootLoader.app_loaded? - end - Thread.exit - end - end + # case Merb::Config[:environment].to_s + # when 'production' + # Merb::Config[:reloader] = Merb::Config.fetch(:reloader, false) + # Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, false) + # Merb::Config[:cache_templates] = true + # else + # Merb::Config[:reloader] = Merb::Config.fetch(:reloader, true) + # Merb::Config[:exception_details] = Merb::Config.fetch(:exception_details, true) + # end + # + # Merb::Config[:reloader_time] ||= 0.5 if Merb::Config[:reloader] == true + # + # + # if Merb::Config[:reloader] + # Thread.abort_on_exception = true + # Thread.new do + # loop do + # sleep( Merb::Config[:reloader_time] ) + # ::Merb::BootLoader.reload if ::Merb::BootLoader.app_loaded? + # end + # Thread.exit + # end + # end @configuration end diff --git a/lib/merb_core/controller/abstract_controller.rb b/lib/merb_core/controller/abstract_controller.rb index fbf83372793da6da4b803b799994f0e341fddf88..f5e9a59057d67a6d56377a516a726cf51aa03d6f 100644 --- a/lib/merb_core/controller/abstract_controller.rb +++ b/lib/merb_core/controller/abstract_controller.rb @@ -96,7 +96,7 @@ class Merb::AbstractController # the superclass. #--- # @public - def _template_location(action, controller = controller_name, type = nil) + def _template_location(action, type = nil, controller = controller_name) "#{controller}/#{action}" end @@ -106,6 +106,8 @@ class Merb::AbstractController # own subclasses. We're using a Set so we don't have to worry about # uniqueness. self._abstract_subclasses = Set.new + self._template_root = Merb.dir_for(:view) + def self.subclasses_list() _abstract_subclasses end class << self @@ -114,7 +116,6 @@ class Merb::AbstractController # The controller that is being inherited from Merb::AbstractController def inherited(klass) _abstract_subclasses << klass.to_s - klass._template_root ||= Merb.dir_for(:view) super end diff --git a/lib/merb_core/controller/merb_controller.rb b/lib/merb_core/controller/merb_controller.rb index 7283f006bb0501b29f825da129600cf045264b62..98af6ef3330a6b3f46d7bb1f8643261e28155ae5 100644 --- a/lib/merb_core/controller/merb_controller.rb +++ b/lib/merb_core/controller/merb_controller.rb @@ -71,6 +71,10 @@ class Merb::Controller < Merb::AbstractController end end + def _template_location(action, type = nil, controller = controller_name) + "#{controller}/#{action}.#{type}" + end + # Sets the variables that came in through the dispatch as available to # the controller. This is called by .build, so see it for more # information. @@ -107,9 +111,7 @@ class Merb::Controller < Merb::AbstractController request.cookies[_session_id_key] = request.params[_session_id_key] end end - @_request, @_response, @_status, @_headers = - request, response, status, headers - + @request, @response, @status, @headers = request, response, status, headers nil end @@ -135,7 +137,8 @@ class Merb::Controller < Merb::AbstractController @_benchmarks[:action_time] = Time.now - start end - _attr_reader :request, :response, :status, :headers + attr_reader :request, :response, :headers + attr_accessor :status def params() request.params end def cookies() request.cookies end def session() request.session end diff --git a/lib/merb_core/controller/mime.rb b/lib/merb_core/controller/mime.rb index d17570786ca318cff7201c4b1e947ae229b01de8..ff9abe4d1c452aeabfcf5f7dc7a2c7cdd3f67035 100644 --- a/lib/merb_core/controller/mime.rb +++ b/lib/merb_core/controller/mime.rb @@ -8,7 +8,7 @@ module Merb # Any specific outgoing headers should be included here. These are not # the content-type header but anything in addition to it. - # +tranform_method+ should be set to a symbol of the method used to + # +transform_method+ should be set to a symbol of the method used to # transform a resource into this mime type. # For example for the :xml mime type an object might be transformed by # calling :to_xml, or for the :js mime type, :to_json. @@ -71,27 +71,6 @@ module Merb def mime_by_request_header(header) available_mime_types.find {|key,info| info[request_headers].include?(header)}.first end - - # Resets the default mime-types - # - # By default, the mime-types include: - # :all:: no transform, */* - # :yaml:: to_yaml, application/x-yaml or text/yaml - # :text:: to_text, text/plain - # :html:: to_html, text/html or application/xhtml+xml or application/html - # :xml:: to_xml, application/xml or text/xml or application/x-xml, adds "Encoding: UTF-8" response header - # :js:: to_json, text/javascript ot application/javascript or application/x-javascript - # :json:: to_json, application/json or text/x-json - def reset_default_mime_types! - available_mime_types.clear - Merb.add_mime_type(:all, nil, %w[*/*]) - Merb.add_mime_type(:yaml, :to_yaml, %w[application/x-yaml text/yaml]) - Merb.add_mime_type(:text, :to_text, %w[text/plain]) - Merb.add_mime_type(:html, :to_html, %w[text/html application/xhtml+xml application/html]) - Merb.add_mime_type(:xml, :to_xml, %w[application/xml text/xml application/x-xml], :Encoding => "UTF-8") - Merb.add_mime_type(:js, :to_json, %w[text/javascript application/javascript application/x-javascript]) - Merb.add_mime_type(:json, :to_json, %w[application/json text/x-json]) - end end end \ No newline at end of file diff --git a/lib/merb_core/controller/mixins/render.rb b/lib/merb_core/controller/mixins/render.rb index 8e096546d4647bb597ab2e00a4b15d09db35e9c9..a298263af7d655d9ce43007554f3827046831287 100644 --- a/lib/merb_core/controller/mixins/render.rb +++ b/lib/merb_core/controller/mixins/render.rb @@ -51,21 +51,22 @@ module Merb::RenderMixin # If you don't specify a thing to render, assume they want to render the current action thing ||= action_name.to_sym - + # Content negotiation opts[:format] ? (self.content_type = opts[:format]) : content_type # Do we have a template to try to render? if thing.is_a?(Symbol) || opts[:template] - + # Find a template path to look up (_template_location adds flexibility here) - template_location = _template_root / (opts[:template] || _template_location(thing)) + template_location = _template_root / (opts[:template] || _template_location(thing, content_type)) + # Get the method name from the previously inlined list template_method = Merb::Template.template_for(template_location) # Raise an error if there's no template raise TemplateNotFound, "No template found at #{template_location}" unless - self.respond_to?(template_method) + template_method && self.respond_to?(template_method) # Call the method in question and throw the content for later consumption by the layout throw_content(:for_layout, self.send(template_method)) diff --git a/lib/merb_core/controller/mixins/responder.rb b/lib/merb_core/controller/mixins/responder.rb index e910b2b32c844ab51cf2a10d0ad26c314dbb3631..5ac67fb907aaf9f95effc7eb3cbb07b8963ce022 100644 --- a/lib/merb_core/controller/mixins/responder.rb +++ b/lib/merb_core/controller/mixins/responder.rb @@ -97,6 +97,8 @@ module Merb # and none of the provides methods can be used. module ResponderMixin + TYPES = {} + class ContentTypeAlreadySet < StandardError; end # ==== Parameters @@ -105,6 +107,7 @@ module Merb base.extend(ClassMethods) base.class_eval do class_inheritable_accessor :class_provided_formats + self.class_provided_formats = [] end base.reset_provides end @@ -178,171 +181,253 @@ module Merb def reset_provides only_provides(:html) end - - # ==== Returns - # The current list of formats provided for this instance of the controller. - # It starts with what has been set in the controller (or :html by default) - # but can be modifed on a per-action basis. - def _provided_formats - @_provided_formats ||= class_provided_formats.dup + end + + # ==== Returns + # The current list of formats provided for this instance of the controller. + # It starts with what has been set in the controller (or :html by default) + # but can be modifed on a per-action basis. + def _provided_formats + @_provided_formats ||= class_provided_formats.dup + end + + # Sets the provided formats for this action. Usually, you would + # use a combination of +provides+, +only_provides+ and +does_not_provide+ + # to manage this, but you can set it directly. + # + # ==== Parameters + # *formats:: A list of formats to be passed to provides + # + # ==== Raises + # Merb::ResponderMixin::ContentTypeAlreadySet:: + # Content negotiation already occured, and the content_type is set. + # + # ==== Returns + # Array:: List of formats passed in + def _set_provided_formats(*formats) + if @_content_type + raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set" end - - # Sets the provided formats for this action. Usually, you would - # use a combination of +provides+, +only_provides+ and +does_not_provide+ - # to manage this, but you can set it directly. - # - # ==== Parameters - # *formats:: A list of formats to be passed to provides - # - # ==== Raises - # Merb::ResponderMixin::ContentTypeAlreadySet:: - # Content negotiation already occured, and the content_type is set. - # - # ==== Returns - # Array:: List of formats passed in - def _set_provided_formats(*formats) - if @_content_type - raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set" - end - @_provided_formats = [] - provides(*formats) + @_provided_formats = [] + provides(*formats) + end + alias :_provided_formats= :_set_provided_formats + + # Adds formats to the list of provided formats for this particular + # request. Usually used to add formats to a single action. See also + # the controller-level provides that affects all actions in a controller. + # + # ==== Parameters + # *formats:: A list of formats to add to the per-action list + # of provided formats + # + # ==== Raises + # Merb::ResponderMixin::ContentTypeAlreadySet:: + # Content negotiation already occured, and the content_type is set. + # + # ==== Returns + # Array:: List of formats passed in + # + #--- + # @public + def provides(*formats) + if @_content_type + raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set" end - alias :_provided_formats= :_set_provided_formats - - # Adds formats to the list of provided formats for this particular - # request. Usually used to add formats to a single action. See also - # the controller-level provides that affects all actions in a controller. - # - # ==== Parameters - # *formats:: A list of formats to add to the per-action list - # of provided formats - # - # ==== Raises - # Merb::ResponderMixin::ContentTypeAlreadySet:: - # Content negotiation already occured, and the content_type is set. - # - # ==== Returns - # Array:: List of formats passed in - # - #--- - # @public - def provides(*formats) - if @_content_type - raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set" - end - formats.each do |fmt| - _provided_formats << fmt unless _provided_formats.include?(fmt) - end + formats.each do |fmt| + _provided_formats << fmt unless _provided_formats.include?(fmt) end + end - # Sets list of provided formats for this particular - # request. Usually used to limit formats to a single action. See also - # the controller-level only_provides that affects all actions - # in a controller. - # - # ==== Parameters - # *formats:: A list of formats to use as the per-action list - # of provided formats - # - # ==== Returns - # Array:: List of formats passed in - # - #--- - # @public - def only_provides(*formats) - self._provided_formats = *formats - end - - # Removes formats from the list of provided formats for this particular - # request. Usually used to remove formats from a single action. See - # also the controller-level does_not_provide that affects all actions in a - # controller. - # - # ==== Parameters - # *formats:: Registered mime-type - # - # ==== Returns - # Array:: List of formats that remain after removing the ones not to provide - # - #--- - # @public - def does_not_provide(*formats) - formats.flatten! - self._provided_formats -= formats - end - - # Do the content negotiation: - # 1. if params[:format] is there, and provided, use it - # 2. Parse the Accept header - # 3. If it's */*, use the first provided format - # 4. Look for one that is provided, in order of request - # 5. Raise 406 if none found - def _perform_content_negotiation # :nodoc: - raise Merb::ControllerExceptions::NotAcceptable if provided_formats.empty? - if fmt = params[:format] - return fmt.to_sym if provided_formats.include?(fmt.to_sym) - else - accepts = Responder.parse(request.accept).map {|t| t.to_sym} - return provided_formats.first if accepts.include?(:all) - return accepts.each { |type| break type if provided_formats.include?(type) } - end - raise Merb::ControllerExceptions::NotAcceptable + # Sets list of provided formats for this particular + # request. Usually used to limit formats to a single action. See also + # the controller-level only_provides that affects all actions + # in a controller. + # + # ==== Parameters + # *formats:: A list of formats to use as the per-action list + # of provided formats + # + # ==== Returns + # Array:: List of formats passed in + # + #--- + # @public + def only_provides(*formats) + self._provided_formats = *formats + end + + # Removes formats from the list of provided formats for this particular + # request. Usually used to remove formats from a single action. See + # also the controller-level does_not_provide that affects all actions in a + # controller. + # + # ==== Parameters + # *formats:: Registered mime-type + # + # ==== Returns + # Array:: List of formats that remain after removing the ones not to provide + # + #--- + # @public + def does_not_provide(*formats) + formats.flatten! + self._provided_formats -= formats + end + + # Do the content negotiation: + # 1. if params[:format] is there, and provided, use it + # 2. Parse the Accept header + # 3. If it's */*, use the first provided format + # 4. Look for one that is provided, in order of request + # 5. Raise 406 if none found + def _perform_content_negotiation # :nodoc: + raise Merb::ControllerExceptions::NotAcceptable if _provided_formats.empty? + if fmt = params[:format] && _provided_formats.include?(fmt.to_sym) + return fmt.to_sym end + accepts = Responder.parse(request.accept).map {|t| t.to_sym} + return _provided_formats.first if accepts.include?(:all) + (accepts & _provided_formats).first || (raise Merb::ControllerExceptions::NotAcceptable) + end - # Returns the output format for this request, based on the - # provided formats, params[:format] and the client's HTTP - # Accept header. - # - # The first time this is called, it triggers content negotiation - # and caches the value. Once you call +content_type+ you can - # not set or change the list of provided formats. - # - # Called automatically by +render+, so you should only call it if - # you need the value, not to trigger content negotiation. - # - # ==== Parameters - # fmt:: - # An optional format to use instead of performing content negotiation. - # This can be used to pass in the values of opts[:format] from the - # render function to short-circuit content-negotiation when it's not - # necessary. This optional parameter should not be considered part - # of the public API. - # - # ==== Returns - # Symbol:: The content-type that will be used for this controller. - # - #--- - # @public - def content_type(fmt = nil) - self.content_type = (fmt || _perform_content_negotiation) unless @_content_type - @_content_type + # Returns the output format for this request, based on the + # provided formats, params[:format] and the client's HTTP + # Accept header. + # + # The first time this is called, it triggers content negotiation + # and caches the value. Once you call +content_type+ you can + # not set or change the list of provided formats. + # + # Called automatically by +render+, so you should only call it if + # you need the value, not to trigger content negotiation. + # + # ==== Parameters + # fmt:: + # An optional format to use instead of performing content negotiation. + # This can be used to pass in the values of opts[:format] from the + # render function to short-circuit content-negotiation when it's not + # necessary. This optional parameter should not be considered part + # of the public API. + # + # ==== Returns + # Symbol:: The content-type that will be used for this controller. + # + #--- + # @public + def content_type(fmt = nil) + @_content_type = (fmt || _perform_content_negotiation) unless @_content_type + @_content_type + end + + # Sets the content type of the current response to a value based on + # a passed in key. The Content-Type header will be set to the first + # registered header for the mime-type. + # + # ==== Parameters + # type:: A type that is in the list of registered mime-types. + # + # ==== Raises + # ArgumentError:: "type" is not in the list of registered mime-types. + # + # ==== Returns + # Symbol:: The content-type that was passed in. + # + #--- + # @semipublic + def content_type=(type) + unless Merb.available_mime_types.has_key?(type) + raise Merb::ControllerExceptions::NotAcceptable.new("Unknown content_type for response: #{type}") + end + headers['Content-Type'] = Merb.available_mime_types[type].first + @_content_type = type + end + + end + + class Responder + + protected + def self.parse(accept_header) + # parse the raw accept header into a unique, sorted array of AcceptType objects + list = accept_header.to_s.split(/,/).enum_for(:each_with_index).map do |entry,index| + AcceptType.new(entry,index += 1) + end.sort.uniq + # firefox (and possibly other browsers) send broken default accept headers. + # fix them up by sorting alternate xml forms (namely application/xhtml+xml) + # ahead of pure xml types (application/xml,text/xml). + if app_xml = list.detect{|e| e.super_range == 'application/xml'} + list.select{|e| e.to_s =~ /\+xml/}.each { |acc_type| + list[list.index(acc_type)],list[list.index(app_xml)] = + list[list.index(app_xml)],list[list.index(acc_type)] } end - - # Sets the content type of the current response to a value based on - # a passed in key. The Content-Type header will be set to the first - # registered header for the mime-type. - # - # ==== Parameters - # type:: A type that is in the list of registered mime-types. - # - # ==== Raises - # ArgumentError:: "type" is not in the list of registered mime-types. - # - # ==== Returns - # Symbol:: The content-type that was passed in. - # - #--- - # @semipublic - def content_type=(type) - unless Merb.available_mime_types.has_key?(type) - raise Merb::ControllerExceptions::NotAcceptable.new("Unknown content_type for response: #{type}") - end - headers['Content-Type'] = Merb.available_mime_types[type].first - @_content_type = type + list + end + + public + def self.params_to_query_string(value, prefix = nil) + case value + when Array + value.map { |v| + params_to_query_string(v, "#{prefix}[]") + } * "&" + when Hash + value.map { |k, v| + params_to_query_string(v, prefix ? "#{prefix}[#{Merb::Request.escape(k)}]" : Merb::Request.escape(k)) + } * "&" + else + "#{prefix}=#{Merb::Request.escape(value)}" end + end - end + end + + class AcceptType + + attr_reader :media_range, :quality, :index, :type, :sub_type + def initialize(entry,index) + @index = index + @media_range, quality = entry.split(/;\s*q=/).map{|a| a.strip } + @type, @sub_type = @media_range.split(/\//) + quality ||= 0.0 if @media_range == '*/*' + @quality = ((quality || 1.0).to_f * 100).to_i + end + + def <=>(entry) + c = entry.quality <=> quality + c = index <=> entry.index if c == 0 + c + end + + def eql?(entry) + synonyms.include?(entry.media_range) + end + + def ==(entry); eql?(entry); end + + def hash; super_range.hash; end + + def synonyms + @syns ||= Merb.available_mime_types.values.map do |e| + e[:request_headers] if e[:request_headers].include?(@media_range) + end.compact.flatten + end + + def super_range + synonyms.first || @media_range + end + + def to_sym + Merb.available_mime_types.select{|k,v| + v[:request_headers] == synonyms || v[:request_headers][0] == synonyms[0]}.flatten.first + end + + def to_s + @media_range + end + end + end \ No newline at end of file diff --git a/lib/merb_core/dispatch/dispatcher.rb b/lib/merb_core/dispatch/dispatcher.rb index c458c9f9ad454d3b0c3055d6b2a8e88b17712b44..f7fed0f539a20f9cce08b72c551725ad0563bf37 100644 --- a/lib/merb_core/dispatch/dispatcher.rb +++ b/lib/merb_core/dispatch/dispatcher.rb @@ -33,10 +33,10 @@ class Merb::Dispatcher # this is the custom dispatch_exception; it allows failures to still be dispatched # to the error controller - rescue => exception - Merb.logger.error(Merb.exception(exception)) - exception = controller_exception(exception) - dispatch_exception(request, response, exception) + # rescue => exception + # Merb.logger.error(Merb.exception(exception)) + # exception = controller_exception(exception) + # dispatch_exception(request, response, exception) end private @@ -49,10 +49,10 @@ class Merb::Dispatcher def dispatch_action(klass, action, request, response, status=200) # build controller controller = klass.build(request, response, status) - if @@use_mutex - @@mutex.synchronize { controller.dispatch(action) } + if use_mutex + @@mutex.synchronize { controller._dispatch(action) } else - controller.dispatch(action) + controller._dispatch(action) end [controller, action] end diff --git a/lib/merb_core/rack/adapter.rb b/lib/merb_core/rack/adapter.rb index ffc7117e9733e83b0567bbe4a43fac7663800b7d..217399a5382d0b3878aaea3d3e302173c5b5f119 100644 --- a/lib/merb_core/rack/adapter.rb +++ b/lib/merb_core/rack/adapter.rb @@ -40,7 +40,7 @@ module Merb begin controller, action = ::Merb::Dispatcher.handle(request, response) rescue Object => e - return [500, {"Content-Type"=>"text/html"}, "Internal Server Error"] + return [500, {"Content-Type"=>"text/html"}, e.message + "
" + e.backtrace.join("
")] end [controller.status, controller.headers, controller.body] end diff --git a/lib/merb_core/test/request_helper.rb b/lib/merb_core/test/request_helper.rb index 10a9fb3ace56eaf1db0fa300df3fb2ab88a7118a..f302a3b71539182ba142cd208fe6d6aae171b1a1 100644 --- a/lib/merb_core/test/request_helper.rb +++ b/lib/merb_core/test/request_helper.rb @@ -26,8 +26,10 @@ module Merb::Test::RequestHelper Merb::Test::FakeRequest.new(env, StringIO.new(req)) end - def dispatch_to(controller_klass, action, env = {}, opt = {}, &blk) - request = fake_request(env, opt) + def dispatch_to(controller_klass, action, params = {}, env = {}, &blk) + request = fake_request(env, + :query_string => Merb::Responder.params_to_query_string(params)) + controller = controller_klass.build(request) controller.instance_eval(&blk) if block_given? controller._dispatch(action) diff --git a/spec/public/abstract_controller/spec_helper.rb b/spec/public/abstract_controller/spec_helper.rb index df759008d14e7572b5c44de24f77f828f83f1682..694cee2592a210a5c1fa40ca7846beeaa09725fe 100644 --- a/spec/public/abstract_controller/spec_helper.rb +++ b/spec/public/abstract_controller/spec_helper.rb @@ -1,12 +1,10 @@ __DIR__ = File.dirname(__FILE__) require File.join(__DIR__, "..", "..", "spec_helper") -# The framework structure *must* be set up before loading in framework -# files. require File.join(__DIR__, "controllers", "filters") require File.join(__DIR__, "controllers", "render") -Merb::BootLoader::Templates.new.run +Merb::BootLoader::Templates.run module Merb::Test::Behaviors def dispatch_should_make_body(klass, body, action = :index) diff --git a/spec/public/controller/base_spec.rb b/spec/public/controller/base_spec.rb index 1709e612629ed2c2b6af4579a8b89684aca9aa3c..5bcdb59948cc22592639b1aee9bd233ff2c306fa 100644 --- a/spec/public/controller/base_spec.rb +++ b/spec/public/controller/base_spec.rb @@ -10,11 +10,11 @@ describe Merb::Controller, " callable actions" do end it "should dispatch to callable actions" do - dispatch_to(Merb::Test::Fixtures::TestFoo, :index).body.should == "index" + dispatch_to(Merb::Test::Fixtures::TestBase, :index).body.should == "index" end it "should not dispatch to hidden actions" do - calling { dispatch_to(Merb::Test::Fixtures::TestFoo, :hidden) }. + calling { dispatch_to(Merb::Test::Fixtures::TestBase, :hidden) }. should raise_error(Merb::ControllerExceptions::ActionNotFound) end diff --git a/spec/public/controller/controllers/base.rb b/spec/public/controller/controllers/base.rb index a1b3beb27899df781d943427d9b23945f02e14de..c4b69a440a9da3c3486208d2cb95ccb8bdb974b9 100644 --- a/spec/public/controller/controllers/base.rb +++ b/spec/public/controller/controllers/base.rb @@ -3,7 +3,7 @@ module Merb::Test::Fixtures self._template_root = File.dirname(__FILE__) / "views" end - class TestFoo < ControllerTesting + class TestBase < ControllerTesting def index "index" end diff --git a/spec/public/controller/controllers/responder.rb b/spec/public/controller/controllers/responder.rb new file mode 100644 index 0000000000000000000000000000000000000000..867192e8f6e995a43fd5cd3daffa0ec11b3d31e5 --- /dev/null +++ b/spec/public/controller/controllers/responder.rb @@ -0,0 +1,25 @@ +module Merb::Test::Fixtures + class ControllerTesting < Merb::Controller + self._template_root = File.dirname(__FILE__) / "views" + end + + class TestResponder < ControllerTesting + def index + render + end + end + + class TestHtmlDefault < TestResponder; end + + class TestClassProvides < TestResponder; + provides :xml + end + + class TestLocalProvides < TestResponder; + def index + provides :xml + render + end + end + +end \ No newline at end of file diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.html.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..1bfb77d4a44c444bba6888ae7740f7df4b074c58 --- /dev/null +++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.html.erb @@ -0,0 +1 @@ +This should not be rendered \ No newline at end of file diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.xml.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.xml.erb new file mode 100644 index 0000000000000000000000000000000000000000..7c91f633987348e87e5e34e1d9e87d9dd0e5100c --- /dev/null +++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_class_provides/index.xml.erb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_html_default/index.html.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_html_default/index.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..eb4b52bf5a7aaba8f1706de419f42789c05684a2 --- /dev/null +++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_html_default/index.html.erb @@ -0,0 +1 @@ +HTML: Default \ No newline at end of file diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.html.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..a3a841a89c62e6174038935a42da9cd24ff54413 --- /dev/null +++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.html.erb @@ -0,0 +1 @@ +This should not render \ No newline at end of file diff --git a/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.xml.erb b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.xml.erb new file mode 100644 index 0000000000000000000000000000000000000000..c1384ec6af0357b585cc367035d1bc3a30347ade --- /dev/null +++ b/spec/public/controller/controllers/views/merb/test/fixtures/test_local_provides/index.xml.erb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spec/public/controller/responder_spec.rb b/spec/public/controller/responder_spec.rb index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bcf18532442e5965cf6ca8501770d7b7a1eb2429 100644 --- a/spec/public/controller/responder_spec.rb +++ b/spec/public/controller/responder_spec.rb @@ -0,0 +1,31 @@ +require File.join(File.dirname(__FILE__), "spec_helper") + +describe Merb::Controller, " responds" do + + before do + Merb.push_path(:layout, File.dirname(__FILE__) / "controllers" / "views" / "layouts") + Merb::Router.prepare do |r| + r.default_routes + end + end + + it "should default the mime-type to HTML" do + dispatch_to(Merb::Test::Fixtures::TestHtmlDefault, :index).body.should == "HTML: Default" + end + + it "should use other mime-types if they are provided on the class level" do + controller = dispatch_to(Merb::Test::Fixtures::TestClassProvides, :index, {}, :http_accept => "application/xml") + controller.body.should == "" + end + + it "should fail if none of the acceptable mime-types are available" do + calling { dispatch_to(Merb::Test::Fixtures::TestClassProvides, :index, {}, :http_accept => "application/json") }. + should raise_error(Merb::ControllerExceptions::NotAcceptable) + end + + it "should use mime-types that are provided at the local level" do + controller = dispatch_to(Merb::Test::Fixtures::TestLocalProvides, :index, {}, :http_accept => "application/xml") + controller.body.should == "" + end + +end \ No newline at end of file diff --git a/spec/public/controller/spec_helper.rb b/spec/public/controller/spec_helper.rb index f68628a63740f4ce0235a15d71c5889e55ecaf78..e360194c1fbaf72c3298c61543c2d3a19b512b41 100644 --- a/spec/public/controller/spec_helper.rb +++ b/spec/public/controller/spec_helper.rb @@ -1,4 +1,10 @@ __DIR__ = File.dirname(__FILE__) +require 'ruby-debug' + require File.join(__DIR__, "..", "..", "spec_helper") -require File.join(__DIR__, "controllers", "base") \ No newline at end of file +require File.join(__DIR__, "controllers", "base") +require File.join(__DIR__, "controllers", "responder") + +Merb::BootLoader::Templates.run +Merb::BootLoader::MimeTypes.run \ No newline at end of file