Skip to content

Commit

Permalink
Add Media Range support and method caching
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Kern committed Jun 6, 2011
1 parent 33d5e9d commit f0f1159
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 26 deletions.
1 change: 1 addition & 0 deletions lib/webbed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Webbed
autoload :Headers, 'webbed/headers'
autoload :HTTPVersion, 'webbed/http_version'
autoload :GenericMessage, 'webbed/generic_message'
autoload :MediaRange, 'webbed/media_range'
autoload :MediaType, 'webbed/media_type'
autoload :Method, 'webbed/method'
autoload :StatusCode, 'webbed/status_code'
Expand Down
22 changes: 21 additions & 1 deletion lib/webbed/helpers/entity_headers_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def content_md5=(content_md5)

# The Content-Type of the Entity (as defined in the Content-Type Header).
#
# @return [MediaType, nil]
# @return [Webbed::MediaType, nil]
def content_type
headers['Content-Type'] ? Webbed::MediaType.new(headers['Content-Type']) : nil
end
Expand All @@ -57,6 +57,26 @@ def content_type
def content_type=(content_type)
headers['Content-Type'] = content_type.to_s
end

# The allowed Methods of the Entity (as defined in the Allow Header).
#
# @return [<Webbed::Method>, nil]
def allowed_methods
if headers['Allow']
headers['Allow'].split(/\s*,\s*/).map do |method|
Webbed::Method.new(method)
end
else
nil
end
end

# Sets the allowed Methods of the Entity (as defined in the Allow Header).
#
# @param [<#to_s>]
def allowed_methods=(allowed_methods)
headers['Allow'] = allowed_methods.join(', ')
end
end
end
end
20 changes: 20 additions & 0 deletions lib/webbed/helpers/request_headers_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ def referer
def referer=(referer)
headers['Referer'] = referer.to_s
end

# The accepted Media Ranges of the Request (as defined in the Allow Header).
#
# @return [<Webbed::MediaRange>, nil]
def accepted_media_ranges
if headers['Accept']
headers['Accept'].split(/\s*,\s*/).map do |media_type|
Webbed::MediaRange.new(media_type)
end
else
nil
end
end

# Sets the accepted Media Ranges of the Request (as defined in the Allow Header).
#
# @param [<#to_s>] accepted_media_ranges
def accepted_media_ranges=(accepted_media_ranges)
headers['Accept'] = accepted_media_ranges.join(', ')
end
end
end
end
51 changes: 51 additions & 0 deletions lib/webbed/media_range.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Webbed
# Representation of an HTTP Media Range.
class MediaRange < MediaType
GLOBAL_GROUP_REGEX = /^(\*)\/(\*)$/
TYPE_GROUP_REGEX = /^([-\w.+]+)\/(\*)$/

# Sets the MIME type of the Media Range.
#
# @param [String] mime_type
def mime_type=(mime_type)
GLOBAL_GROUP_REGEX =~ mime_type ||
TYPE_GROUP_REGEX =~ mime_type ||
MIME_TYPE_REGEX =~ mime_type

self.type = $1
self.subtype = $2
end

# Whether or not the Media Type is in the Media Range.
#
# @param [MediaType] media_type
def include?(media_type)
if ['*', media_type.type].include?(type) && ['*', media_type.subtype].include?(subtype)
accept_extensions.empty? || accept_extensions == media_type.parameters
else
false
end
end

# The accept-extensions of the Media Range (all the parameters except for "q").
#
# @return [{String => String}]
def accept_extensions
parameters.reject { |k, v| k == 'q' }
end

# The quality of the Media Range (as defined in the "q" parameter).
#
# @return [Float]
def quality
parameters['q'] ? parameters['q'].to_f : 1.0
end

# Sets the quality of the Media Range (as defined in the "q" parameter).
#
# @param [#to_s] quality
def quality=(quality)
parameters['q'] = quality.to_s
end
end
end
61 changes: 36 additions & 25 deletions lib/webbed/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,33 @@ class Method
# @return [Array<:request, :response>]
attr_reader :allowable_entities

# Checks for a cached Method or creates a new one.
#
# It caches the standard HTTP Methods that are in RFC 2616 as well as the
# new method `PATCH`. If the Method cannot be found, it calls `#initialize`.
#
# @example
# Webbed::Method.new('GET') # => Webbed::Method::GET
#
# @param (see #initialize)
# @return [Method] the new or cached Method
# @see #initialize
def self.new(value, options = {})
if const_defined?(value)
const_get(value)
else
super(value, options)
class << self
attr_writer :cached

# The cached Methods.
#
# @return [{String => Method}]
def cached
@cached ||= {}
end

# Checks for a cached Method or creates a new one.
#
# Whenever a new Method is created, it is automatically cached. If the
# Method cannot be found, this method calls `#initialize`.
#
# @example
# Webbed::Method.new('GET') # => Webbed::Method::GET
#
# @param (see #initialize)
# @return [Method] the new or cached Method
# @see #initialize
def new(value, options = {})
if options.delete(:cache)
cached[value] ||= super
else
cached[value] || super
end
end
end

Expand Down Expand Up @@ -81,14 +92,14 @@ def ==(other_method)
to_s == other_method.to_s
end

OPTIONS = new('OPTIONS', :safe => true, :idempotent => true, :allowable_entities => [:response])
GET = new('GET', :safe => true, :idempotent => true, :allowable_entities => [:response])
HEAD = new('HEAD', :safe => true, :idempotent => true, :allowable_entities => [])
POST = new('POST', :safe => false, :idempotent => false, :allowable_entities => [:request, :response])
PUT = new('PUT', :safe => false, :idempotent => true, :allowable_entities => [:request, :response])
DELETE = new('DELETE', :safe => false, :idempotent => true, :allowable_entities => [:response])
TRACE = new('TRACE', :safe => true, :idempotent => true, :allowable_entities => [:response])
CONNECT = new('CONNECT', :safe => false, :idempotent => false, :allowable_entities => [:request, :response])
PATCH = new('PATCH', :safe => false, :idempotent => false, :allowable_entities => [:request, :response])
OPTIONS = new('OPTIONS', :safe => true, :idempotent => true, :allowable_entities => [:response], :cache => true)
GET = new('GET', :safe => true, :idempotent => true, :allowable_entities => [:response], :cache => true)
HEAD = new('HEAD', :safe => true, :idempotent => true, :allowable_entities => [], :cache => true)
POST = new('POST', :safe => false, :idempotent => false, :allowable_entities => [:request, :response], :cache => true)
PUT = new('PUT', :safe => false, :idempotent => true, :allowable_entities => [:request, :response], :cache => true)
DELETE = new('DELETE', :safe => false, :idempotent => true, :allowable_entities => [:response], :cache => true)
TRACE = new('TRACE', :safe => true, :idempotent => true, :allowable_entities => [:response], :cache => true)
CONNECT = new('CONNECT', :safe => false, :idempotent => false, :allowable_entities => [:request, :response], :cache => true)
PATCH = new('PATCH', :safe => false, :idempotent => false, :allowable_entities => [:request, :response], :cache => true)
end
end
23 changes: 23 additions & 0 deletions test/webbed/helpers/entity_headers_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,29 @@ def setup
assert_equal 'application/json', message.headers['Content-Type']
end
end

test '#allowed_methods' do
[@request, @response].each do |message|
assert_nil message.allowed_methods

message.headers['Allow'] = 'GET, POST,PUT'
allowed_methods = message.allowed_methods
['GET', 'POST', 'PUT'].each.with_index do |method, index|
allowed_method = allowed_methods[index]
assert_instance_of Webbed::Method, allowed_method
assert_equal method, allowed_method.to_s
end

message.allowed_methods = ['DELETE', Webbed::Method::OPTIONS]
assert_equal 'DELETE, OPTIONS', message.headers['Allow']
allowed_methods = message.allowed_methods
['DELETE', 'OPTIONS'].each.with_index do |method, index|
allowed_method = allowed_methods[index]
assert_instance_of Webbed::Method, allowed_method
assert_equal method, allowed_method.to_s
end
end
end
end
end
end
28 changes: 28 additions & 0 deletions test/webbed/helpers/request_headers_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,34 @@ def setup
assert_equal Addressable::URI.parse('http://example.com'), @request.referer
assert_equal 'http://example.com', @request.headers['Referer']
end

test '#accepted_media_ranges' do
assert_nil @request.accepted_media_ranges

@request.headers['Accept'] = 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c, */*'
assert_equal 'text/plain', @request.accepted_media_ranges[0].mime_type
assert_equal 0.5, @request.accepted_media_ranges[0].quality
assert_equal 'text/html', @request.accepted_media_ranges[1].mime_type
assert_equal 1.0, @request.accepted_media_ranges[1].quality
assert_equal 'text/x-dvi', @request.accepted_media_ranges[2].mime_type
assert_equal 0.8, @request.accepted_media_ranges[2].quality
assert_equal 'text/x-c', @request.accepted_media_ranges[3].mime_type
assert_equal 1.0, @request.accepted_media_ranges[3].quality
assert_equal '*/*', @request.accepted_media_ranges[4].mime_type
assert_equal 1.0, @request.accepted_media_ranges[4].quality

@request.accepted_media_ranges = ['text/*', Webbed::MediaType.new('text/html'), 'text/html;level=1', '*/*']
assert_equal 'text/*, text/html, text/html;level=1, */*', @request.headers['Accept']
assert_equal 'text/*', @request.accepted_media_ranges[0].mime_type
assert_equal 1.0, @request.accepted_media_ranges[0].quality
assert_equal 'text/html', @request.accepted_media_ranges[1].mime_type
assert_equal 1.0, @request.accepted_media_ranges[1].quality
assert_equal 'text/html', @request.accepted_media_ranges[2].mime_type
assert_equal 1.0, @request.accepted_media_ranges[2].quality
assert_equal '1', @request.accepted_media_ranges[2].parameters['level']
assert_equal '*/*', @request.accepted_media_ranges[3].mime_type
assert_equal 1.0, @request.accepted_media_ranges[3].quality
end
end
end
end
60 changes: 60 additions & 0 deletions test/webbed/media_range_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'test_helper'

module WebbedTest
class MediaRangeTest < TestCase
test 'inheritance' do
media_range = Webbed::MediaRange.new('*/*')
assert_kind_of Webbed::MediaType, media_range
end

test '#initialize with a global group' do
media_range = Webbed::MediaRange.new('*/*')
assert_equal '*/*', media_range.mime_type
assert media_range.include?(Webbed::MediaType.new('foo/bar'))
end

test '#initialize with a type group' do
media_range = Webbed::MediaRange.new('text/*')
assert_equal 'text/*', media_range.mime_type
assert media_range.include?(Webbed::MediaType.new('text/html'))
refute media_range.include?(Webbed::MediaType.new('application/json'))
end

test '#initialize without a group' do
media_range = Webbed::MediaRange.new('text/html')
assert media_range.include?(Webbed::MediaType.new('text/html'))
assert media_range.include?(Webbed::MediaType.new('text/html;level=1'))
refute media_range.include?(Webbed::MediaType.new('text/xml'))
refute media_range.include?(Webbed::MediaType.new('application/json'))
end

test '#initialize with parameters' do
media_range = Webbed::MediaRange.new('text/html;q=1;level=1')
assert media_range.include?(Webbed::MediaType.new('text/html;level=1'))
refute media_range.include?(Webbed::MediaType.new('text/html;level=2'))
refute media_range.include?(Webbed::MediaType.new('text/html'))
refute media_range.include?(Webbed::MediaType.new('text/xml'))
refute media_range.include?(Webbed::MediaType.new('application/json'))
end

test '#quality' do
media_range = Webbed::MediaRange.new('text/html')
assert_equal 1.0, media_range.quality

media_range = Webbed::MediaRange.new('text/html; q=0.5')
assert_equal 0.5, media_range.quality

media_range.quality = 0.7
assert_equal '0.7', media_range.parameters['q']
assert_equal 0.7, media_range.quality
end

test '#accept_extensions' do
media_range = Webbed::MediaRange.new('text/html; q=0.5')
assert_equal({}, media_range.accept_extensions)

media_range = Webbed::MediaRange.new('text/html; q=0.5; level=1')
assert_equal({ 'level' => '1' }, media_range.accept_extensions)
end
end
end
12 changes: 12 additions & 0 deletions test/webbed/method_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class MethodTest < TestCase
refute_equal Webbed::Method::POST, fake
end

test 'caching' do
uncached = Webbed::Method.new('UNCACHED')
cached = Webbed::Method.new('CACHED', :cache => true)

refute_same Webbed::Method.new('UNCACHED'), uncached
assert_same Webbed::Method.new('CACHED'), cached
end

test 'OPTIONS' do
method = Webbed::Method::OPTIONS

Expand Down Expand Up @@ -156,5 +164,9 @@ class MethodTest < TestCase
assert_includes method.allowable_entities, :request
assert_includes method.allowable_entities, :response
end

def teardown
Webbed::Method.cached.delete('FAKE')
end
end
end

0 comments on commit f0f1159

Please sign in to comment.