diff --git a/README.markdown b/README.markdown index da2f4d7..a67bab5 100644 --- a/README.markdown +++ b/README.markdown @@ -129,6 +129,7 @@ end ## Parsing incoming documents In `#create` and `#update` actions it is often necessary to parse the incoming representation and map it to a model instance. Use the `#consume!` method for this. +The client must provide `Content-Type` request header with proper MIME type to let `#consume!` know what kind of parser it should use. ```ruby class SingersController < ApplicationController @@ -143,17 +144,22 @@ class SingersController < ApplicationController end ``` -The `consume!` call will roughly do the following. +If content type is set to `application/xml` the `consume!` call will roughly do the following. ```ruby singer. extend(SingerRepresenter) - from_json(request.body) + from_xml(request.body) ``` -So, `#consume!` helps you figuring out the representer module and reading the incoming document. +So, `#consume!` helps you figuring out the representer module and reading the incoming document. It also chooses +parser according to content type header provided in request. -Note that it respects settings from `#represents`. It uses the same mechanics known from `#respond_with` to choose a representer. +It is important to provide known content type in request. If content type is missing or not supported by the responder then +`#consume!` will raise an exception `Roar::Rails::ControllerAdditions::UnsupportedMediaType`. Unless you rescue the exception +the action will stop and respond with HTTP status `406 Unsupported Media Type`. + +Note that `#consume!` respects settings from `#represents`. It uses the same mechanics known from `#respond_with` to choose a representer. ```ruby consume!(singer, :represent_with => MusicianRepresenter) diff --git a/lib/roar/rails/controller_additions.rb b/lib/roar/rails/controller_additions.rb index 4dc146d..bf4e30d 100644 --- a/lib/roar/rails/controller_additions.rb +++ b/lib/roar/rails/controller_additions.rb @@ -23,11 +23,30 @@ def represents(format, options) end + class UnsupportedMediaType < StandardError #:nodoc: + end + + def consume!(model, options={}) - format = formats.first # FIXME: i expected request.content_mime_type to do the job. copied from responder.rb. this will return the wrong format when the controller responds to :json and :xml and the Content-type is :xml (?) + content_type = request.content_type + if content_type.nil? + raise UnsupportedMediaType.new("Cannot consume input without content type.") + end + + format = Mime::Type.lookup(content_type).try(:symbol) + + unless format + raise UnsupportedMediaType.new("Cannot consume unregistered media type '#{content_type}'") + end + + parsing_method = compute_parsing_method(format) representer = prepare_model_for(format, model, options) - representer.send(compute_parsing_method(format), incoming_string, options) # e.g. from_json("...") + if parsing_method && !representer.respond_to?(parsing_method) + raise UnsupportedMediaType.new("Cannot consume unsupported media type '#{content_type}'") + end + + representer.send(parsing_method, incoming_string, options) # e.g. from_json("...") model end diff --git a/lib/roar/rails/railtie.rb b/lib/roar/rails/railtie.rb index 43652a4..b3cf116 100644 --- a/lib/roar/rails/railtie.rb +++ b/lib/roar/rails/railtie.rb @@ -6,6 +6,11 @@ module Rails class Railtie < ::Rails::Railtie config.representer = ActiveSupport::OrderedOptions.new + rescue_responses = config.action_dispatch.rescue_responses || ActionDispatch::ShowExceptions.rescue_responses #newer or fallback to 3.0 + rescue_responses.merge!( + 'Roar::Rails::ControllerAdditions::UnsupportedMediaType' => :unsupported_media_type + ) + initializer "roar.set_configs" do |app| ::Roar::Representer.module_eval do include app.routes.url_helpers diff --git a/test/consume_test.rb b/test/consume_test.rb index 585718f..31345fb 100644 --- a/test/consume_test.rb +++ b/test/consume_test.rb @@ -16,11 +16,25 @@ class ConsumeTest < ActionController::TestCase tests UnnamespaceSingersController test "#consume parses incoming document and updates the model" do - post :consume_json, "{\"name\": \"Bumi\"}", :format => 'json' + @request.env['CONTENT_TYPE'] = 'application/json' + post :consume_json, "{\"name\": \"Bumi\"}" assert_equal %{#}, @response.body end end +class ConsumeHalWithNoHalRespondTest < ActionController::TestCase + include Roar::Rails::TestCase + + tests UnnamespaceSingersController + + test "#consume parses hal document and updates the model" do + @request.env['CONTENT_TYPE'] = 'application/json+hal' + assert_raises Roar::Rails::ControllerAdditions::UnsupportedMediaType do + post :consume_json, "{\"name\": \"Bumi\"}" + end + end +end + class ConsumeWithConfigurationTest < ActionController::TestCase include Roar::Rails::TestCase @@ -44,9 +58,52 @@ def consume_json tests SingersController test "#consume uses ConsumeWithConfigurationTest::MusicianRepresenter to parse incoming document" do + @request.env['CONTENT_TYPE'] = 'application/json' post :consume_json, %{{"called":"Bumi"}}, :format => :json assert_equal %{#}, @response.body end + + test "#do not consume missing content type" do + assert_raises Roar::Rails::ControllerAdditions::UnsupportedMediaType do + post :consume_json, "{\"name\": \"Bumi\"}" + end + end + + + test "#do not consume parses unknown content type" do + @request.env['CONTENT_TYPE'] = 'application/custom+json' + assert_raises Roar::Rails::ControllerAdditions::UnsupportedMediaType do + post :consume_json, "{\"name\": \"Bumi\"}" + end + end +end + +class ConsumeHalTest < ActionController::TestCase + include Roar::Rails::TestCase + + module MusicianRepresenter + include Roar::Representer::JSON::HAL + property :name + end + + + class SingersController < ActionController::Base + include Roar::Rails::ControllerAdditions + represents :hal, :entity => MusicianRepresenter + + def consume_hal + singer = consume!(Singer.new) + render :text => singer.inspect + end + end + + tests SingersController + + test "#consume parses HAL document and updates the model" do + @request.env['CONTENT_TYPE'] = 'application/json+hal' + post :consume_hal, "{\"name\": \"Bumi\"}" + assert_equal %{#}, @response.body + end end class ConsumeWithOptionsOverridingConfigurationTest < ActionController::TestCase @@ -66,6 +123,7 @@ def consume_json tests SingersController test "#consume uses #represents config to parse incoming document" do + @request.env['CONTENT_TYPE'] = 'application/json' post :consume_json, %{{"called":"Bumi"}}, :format => :json assert_equal %{#}, @response.body end @@ -73,6 +131,7 @@ def consume_json class RequestBodyStringTest < ConsumeTest test "#read rewinds before reading" do + @request.env['CONTENT_TYPE'] = 'application/json' @request.instance_eval do def body incoming = super diff --git a/test/responder_test.rb b/test/responder_test.rb index 079e6fa..c5a4505 100644 --- a/test/responder_test.rb +++ b/test/responder_test.rb @@ -282,6 +282,8 @@ class MusicianController < BaseController test "parsing uses decorating representer" do # FIXME: move to controller_test. created_singer = nil + @request.env['CONTENT_TYPE'] = 'application/json' + put singer.to_json do created_singer = consume!(Singer.new) respond_with created_singer @@ -325,6 +327,8 @@ class MusicianController < BaseController test "passes options in #consume!" do created_singer = nil + @request.env['CONTENT_TYPE'] = 'application/json' + put singer.to_json do created_singer = consume!(Singer.new, :title => "Mr.") respond_with created_singer