Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Reify the ETag concept further.

* Treat ETag strings as ETag instances, including weak tags.
* Put quoted-string logic in a reusable module.
* Move weak_etag*.rb -> etags*.rb
  • Loading branch information...
commit 5ad2b9d64c6bd4d025337b253d20baabe7b328ba 1 parent fed5aab
@seancribbs seancribbs authored
View
2  lib/webmachine.rb
@@ -3,7 +3,7 @@
require 'webmachine/headers'
require 'webmachine/request'
require 'webmachine/response'
-require 'webmachine/weak_etag'
+require 'webmachine/etags'
require 'webmachine/errors'
require 'webmachine/decision'
require 'webmachine/streaming'
View
15 lib/webmachine/decision/flow.rb
@@ -2,6 +2,7 @@
require 'digest/md5'
require 'webmachine/decision/conneg'
require 'webmachine/translation'
+require 'webmachine/etags'
module Webmachine
module Decision
@@ -233,18 +234,18 @@ def g8
# If-Match: * exists?
def g9
- request.if_match == "*" ? :h10 : :g11
+ quote(request.if_match) == '"*"' ? :h10 : :g11
end
# ETag in If-Match
def g11
- request_etags = request.if_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
- request_etags.include?(resource.generate_etag) ? :h10 : 412
+ request_etags = request.if_match.split(/\s*,\s*/).map {|etag| ETag.new(etag) }
+ request_etags.include?(ETag.new(resource.generate_etag)) ? :h10 : 412
end
# If-Match exists?
def h7
- (request.if_match && unquote_header(request.if_match) == '*') ? 412 : :i7
+ (request.if_match && unquote(request.if_match) == '*') ? 412 : :i7
end
# If-Unmodified-Since exists?
@@ -292,7 +293,7 @@ def i12
# If-none-match: * exists?
def i13
- request.if_none_match == "*" ? :j18 : :k13
+ quote(request.if_none_match) == '"*"' ? :j18 : :k13
end
# GET or HEAD?
@@ -320,8 +321,8 @@ def k7
# Etag in if-none-match?
def k13
- request_etags = request.if_none_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
- request_etags.include?(resource.generate_etag) ? :j18 : :l13
+ request_etags = request.if_none_match.split(/\s*,\s*/).map {|etag| ETag.new(etag) }
+ request_etags.include?(ETag.new(resource.generate_etag)) ? :j18 : :l13
end
# Moved temporarily?
View
25 lib/webmachine/decision/helpers.rb
@@ -1,12 +1,13 @@
require 'webmachine/streaming'
require 'webmachine/media_type'
+require 'webmachine/quoted_string'
+require 'webmachine/etags'
module Webmachine
module Decision
# Methods that assist the Decision {Flow}.
module Helpers
- # Pattern for quoted headers
- QUOTED = /^"(.*)"$/
+ include QuotedString
# Determines if the response has a body/entity set.
def has_response_body?
@@ -48,24 +49,6 @@ def encode_body
end
end
- # Ensures that a header is quoted (like ETag)
- def ensure_quoted_header(value)
- if value =~ QUOTED
- value
- else
- '"' << value << '"'
- end
- end
-
- # Unquotes request headers (like ETag)
- def unquote_header(value)
- if value =~ QUOTED
- $1
- else
- value
- end
- end
-
# Assists in receiving request bodies
def accept_helper
content_type = MediaType.parse(request.content_type || 'application/octet-stream')
@@ -90,7 +73,7 @@ def variances
# Adds caching-related headers to the response.
def add_caching_headers
if etag = resource.generate_etag
- response.headers['ETag'] = ensure_quoted_header(etag)
+ response.headers['ETag'] = ETag.new(etag).to_s
end
if expires = resource.expires
response.headers['Expires'] = expires.httpdate
View
69 lib/webmachine/etags.rb
@@ -0,0 +1,69 @@
+require 'webmachine/quoted_string'
+
+module Webmachine
+ # A wrapper around entity tags that encapsulates their semantics.
+ # This class by itself represents a "strong" entity tag.
+ class ETag
+ include QuotedString
+ # The pattern for a weak entity tag
+ WEAK_ETAG = /^W\/#{QUOTED_STRING}$/.freeze
+
+ def self.new(etag)
+ return etag if ETag === etag
+ klass = etag =~ WEAK_ETAG ? WeakETag : self
+ klass.send(:allocate).tap do |obj|
+ obj.send(:initialize, etag)
+ end
+ end
+
+ attr_reader :etag
+
+ def initialize(etag)
+ @etag = quote(etag)
+ end
+
+ # An entity tag is equivalent to another entity tag if their
+ # quoted values are equivalent. It is also equivalent to a String
+ # which represents the equivalent ETag.
+ def ==(other)
+ case other
+ when ETag
+ other.etag == @etag
+ when String
+ quote(other) == @etag
+ end
+ end
+
+ # Converts the entity tag into a string appropriate for use in a
+ # header.
+ def to_s
+ quote @etag
+ end
+ end
+
+ # A Weak Entity Tag, which can be used to compare entities which are
+ # semantically equivalent, but do not have the same byte-content. A
+ # WeakETag is equivalent to another entity tag if the non-weak
+ # portions are equivalent. It is also equivalent to a String which
+ # represents the equivalent strong or weak ETag.
+ class WeakETag < ETag
+ # Converts the WeakETag to a String for use in a header.
+ def to_s
+ "W/#{super}"
+ end
+
+ private
+ def unquote(str)
+ if str =~ WEAK_ETAG
+ unescape_quotes $1
+ else
+ super
+ end
+ end
+
+ def quote(str)
+ str = unescape_quotes($1) if str =~ WEAK_ETAG
+ super
+ end
+ end
+end
View
39 lib/webmachine/quoted_string.rb
@@ -0,0 +1,39 @@
+module Webmachine
+ # Helper methods for dealing with the 'quoted-string' type often
+ # present in header values.
+ module QuotedString
+ # The pattern for a 'quoted-string' type
+ QUOTED_STRING = /"((?:\\"|[^"])*)"/.freeze
+
+ # The pattern for a 'quoted-string' type, without any other content.
+ QS_ANCHORED = /^#{QUOTED_STRING}$/.freeze
+
+ # Removes surrounding quotes from a quoted-string
+ def unquote(str)
+ if str =~ QS_ANCHORED
+ unescape_quotes $1
+ else
+ str
+ end
+ end
+
+ # Ensures that quotes exist around a quoted-string
+ def quote(str)
+ if str =~ QS_ANCHORED
+ str
+ else
+ %Q{"#{escape_quotes str}"}
+ end
+ end
+
+ # Escapes quotes within a quoted string.
+ def escape_quotes(str)
+ str.gsub(/"/, '\\"')
+ end
+
+ # Unescapes quotes within a quoted string
+ def unescape_quotes(str)
+ str.gsub(%r{\\}, '')
+ end
+ end
+end
View
58 lib/webmachine/weak_etag.rb
@@ -1,58 +0,0 @@
-module Webmachine
- # A Weak Entity Tag, which can be used to compare entities which are
- # semantically equivalent, but do not have the same byte-content.
- class WeakETag
- # The pattern for a 'quoted-string' type
- QUOTED_STRING = /^"((?:\\"|[^"])*)"$/.freeze
-
- # The pattern for a weak entity tag
- WEAK_ETAG = /^W\/"((?:\\"|[^"])*)"$/.freeze
-
- attr_reader :etag
-
- def initialize(etag)
- @etag = quote(etag)
- end
-
- # A WeakETag is equivalent to an entity tag if the non-weak
- # portions are equivalent. It is also equivalent to a String which
- # represents the equivalent strong or weak ETag.
- def ==(other)
- case other
- when self.class
- other.etag == @etag
- when String
- quote(other) == @etag
- end
- end
-
- def to_s
- "W/#{quote @etag}"
- end
-
- private
- def unquote(str)
- if str =~ QUOTED_STRING
- $1
- elsif str =~ WEAK_ETAG
- $1
- else
- str
- end
- end
-
- def quote(str)
- if str =~ QUOTED_STRING
- str
- elsif str =~ WEAK_ETAG
- quote($1)
- else
- %Q{"#{escape_quotes str}"}
- end
- end
-
- def escape_quotes(str)
- str.gsub(/"/, '\\"')
- end
- end
-end
View
2  spec/webmachine/decision/helpers_spec.rb
@@ -46,7 +46,7 @@ def accept_doc; result; end
end
end
- context "setting the Content-Length header when responding" do
+ context "setting the Content-Length header when responding" do
[204, 205, 304].each do |code|
it "removes the header for entity-less response code #{code}" do
response.headers['Content-Length'] = '0'
View
41 spec/webmachine/weak_etag_spec.rb → spec/webmachine/etags_spec.rb
@@ -1,5 +1,36 @@
require 'spec_helper'
+describe Webmachine::ETag do
+ let(:etag_str){ '"deadbeef12345678"' }
+ let(:etag) { described_class.new etag_str }
+
+ subject { etag }
+
+ it { should == etag_str }
+ it { should be_kind_of(described_class) }
+ its(:to_s) { should == '"deadbeef12345678"' }
+ its(:etag) { should == '"deadbeef12345678"' }
+ it { should == described_class.new(etag_str.dup) }
+
+ context "when the original etag is unquoted" do
+ let(:etag_str) { 'deadbeef12345678' }
+
+ it { should == etag_str }
+ its(:to_s) { should == '"deadbeef12345678"' }
+ its(:etag) { should == '"deadbeef12345678"' }
+ it { should == described_class.new(etag_str.dup) }
+ end
+
+ context "when the original etag contains unbalanced quotes" do
+ let(:etag_str) { 'deadbeef"12345678' }
+
+ it { should == etag_str }
+ its(:to_s) { should == '"deadbeef\\"12345678"' }
+ its(:etag) { should == '"deadbeef\\"12345678"' }
+ it { should == described_class.new(etag_str.dup) }
+ end
+end
+
describe Webmachine::WeakETag do
let(:strong_etag){ '"deadbeef12345678"' }
let(:weak_etag) { described_class.new strong_etag }
@@ -7,6 +38,7 @@
subject { weak_etag }
it { should == strong_etag }
+ it { should be_kind_of(described_class) }
its(:to_s) { should == 'W/"deadbeef12345678"' }
its(:etag) { should == '"deadbeef12345678"' }
it { should == described_class.new(strong_etag.dup) }
@@ -15,6 +47,7 @@
let(:strong_etag) { 'deadbeef12345678' }
it { should == strong_etag }
+ it { should be_kind_of(described_class) }
its(:to_s) { should == 'W/"deadbeef12345678"' }
its(:etag) { should == '"deadbeef12345678"' }
it { should == described_class.new(strong_etag.dup) }
@@ -22,8 +55,9 @@
context "when the original etag contains unbalanced quotes" do
let(:strong_etag) { 'deadbeef"12345678' }
-
+
it { should == strong_etag }
+ it { should be_kind_of(described_class) }
its(:to_s) { should == 'W/"deadbeef\\"12345678"' }
its(:etag) { should == '"deadbeef\\"12345678"' }
it { should == described_class.new(strong_etag.dup) }
@@ -31,10 +65,11 @@
context "when the original etag is already a weak tag" do
let(:strong_etag) { 'W/"deadbeef12345678"' }
-
+
it { should == strong_etag }
+ it { should be_kind_of(described_class) }
its(:to_s) { should == 'W/"deadbeef12345678"' }
its(:etag) { should == '"deadbeef12345678"' }
- it { should == described_class.new(strong_etag.dup) }
+ it { should == described_class.new(strong_etag.dup) }
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.