Skip to content

Commit

Permalink
Add Accept-Language negotiation.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Kern committed Aug 27, 2011
1 parent ddbc007 commit a2f69e4
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 50 deletions.
59 changes: 56 additions & 3 deletions lib/webbed/helpers/request_headers_helper.rb
Expand Up @@ -58,7 +58,7 @@ def referer=(referer)
headers['Referer'] = referer.to_s
end

# The accepted Media Ranges of the Request (as defined in the Allow Header).
# The accepted Media Ranges of the Request (as defined in the Accept Header).
#
# @return [<Webbed::MediaRange>, nil]
def accepted_media_ranges
Expand All @@ -71,7 +71,7 @@ def accepted_media_ranges
end
end

# Sets the accepted Media Ranges of the Request (as defined in the Allow Header).
# Sets the accepted Media Ranges of the Request (as defined in the Accept Header).
#
# @param [<#to_s>] accepted_media_ranges
def accepted_media_ranges=(accepted_media_ranges)
Expand All @@ -87,7 +87,7 @@ def accepted_media_ranges=(accepted_media_ranges)
def preferred_media_ranges
accepted_media_ranges = self.accepted_media_ranges

# Fix the broken `text/html` Media Range.
# Fix the broken `text/xml` Media Range.
text_xml = accepted_media_ranges.find { |media_type| media_type.mime_type == 'text/xml' }
application_xml = accepted_media_ranges.find { |media_type| media_type.mime_type == 'application/xml' }
if text_xml && application_xml
Expand Down Expand Up @@ -126,6 +126,59 @@ def negotiate_media_type(media_types)
matches.sort! { |a, b| b[1] <=> a[1] }
matches[0] ? matches[0][0] : nil
end

# The accepted language ranges of the Request (as defined in the Accept-Language Header).
#
# @return [<Webbed::LanguageRange>, nil]
def accepted_language_ranges
if headers['Accept-Language']
headers['Accept-Language'].split(/\s*,\s*/).each_with_index.map do |language_tag, index|
Webbed::LanguageRange.new(language_tag, :order => index)
end
else
[Webbed::LanguageRange.new('*')]
end
end

# Sets the accepted language ranges of the Request (as defined in the Accept-Language Header).
#
# @param [<#to_s>] accepted_language_ranges
def accepted_language_ranges=(accepted_language_ranges)
headers['Accept-Language'] = accepted_language_ranges.join(', ')
end

# Sorts the accepted language ranges of the Request in order of preference.
#
# @return [<Webbed::LanguageRange>]
def preferred_language_ranges
accepted_language_ranges.sort.reverse
end

# Negotiates which language tag to use based on the accepted language ranges.
#
# @param [<Webbed::LanguageTag>] language_tags
# @return [Webbed::LanguageTag, nil]
def negotiate_language_tag(language_tags)
preferred_language_ranges = self.preferred_language_ranges
matches = []

language_tags.each do |language_tag|
language_tag_match = nil

preferred_language_ranges.each do |language_range|
if language_range.include?(language_tag) && (!language_tag_match || language_tag_match.specificity < language_range.specificity)
language_tag_match = language_range
end
end

if language_tag_match && language_tag_match.quality != 0
matches.push([language_tag, language_tag_match])
end
end

matches.sort! { |a, b| b[1] <=> a[1] }
matches[0] ? matches[0][0] : nil
end
end
end
end
14 changes: 12 additions & 2 deletions lib/webbed/language_range.rb
Expand Up @@ -6,16 +6,26 @@ class LanguageRange < LanguageTag
# @return [Fixnum]
attr_reader :quality

# The order of the language range.
#
# @return [Fixnum]
attr_accessor :order

Q_REGEX = /(?:\s*;\s*q\s*=\s*(.*))?/
STAR_REGEX = /(\*)#{Q_REGEX}/
TAG_REGEX = /[a-zA-Z]{0,8}/
LANGUAGE_TAG_REGEX = /(#{TAG_REGEX}(?:-#{TAG_REGEX})*)#{Q_REGEX}/

# (see Webbed::LanguageTag#initialize)
def initialize(string)
# Creates a new language range.
#
# @param [String] tag
# @param [Hash] options the options to create the language range with
# @option options [Fixnum] :order (0) the order of the language range
def initialize(string, options = {})
STAR_REGEX =~ string || LANGUAGE_TAG_REGEX =~ string
super($1)
@quality = $2
@order = options.fetch(:order, 0)
end

# Whether or not the language range is a catch-all.
Expand Down
5 changes: 4 additions & 1 deletion lib/webbed/media_range.rb
Expand Up @@ -6,6 +6,9 @@ class MediaRange < MediaType
GLOBAL_GROUP_REGEX = /^(\*)\/(\*)$/
TYPE_GROUP_REGEX = /^([-\w.+]+)\/(\*)$/

# The order of the Media Range.
#
# @return [Fixnum]
attr_accessor :order

# Creates a new Media Range.
Expand All @@ -15,7 +18,7 @@ class MediaRange < MediaType
# @option options [Fixnum] :order (0) the order of the Media Range
# @see Webbed::MediaType#initialize
def initialize(media_range, options = {})
self.order = options.delete(:order) || 0
self.order = options.fetch(:order, 0)
super(media_range)
end

Expand Down
107 changes: 73 additions & 34 deletions test/webbed/helpers/request_headers_helper_test.rb
Expand Up @@ -140,42 +140,81 @@ def setup
end

test '#accepted_language_ranges' do
assert_equal '*/*', @request.accepted_media_ranges[0].mime_type
assert_equal 1, @request.accepted_media_ranges[0].quality
assert_equal 0, @request.accepted_media_ranges[0].order
assert_equal '*', @request.accepted_language_ranges[0].range
assert_equal 1, @request.accepted_language_ranges[0].quality
assert_equal 0, @request.accepted_language_ranges[0].order

@request.headers['Accept-Language'] = 'en-gb; q=0.5, en-us, x-pig-latin; q=0.8, i-cherokee, *'
assert_equal 'en-gb', @request.accepted_language_ranges[0].range
assert_equal 0.5, @request.accepted_language_ranges[0].quality
assert_equal 0, @request.accepted_language_ranges[0].order
assert_equal 'en-us', @request.accepted_language_ranges[1].range
assert_equal 1.0, @request.accepted_language_ranges[1].quality
assert_equal 1, @request.accepted_language_ranges[1].order
assert_equal 'x-pig-latin', @request.accepted_language_ranges[2].range
assert_equal 0.8, @request.accepted_language_ranges[2].quality
assert_equal 2, @request.accepted_language_ranges[2].order
assert_equal 'i-cherokee', @request.accepted_language_ranges[3].range
assert_equal 1.0, @request.accepted_language_ranges[3].quality
assert_equal 3, @request.accepted_language_ranges[3].order
assert_equal '*', @request.accepted_language_ranges[4].range
assert_equal 1.0, @request.accepted_language_ranges[4].quality
assert_equal 4, @request.accepted_language_ranges[4].order

@request.accepted_language_ranges = ['en', Webbed::LanguageRange.new('en-gb'), 'en-gb-foo', '*']
assert_equal 'en, en-gb, en-gb-foo, *', @request.headers['Accept-Language']
assert_equal 'en', @request.accepted_language_ranges[0].range
assert_equal 1.0, @request.accepted_language_ranges[0].quality
assert_equal 0, @request.accepted_language_ranges[0].order
assert_equal 'en-gb', @request.accepted_language_ranges[1].range
assert_equal 1.0, @request.accepted_language_ranges[1].quality
assert_equal 1, @request.accepted_language_ranges[1].order
assert_equal 'en-gb-foo', @request.accepted_language_ranges[2].range
assert_equal 1.0, @request.accepted_language_ranges[2].quality
assert_equal 2, @request.accepted_language_ranges[2].order
assert_equal '*', @request.accepted_language_ranges[3].range
assert_equal 1.0, @request.accepted_language_ranges[3].quality
assert_equal 3, @request.accepted_language_ranges[3].order
end

test '#preferred_language_ranges' do
assert_equal '*', @request.preferred_language_ranges[0].to_s

@request.headers['Accept-Language'] = 'en;q=0.3, en-gb;q=0.7, en-gb-foo, en-gb-bar, en-gb-bar;q=0.4, *;q=0.5'
assert_equal 'en-gb-foo', @request.preferred_language_ranges[0].to_s
assert_equal 'en-gb-bar', @request.preferred_language_ranges[1].to_s
assert_equal 'en-gb; q=0.7', @request.preferred_language_ranges[2].to_s
assert_equal '*; q=0.5', @request.preferred_language_ranges[3].to_s
assert_equal 'en-gb-bar; q=0.4', @request.preferred_language_ranges[4].to_s
assert_equal 'en; q=0.3', @request.preferred_language_ranges[5].to_s
end

test '#negotiate_language_tag' do
en_gb = Webbed::LanguageTag.new('en-gb')
i_cherokee = Webbed::LanguageTag.new('i-cherokee')

@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 0, @request.accepted_media_ranges[0].order
assert_equal 'text/html', @request.accepted_media_ranges[1].mime_type
assert_equal 1.0, @request.accepted_media_ranges[1].quality
assert_equal 1, @request.accepted_media_ranges[1].order
assert_equal 'text/x-dvi', @request.accepted_media_ranges[2].mime_type
assert_equal 0.8, @request.accepted_media_ranges[2].quality
assert_equal 2, @request.accepted_media_ranges[2].order
assert_equal 'text/x-c', @request.accepted_media_ranges[3].mime_type
assert_equal 1.0, @request.accepted_media_ranges[3].quality
assert_equal 3, @request.accepted_media_ranges[3].order
assert_equal '*/*', @request.accepted_media_ranges[4].mime_type
assert_equal 1.0, @request.accepted_media_ranges[4].quality
assert_equal 4, @request.accepted_media_ranges[4].order
assert_equal en_gb, @request.negotiate_language_tag([en_gb])
assert_equal en_gb, @request.negotiate_language_tag([en_gb, i_cherokee])

@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 0, @request.accepted_media_ranges[0].order
assert_equal 'text/html', @request.accepted_media_ranges[1].mime_type
assert_equal 1.0, @request.accepted_media_ranges[1].quality
assert_equal 1, @request.accepted_media_ranges[1].order
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 2, @request.accepted_media_ranges[2].order
assert_equal '*/*', @request.accepted_media_ranges[3].mime_type
assert_equal 1.0, @request.accepted_media_ranges[3].quality
assert_equal 3, @request.accepted_media_ranges[3].order
@request.headers['Accept-Language'] = 'i'
assert_nil @request.negotiate_language_tag([en_gb])
assert_equal i_cherokee, @request.negotiate_language_tag([en_gb, i_cherokee])

@request.headers['Accept-Language'] = 'i, en'
assert_equal en_gb, @request.negotiate_language_tag([en_gb])
assert_equal i_cherokee, @request.negotiate_language_tag([en_gb, i_cherokee])

@request.headers['Accept-Language'] = 'en, i'
assert_equal en_gb, @request.negotiate_language_tag([en_gb])
assert_equal en_gb, @request.negotiate_language_tag([en_gb, i_cherokee])

@request.headers['Accept-Language'] = 'en;q=0, i'
assert_nil @request.negotiate_language_tag([en_gb])
assert_equal i_cherokee, @request.negotiate_language_tag([en_gb, i_cherokee])

@request.headers['Accept-Language'] = 'en, en-gb;q=0.2, i;q=0.5'
assert_equal en_gb, @request.negotiate_language_tag([en_gb])
assert_equal i_cherokee, @request.negotiate_language_tag([en_gb, i_cherokee])
end
end
end
Expand Down
18 changes: 18 additions & 0 deletions test/webbed/language_range_test.rb
Expand Up @@ -101,5 +101,23 @@ class LanguageRangeTest < TestCase
other_with_q = Webbed::LanguageRange.new('en-gb; q=0.99')
assert_equal 0.99, other_with_q.quality
end

test '#<=>' do
language_range_high_quality = Webbed::LanguageRange.new('en-gb')
language_range_low_quality = Webbed::LanguageRange.new('x-pig-latin;q=0.2')
language_range_ordered_0 = Webbed::LanguageRange.new('en-gb;q=0.5', :order => 0)
language_range_ordered_1 = Webbed::LanguageRange.new('en-gb;q=0.5', :order => 1)
language_range_ordered_2 = Webbed::LanguageRange.new('en-gb;q=0.5', :order => 2)

language_ranges = [
language_range_low_quality,
language_range_ordered_2,
language_range_ordered_1,
language_range_ordered_0,
language_range_high_quality
]

assert_equal(language_ranges, language_ranges.shuffle.sort)
end
end
end
14 changes: 4 additions & 10 deletions test/webbed/media_range_test.rb
Expand Up @@ -64,21 +64,15 @@ class MediaRangeTest < TestCase
media_range_ordered_1 = Webbed::MediaRange.new('text/html;q=0.5', :order => 1)
media_range_ordered_2 = Webbed::MediaRange.new('text/html;q=0.5', :order => 2)

shuffled_media_ranges = [
media_range_high_quality,
media_range_low_quality,
media_range_ordered_0,
media_range_ordered_1,
media_range_ordered_2
].shuffle

assert_equal([
media_ranges = [
media_range_low_quality,
media_range_ordered_2,
media_range_ordered_1,
media_range_ordered_0,
media_range_high_quality
], shuffled_media_ranges.sort)
]

assert_equal(media_ranges, media_ranges.shuffle.sort)
end

test '#specificity' do
Expand Down

0 comments on commit a2f69e4

Please sign in to comment.