Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Match media types less strictly. Closes #1.

Media types are now matched at the level specified by the client. That
is, a response type may be negotiated that is MORE
specific (containing more type parameters) than requested by the
client in the Accept header.  This also applies to
content_types_accepted, meaning that an incoming entity may be
accepted by a less-specific processing method.

This will have less-surprising behavior when a client requests an
unparameterized type but the resource only provides parameterized
ones, as well as supporting patterns like versioned APIs via the type
parameters.
  • Loading branch information...
commit 3686d0d9ff77fc98aff59f89478e9c6c18844ca1 1 parent 0eefedc
Sean Cribbs authored
1  Gemfile
View
@@ -5,7 +5,6 @@ source :rubygems
gemspec
gem 'bundler'
-gem 'mongrel', '~>1.2.beta'
unless ENV['TRAVIS']
gem 'guard-rspec'
3  lib/webmachine/decision/conneg.rb
View
@@ -80,7 +80,8 @@ def choose_language(provided, header)
end
end
- # RFC2616, section 14.14:
+ # Implements language-negotation matching as described in
+ # RFC2616, section 14.14.
#
# A language-range matches a language-tag if it exactly
# equals the tag, or if it exactly equals a prefix of the
6 lib/webmachine/decision/helpers.rb
View
@@ -65,10 +65,8 @@ def unquote_header(value)
# Assists in receiving request bodies
def accept_helper
- content_type = request.content_type || 'application/octet-stream'
- mt = MediaType.parse(content_type)
- metadata['mediaparams'] = mt.params
- acceptable = resource.content_types_accepted.find {|ct, _| mt.type_matches?(MediaType.parse(ct)) }
+ content_type = MediaType.parse(request.content_type || 'application/octet-stream')
+ acceptable = resource.content_types_accepted.find {|ct, _| content_type.match?(ct) }
if acceptable
resource.send(acceptable.last)
else
28 lib/webmachine/media_type.rb
View
@@ -57,15 +57,35 @@ def ==(other)
end
# Detects whether this {MediaType} matches the other {MediaType},
- # taking into account wildcards.
- # @param [MediaType, String, Array<String,Hash>] other the other
- # type
+ # taking into account wildcards. Sub-type parameters are treated
+ # strictly.
+ # @param [MediaType, String, Array<String,Hash>] other the other type
# @return [true,false] whether it is an acceptable match
- def match?(other)
+ def exact_match?(other)
other = self.class.parse(other)
type_matches?(other) && other.params == params
end
+ # Detects whether the {MediaType} is an acceptable match for the
+ # other {MediaType}, taking into account wildcards and satisfying
+ # all requested parameters, but allowing this type to have extra
+ # specificity.
+ # @param [MediaType, String, Array<String,Hash>] other the other type
+ # @return [true,false] whether it is an acceptable match
+ def match?(other)
+ other = self.class.parse(other)
+ type_matches?(other) && params_match?(other.params)
+ end
+
+ # Detects whether the passed sub-type parameters are all satisfied
+ # by this {MediaType}. The receiver is allowed to have other
+ # params than the ones specified, but all specified must be equal.
+ # @param [Hash] params the requested params
+ # @return [true,false] whether it is an acceptable match
+ def params_match?(other)
+ other.all? {|k,v| params[k] == v }
+ end
+
# Reconstitutes the type into a String
# @return [String] the type as a String
def to_s
29 spec/webmachine/decision/conneg_spec.rb
View
@@ -8,36 +8,43 @@
def to_html; "hello world!"; end
end
end
+
subject do
Webmachine::Decision::FSM.new(resource, request, response)
end
context "choosing a media type" do
it "should not choose a type when none are provided" do
- subject.choose_media_type([], "*/*").should be_nil
+ subject.choose_media_type([], "*/*").should be_nil
end
-
+
it "should not choose a type when none are acceptable" do
subject.choose_media_type(["text/html"], "application/json").should be_nil
end
-
+
it "should choose the first acceptable type" do
subject.choose_media_type(["text/html", "application/xml"],
"application/xml, text/html, */*").should == "application/xml"
end
-
+
it "should choose the type that matches closest when matching subparams" do
subject.choose_media_type(["text/html",
["text/html", {"charset" => "iso8859-1"}]],
"text/html;charset=iso8859-1, application/xml").
should == "text/html;charset=iso8859-1"
-
end
+ it "should choose a type more specific than requested when an exact match is not present" do
+ subject.choose_media_type(["application/json;v=3;foo=bar", "application/json;v=2"],
+ "text/html, application/json").
+ should == "application/json;v=3;foo=bar"
+ end
+
+
it "should choose the preferred type over less-preferred types" do
subject.choose_media_type(["text/html", "application/xml"],
"application/xml;q=0.7, text/html, */*").should == "text/html"
-
+
end
it "should raise an exception when a media-type is improperly formatted" do
@@ -112,17 +119,17 @@ def to_html; "hello world!"; end
subject.choose_language([], "en")
subject.metadata['Language'].should be_nil
end
-
+
it "should choose the first acceptable language" do
subject.choose_language(['en', 'en-US', 'es'], "en-US, es")
subject.metadata['Language'].should == "en-US"
response.headers['Content-Language'].should == "en-US"
end
-
+
it "should choose the preferred language over less-preferred languages" do
subject.choose_language(['en', 'en-US', 'es'], "en-US;q=0.6, es")
subject.metadata['Language'].should == "es"
- response.headers['Content-Language'].should == "es"
+ response.headers['Content-Language'].should == "es"
end
it "should select the first language if all are acceptable" do
@@ -130,13 +137,13 @@ def to_html; "hello world!"; end
subject.metadata['Language'].should == "en"
response.headers['Content-Language'].should == "en"
end
-
+
it "should select the closest acceptable language when an exact match is not available" do
subject.choose_language(['en-US', 'es'], "en, fr")
subject.metadata['Language'].should == 'en-US'
response.headers['Content-Language'].should == 'en-US'
end
-
+
it "should not set the language if none are acceptable" do
subject.choose_language(['en'], 'es')
subject.metadata['Language'].should be_nil
32 spec/webmachine/decision/helpers_spec.rb
View
@@ -19,6 +19,38 @@ def to_html; "test resource"; end
let(:resource) { resource_with }
+ describe "accepting request bodies" do
+ let(:resource) do
+ resource_with do
+ def initialize
+ @accepted, @result = [], true
+ end
+ attr_accessor :accepted, :result
+ def content_types_accepted
+ (accepted || []).map {|t| Array === t ? t : [t, :accept_doc] }
+ end
+ def accept_doc; result; end
+ end
+ end
+
+ it "should return 415 when no types are accepted" do
+ subject.accept_helper.should == 415
+ end
+
+ it "should return 415 when the posted type is not acceptable" do
+ resource.accepted = %W{application/json}
+ headers['Content-Type'] = "text/xml"
+ subject.accept_helper.should == 415
+ end
+
+ it "should call the method for the first acceptable type, taking into account params" do
+ resource.accepted = ["application/json;v=3", ["application/json", :other]]
+ resource.should_receive(:other).and_return(true)
+ headers['Content-Type'] = 'application/json;v=2'
+ subject.accept_helper.should be_true
+ end
+ end
+
describe "#encode_body" do
before { subject.run }
29 spec/webmachine/media_type_spec.rb
View
@@ -55,19 +55,24 @@
end
describe "matching a requested type" do
- it { should be_match("application/xml;charset=UTF-8") }
- it { should be_match("application/*;charset=UTF-8") }
- it { should be_match("*/*;charset=UTF-8") }
- it { should be_match("*;charset=UTF-8") }
- it { should_not be_match("text/xml") }
- it { should_not be_match("application/xml") }
- it { should_not be_match("application/xml;version=1") }
-
- it { should be_type_matches("application/xml") }
- it { should be_type_matches("application/*") }
- it { should be_type_matches("*/*") }
- it { should be_type_matches("*") }
+ it { should be_exact_match("application/xml;charset=UTF-8") }
+ it { should be_exact_match("application/*;charset=UTF-8") }
+ it { should be_exact_match("*/*;charset=UTF-8") }
+ it { should be_exact_match("*;charset=UTF-8") }
+ it { should_not be_exact_match("text/xml") }
+ it { should_not be_exact_match("application/xml") }
+ it { should_not be_exact_match("application/xml;version=1") }
+
+ it { should be_type_matches("application/xml") }
+ it { should be_type_matches("application/*") }
+ it { should be_type_matches("*/*") }
+ it { should be_type_matches("*") }
it { should_not be_type_matches("text/xml") }
it { should_not be_type_matches("text/*") }
+
+ it { should be_params_match({}) }
+ it { should be_params_match({"charset" => "UTF-8"}) }
+ it { should_not be_params_match({"charset" => "Windows-1252"}) }
+ it { should_not be_params_match({"version" => "3"}) }
end
end
1  webmachine.gemspec
View
@@ -23,6 +23,7 @@ Gem::Specification.new do |gem|
gem.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
gem.add_development_dependency(%q<yard>, ["~> 0.6.7"])
gem.add_development_dependency(%q<rake>)
+ gem.add_development_dependency(%q<mongrel>, ['~>1.2.beta'])
else
gem.add_dependency(%q<i18n>, [">= 0.4.0"])
gem.add_dependency(%q<rspec>, ["~> 2.6.0"])
Please sign in to comment.
Something went wrong with that request. Please try again.