Skip to content
This repository
Browse code

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 September 11, 2011
1  Gemfile
@@ -5,7 +5,6 @@ source :rubygems
5 5
 gemspec
6 6
 
7 7
 gem 'bundler'
8  
-gem 'mongrel', '~>1.2.beta'
9 8
 
10 9
 unless ENV['TRAVIS']
11 10
   gem 'guard-rspec'
3  lib/webmachine/decision/conneg.rb
@@ -80,7 +80,8 @@ def choose_language(provided, header)
80 80
         end
81 81
       end
82 82
 
83  
-      # RFC2616, section 14.14:
  83
+      # Implements language-negotation matching as described in
  84
+      # RFC2616, section 14.14.
84 85
       #
85 86
       # A language-range matches a language-tag if it exactly
86 87
       # equals the tag, or if it exactly equals a prefix of the
6  lib/webmachine/decision/helpers.rb
@@ -65,10 +65,8 @@ def unquote_header(value)
65 65
 
66 66
       # Assists in receiving request bodies
67 67
       def accept_helper
68  
-        content_type = request.content_type || 'application/octet-stream'
69  
-        mt = MediaType.parse(content_type)
70  
-        metadata['mediaparams'] = mt.params
71  
-        acceptable = resource.content_types_accepted.find {|ct, _| mt.type_matches?(MediaType.parse(ct)) }
  68
+        content_type = MediaType.parse(request.content_type || 'application/octet-stream')
  69
+        acceptable = resource.content_types_accepted.find {|ct, _| content_type.match?(ct) }
72 70
         if acceptable
73 71
           resource.send(acceptable.last)
74 72
         else
28  lib/webmachine/media_type.rb
@@ -57,15 +57,35 @@ def ==(other)
57 57
     end
58 58
 
59 59
     # Detects whether this {MediaType} matches the other {MediaType},
60  
-    # taking into account wildcards.
61  
-    # @param [MediaType, String, Array<String,Hash>] other the other
62  
-    #   type
  60
+    # taking into account wildcards. Sub-type parameters are treated
  61
+    # strictly.
  62
+    # @param [MediaType, String, Array<String,Hash>] other the other type 
63 63
     # @return [true,false] whether it is an acceptable match
64  
-    def match?(other)
  64
+    def exact_match?(other)
65 65
       other = self.class.parse(other)
66 66
       type_matches?(other) && other.params == params
67 67
     end
68 68
 
  69
+    # Detects whether the {MediaType} is an acceptable match for the
  70
+    # other {MediaType}, taking into account wildcards and satisfying
  71
+    # all requested parameters, but allowing this type to have extra
  72
+    # specificity.
  73
+    # @param [MediaType, String, Array<String,Hash>] other the other type 
  74
+    # @return [true,false] whether it is an acceptable match
  75
+    def match?(other)
  76
+      other = self.class.parse(other)
  77
+      type_matches?(other) && params_match?(other.params)
  78
+    end
  79
+
  80
+    # Detects whether the passed sub-type parameters are all satisfied
  81
+    # by this {MediaType}. The receiver is allowed to have other
  82
+    # params than the ones specified, but all specified must be equal.
  83
+    # @param [Hash] params the requested params
  84
+    # @return [true,false] whether it is an acceptable match
  85
+    def params_match?(other)
  86
+      other.all? {|k,v| params[k] == v }
  87
+    end
  88
+    
69 89
     # Reconstitutes the type into a String
70 90
     # @return [String] the type as a String
71 91
     def to_s
29  spec/webmachine/decision/conneg_spec.rb
@@ -8,36 +8,43 @@
8 8
       def to_html; "hello world!"; end
9 9
     end
10 10
   end
  11
+
11 12
   subject do
12 13
     Webmachine::Decision::FSM.new(resource, request, response)
13 14
   end
14 15
 
15 16
   context "choosing a media type" do
16 17
     it "should not choose a type when none are provided" do
17  
-      subject.choose_media_type([], "*/*").should be_nil      
  18
+      subject.choose_media_type([], "*/*").should be_nil
18 19
     end
19  
-    
  20
+
20 21
     it "should not choose a type when none are acceptable" do
21 22
       subject.choose_media_type(["text/html"], "application/json").should be_nil
22 23
     end
23  
-    
  24
+
24 25
     it "should choose the first acceptable type" do
25 26
       subject.choose_media_type(["text/html", "application/xml"],
26 27
                                 "application/xml, text/html, */*").should == "application/xml"
27 28
     end
28  
-    
  29
+
29 30
     it "should choose the type that matches closest when matching subparams" do
30 31
       subject.choose_media_type(["text/html",
31 32
                                  ["text/html", {"charset" => "iso8859-1"}]],
32 33
                                 "text/html;charset=iso8859-1, application/xml").
33 34
         should == "text/html;charset=iso8859-1"
34  
-        
35 35
     end
36 36
     
  37
+    it "should choose a type more specific than requested when an exact match is not present" do
  38
+      subject.choose_media_type(["application/json;v=3;foo=bar", "application/json;v=2"],
  39
+                                "text/html, application/json").
  40
+        should == "application/json;v=3;foo=bar"
  41
+    end
  42
+
  43
+
37 44
     it "should choose the preferred type over less-preferred types" do
38 45
       subject.choose_media_type(["text/html", "application/xml"],
39 46
                                 "application/xml;q=0.7, text/html, */*").should == "text/html"
40  
-      
  47
+
41 48
     end
42 49
 
43 50
     it "should raise an exception when a media-type is improperly formatted" do
@@ -112,17 +119,17 @@ def to_html; "hello world!"; end
112 119
       subject.choose_language([], "en")
113 120
       subject.metadata['Language'].should be_nil
114 121
     end
115  
-    
  122
+
116 123
     it "should choose the first acceptable language" do
117 124
       subject.choose_language(['en', 'en-US', 'es'], "en-US, es")
118 125
       subject.metadata['Language'].should == "en-US"
119 126
       response.headers['Content-Language'].should == "en-US"
120 127
     end
121  
-    
  128
+
122 129
     it "should choose the preferred language over less-preferred languages" do
123 130
       subject.choose_language(['en', 'en-US', 'es'], "en-US;q=0.6, es")
124 131
       subject.metadata['Language'].should == "es"
125  
-      response.headers['Content-Language'].should == "es"      
  132
+      response.headers['Content-Language'].should == "es"
126 133
     end
127 134
 
128 135
     it "should select the first language if all are acceptable" do
@@ -130,13 +137,13 @@ def to_html; "hello world!"; end
130 137
       subject.metadata['Language'].should == "en"
131 138
       response.headers['Content-Language'].should == "en"
132 139
     end
133  
-    
  140
+
134 141
     it "should select the closest acceptable language when an exact match is not available" do
135 142
       subject.choose_language(['en-US', 'es'], "en, fr")
136 143
       subject.metadata['Language'].should == 'en-US'
137 144
       response.headers['Content-Language'].should == 'en-US'
138 145
     end
139  
-    
  146
+
140 147
     it "should not set the language if none are acceptable" do
141 148
       subject.choose_language(['en'], 'es')
142 149
       subject.metadata['Language'].should be_nil
32  spec/webmachine/decision/helpers_spec.rb
@@ -19,6 +19,38 @@ def to_html; "test resource"; end
19 19
 
20 20
   let(:resource) { resource_with }
21 21
 
  22
+  describe "accepting request bodies" do
  23
+    let(:resource) do
  24
+      resource_with do
  25
+        def initialize
  26
+          @accepted, @result = [], true
  27
+        end
  28
+        attr_accessor :accepted, :result
  29
+        def content_types_accepted
  30
+          (accepted || []).map {|t| Array === t ? t : [t, :accept_doc] }
  31
+        end
  32
+        def accept_doc; result; end
  33
+      end
  34
+    end
  35
+
  36
+    it "should return 415 when no types are accepted" do
  37
+      subject.accept_helper.should == 415
  38
+    end
  39
+
  40
+    it "should return 415 when the posted type is not acceptable" do
  41
+      resource.accepted = %W{application/json}
  42
+      headers['Content-Type'] = "text/xml"
  43
+      subject.accept_helper.should == 415
  44
+    end
  45
+
  46
+    it "should call the method for the first acceptable type, taking into account params" do
  47
+      resource.accepted = ["application/json;v=3", ["application/json", :other]]
  48
+      resource.should_receive(:other).and_return(true)
  49
+      headers['Content-Type'] = 'application/json;v=2'
  50
+      subject.accept_helper.should be_true
  51
+    end
  52
+  end
  53
+
22 54
   describe "#encode_body" do
23 55
     before { subject.run }
24 56
 
29  spec/webmachine/media_type_spec.rb
@@ -55,19 +55,24 @@
55 55
   end
56 56
 
57 57
   describe "matching a requested type" do
58  
-    it { should be_match("application/xml;charset=UTF-8") }
59  
-    it { should be_match("application/*;charset=UTF-8") }
60  
-    it { should be_match("*/*;charset=UTF-8") }
61  
-    it { should be_match("*;charset=UTF-8") }
62  
-    it { should_not be_match("text/xml") }
63  
-    it { should_not be_match("application/xml") }
64  
-    it { should_not be_match("application/xml;version=1") }
65  
-
66  
-    it { should be_type_matches("application/xml") }
67  
-    it { should be_type_matches("application/*") }
68  
-    it { should be_type_matches("*/*") }
69  
-    it { should be_type_matches("*") }
  58
+    it { should     be_exact_match("application/xml;charset=UTF-8") }
  59
+    it { should     be_exact_match("application/*;charset=UTF-8") }
  60
+    it { should     be_exact_match("*/*;charset=UTF-8") }
  61
+    it { should     be_exact_match("*;charset=UTF-8") }
  62
+    it { should_not be_exact_match("text/xml") }
  63
+    it { should_not be_exact_match("application/xml") }
  64
+    it { should_not be_exact_match("application/xml;version=1") }
  65
+    
  66
+    it { should     be_type_matches("application/xml") }
  67
+    it { should     be_type_matches("application/*") }
  68
+    it { should     be_type_matches("*/*") }
  69
+    it { should     be_type_matches("*") }
70 70
     it { should_not be_type_matches("text/xml") }
71 71
     it { should_not be_type_matches("text/*") }
  72
+
  73
+    it { should     be_params_match({}) }
  74
+    it { should     be_params_match({"charset" => "UTF-8"}) }
  75
+    it { should_not be_params_match({"charset" => "Windows-1252"}) }
  76
+    it { should_not be_params_match({"version" => "3"}) }
72 77
   end
73 78
 end
1  webmachine.gemspec
@@ -23,6 +23,7 @@ Gem::Specification.new do |gem|
23 23
       gem.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
24 24
       gem.add_development_dependency(%q<yard>, ["~> 0.6.7"])
25 25
       gem.add_development_dependency(%q<rake>)
  26
+      gem.add_development_dependency(%q<mongrel>, ['~>1.2.beta'])
26 27
     else
27 28
       gem.add_dependency(%q<i18n>, [">= 0.4.0"])
28 29
       gem.add_dependency(%q<rspec>, ["~> 2.6.0"])

0 notes on commit 3686d0d

Please sign in to comment.
Something went wrong with that request. Please try again.