webmachine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +28 -0
- data/Gemfile +16 -0
- data/Guardfile +11 -0
- data/README.md +89 -0
- data/Rakefile +31 -0
- data/examples/webrick.rb +19 -0
- data/lib/webmachine/adapters/webrick.rb +74 -0
- data/lib/webmachine/adapters.rb +15 -0
- data/lib/webmachine/decision/conneg.rb +304 -0
- data/lib/webmachine/decision/flow.rb +502 -0
- data/lib/webmachine/decision/fsm.rb +79 -0
- data/lib/webmachine/decision/helpers.rb +80 -0
- data/lib/webmachine/decision.rb +12 -0
- data/lib/webmachine/dispatcher/route.rb +85 -0
- data/lib/webmachine/dispatcher.rb +40 -0
- data/lib/webmachine/errors.rb +37 -0
- data/lib/webmachine/headers.rb +16 -0
- data/lib/webmachine/locale/en.yml +28 -0
- data/lib/webmachine/request.rb +56 -0
- data/lib/webmachine/resource/callbacks.rb +362 -0
- data/lib/webmachine/resource/encodings.rb +36 -0
- data/lib/webmachine/resource.rb +48 -0
- data/lib/webmachine/response.rb +49 -0
- data/lib/webmachine/streaming.rb +27 -0
- data/lib/webmachine/translation.rb +11 -0
- data/lib/webmachine/version.rb +4 -0
- data/lib/webmachine.rb +19 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/tests.org +57 -0
- data/spec/webmachine/decision/conneg_spec.rb +152 -0
- data/spec/webmachine/decision/flow_spec.rb +1030 -0
- data/spec/webmachine/dispatcher/route_spec.rb +109 -0
- data/spec/webmachine/dispatcher_spec.rb +34 -0
- data/spec/webmachine/headers_spec.rb +19 -0
- data/spec/webmachine/request_spec.rb +24 -0
- data/webmachine.gemspec +44 -0
- metadata +137 -0
data/.gitignore
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
pkg
|
20
|
+
|
21
|
+
## PROJECT::SPECIFIC
|
22
|
+
doc
|
23
|
+
.yardoc
|
24
|
+
.bundle
|
25
|
+
Gemfile.lock
|
26
|
+
**/bin
|
27
|
+
*.rbc
|
28
|
+
.rvmrc
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
gemset = ENV['RVM_GEMSET'] || 'webmachine'
|
2
|
+
gemset = "@#{gemset}" unless gemset.to_s == ''
|
3
|
+
|
4
|
+
rvms = %W[ 1.9.2 ].map {|v| "#{v}#{gemset}" }
|
5
|
+
|
6
|
+
guard 'rspec', :cli => "--color --profile", :growl => true, :rvm => rvms do
|
7
|
+
watch(%r{^lib/webmachine/locale/.+$}) { "spec" }
|
8
|
+
watch(%r{^spec/.+_spec\.rb$})
|
9
|
+
watch(%r{^lib/(.+)\.rb$}){ |m| "spec/#{m[1]}_spec.rb" }
|
10
|
+
watch('spec/spec_helper.rb') { "spec" }
|
11
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# webmachine for Ruby [](http://travis-ci.org/seancribbs/webmachine-ruby)
|
2
|
+
|
3
|
+
webmachine-ruby is a port of
|
4
|
+
[Webmachine](https://github.com/basho/webmachine), which is written in
|
5
|
+
Erlang. The goal of both projects is to expose interesting parts of
|
6
|
+
the HTTP protocol to your application in a declarative way. This
|
7
|
+
means that you are less concerned with handling requests directly and
|
8
|
+
more with describing the behavior of the resources that make up your
|
9
|
+
application. Webmachine is not a web framework _per se_, but more of a
|
10
|
+
toolkit for building HTTP-friendly applications. For example, it does
|
11
|
+
not provide a templating engine or a persistence layer; those choices
|
12
|
+
are up to you.
|
13
|
+
|
14
|
+
**NOTE**: _Webmachine is NOT compatible with Rack._ This is
|
15
|
+
intentional! Rack obscures HTTP in a way that makes it hard for
|
16
|
+
Webmachine to do its job properly, and encourages people to add
|
17
|
+
middleware that might break Webmachine's behavior. Rack is also built
|
18
|
+
on the tradition of CGI, which is nice for backwards compatibility but
|
19
|
+
also an antiquated paradigm and should be scuttled (IMHO). _Rack may
|
20
|
+
be supported in the future, but only as a shim to support other web
|
21
|
+
application servers._
|
22
|
+
|
23
|
+
## Getting Started
|
24
|
+
|
25
|
+
Webmachine is very young, but it's still easy to construct an
|
26
|
+
application for it!
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require 'webmachine'
|
30
|
+
# Require any of the files that contain your resources here
|
31
|
+
require 'my_resource'
|
32
|
+
|
33
|
+
# Point all URIs at the MyResource class
|
34
|
+
Webmachine::Dispatcher.add_route(['*'], MyResource)
|
35
|
+
|
36
|
+
# Start the server, binds to port 3000 using WEBrick
|
37
|
+
Webmachine.run
|
38
|
+
```
|
39
|
+
|
40
|
+
Your resource will look something like this:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class MyResource < Webmachine::Resource
|
44
|
+
def to_html
|
45
|
+
"<html><body>Hello, world!</body></html>"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
Run the first file and your application is up. That's all there is to
|
51
|
+
it! If you want to customize your resource more, look at the available
|
52
|
+
callbacks in lib/webmachine/resource/callbacks.rb. For example, you
|
53
|
+
might want to enable "gzip" compression on your resource, for which
|
54
|
+
you can simply add an `encodings_provided` callback method:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class MyResource < Webmachine::Resource
|
58
|
+
def encodings_provided
|
59
|
+
{"gzip" => :encode_gzip, "identity" => :encode_identity}
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_html
|
63
|
+
"<html><body>Hello, world!</body></html>"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
There are many other HTTP features exposed to your resource through
|
69
|
+
callbacks. Give them a try!
|
70
|
+
|
71
|
+
## Features
|
72
|
+
|
73
|
+
* Handles the hard parts of content negotiation, conditional
|
74
|
+
requests, and response codes for you.
|
75
|
+
* Most callbacks can interrupt the decision flow by returning an
|
76
|
+
integer response code. You generally only want to do this when new
|
77
|
+
information comes to light, requiring a modification of the response.
|
78
|
+
* Currently supports WEBrick. Other host servers are planned.
|
79
|
+
* Streaming/chunked response bodies are permitted as Enumerables or Procs.
|
80
|
+
|
81
|
+
## Problems/TODOs
|
82
|
+
|
83
|
+
* Support streamed responses as Fibers.
|
84
|
+
* Configuration, command-line tools, and general polish.
|
85
|
+
* An effort has been made to make the code feel as Ruby-ish as
|
86
|
+
possible, but there is still work to do.
|
87
|
+
* Tracing is exposed as an Array of decisions visited on the response
|
88
|
+
object. You should be able to turn this off and on, and visualize
|
89
|
+
the decisions on the sequence diagram.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/package_task'
|
3
|
+
|
4
|
+
def gemspec
|
5
|
+
$webmachine_gemspec ||= Gem::Specification.load("webmachine.gemspec")
|
6
|
+
end
|
7
|
+
|
8
|
+
Gem::PackageTask.new(gemspec) do |pkg|
|
9
|
+
pkg.need_zip = false
|
10
|
+
pkg.need_tar = false
|
11
|
+
end
|
12
|
+
|
13
|
+
task :gem => :gemspec
|
14
|
+
|
15
|
+
desc %{Validate the gemspec file.}
|
16
|
+
task :gemspec do
|
17
|
+
gemspec.validate
|
18
|
+
end
|
19
|
+
|
20
|
+
desc %{Release the gem to RubyGems.org}
|
21
|
+
task :release => :gem do
|
22
|
+
system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'rspec/core'
|
26
|
+
require 'rspec/core/rake_task'
|
27
|
+
|
28
|
+
desc "Run specs"
|
29
|
+
RSpec::Core::RakeTask.new(:spec)
|
30
|
+
|
31
|
+
task :default => :spec
|
data/examples/webrick.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'webmachine'
|
2
|
+
|
3
|
+
class HelloResource < Webmachine::Resource
|
4
|
+
def last_modified
|
5
|
+
File.mtime(__FILE__)
|
6
|
+
end
|
7
|
+
|
8
|
+
def encodings_provided
|
9
|
+
{ "gzip" => :encode_gzip, "identity" => :encode_identity }
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_html
|
13
|
+
"<html><head><title>Hello from Webmachine</title></head><body>Hello, world!</body></html>"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Webmachine::Dispatcher.add_route([], HelloResource)
|
18
|
+
|
19
|
+
Webmachine.run
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
require 'webmachine/version'
|
3
|
+
require 'webmachine/headers'
|
4
|
+
require 'webmachine/request'
|
5
|
+
require 'webmachine/response'
|
6
|
+
require 'webmachine/dispatcher'
|
7
|
+
|
8
|
+
module Webmachine
|
9
|
+
module Adapters
|
10
|
+
# Connects Webmachine to WEBrick.
|
11
|
+
module WEBrick
|
12
|
+
# Starts the WEBrick adapter
|
13
|
+
def self.run
|
14
|
+
server = Webmachine::Adapters::WEBrick::Server.new :Port => 3000
|
15
|
+
trap("INT"){ server.shutdown }
|
16
|
+
Thread.new { server.start }.join
|
17
|
+
end
|
18
|
+
|
19
|
+
class Server < ::WEBrick::HTTPServer
|
20
|
+
def service(wreq, wres)
|
21
|
+
header = Webmachine::Headers.new
|
22
|
+
wreq.each {|k,v| header[k] = v }
|
23
|
+
request = Webmachine::Request.new(wreq.request_method,
|
24
|
+
wreq.request_uri,
|
25
|
+
header,
|
26
|
+
RequestBody.new(wreq))
|
27
|
+
response = Webmachine::Response.new
|
28
|
+
Webmachine::Dispatcher.dispatch(request, response)
|
29
|
+
wres.status = response.code.to_i
|
30
|
+
response.headers.each do |k,v|
|
31
|
+
wres[k] = v
|
32
|
+
end
|
33
|
+
wres['Server'] = [Webmachine::SERVER_STRING, wres.config[:ServerSoftware]].join(" ")
|
34
|
+
case response.body
|
35
|
+
when String
|
36
|
+
wres.body << response.body
|
37
|
+
when Enumerable
|
38
|
+
response.body.each {|part| wres.body << part }
|
39
|
+
when response.body.respond_to?(:call)
|
40
|
+
wres.body << response.body.call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Wraps the WEBrick request body so that it can be passed to
|
46
|
+
# {Request} while still lazily evaluating the body.
|
47
|
+
class RequestBody
|
48
|
+
def initialize(request)
|
49
|
+
@request = request
|
50
|
+
end
|
51
|
+
|
52
|
+
# Converts the body to a String so you can work with the entire
|
53
|
+
# thing.
|
54
|
+
def to_s
|
55
|
+
@value ? @value.join : @request.body
|
56
|
+
end
|
57
|
+
|
58
|
+
# Iterates over the body in chunks. If the body has previously
|
59
|
+
# been read, this method can be called again and get the same
|
60
|
+
# sequence of chunks.
|
61
|
+
# @yield [chunk]
|
62
|
+
# @yieldparam [String] chunk a chunk of the request body
|
63
|
+
def each
|
64
|
+
if @value
|
65
|
+
@value.each {|chunk| yield chunk }
|
66
|
+
else
|
67
|
+
@value = []
|
68
|
+
@request.body {|chunk| @value << chunk; yield chunk }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'webmachine/adapters/webrick'
|
2
|
+
|
3
|
+
module Webmachine
|
4
|
+
# Contains classes and modules that connect Webmachine to Ruby
|
5
|
+
# application servers.
|
6
|
+
module Adapters
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# @return [Symbol] the current webserver adapter
|
11
|
+
attr_accessor :adapter
|
12
|
+
end
|
13
|
+
|
14
|
+
self.adapter = :WEBrick
|
15
|
+
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
require 'webmachine/translation'
|
2
|
+
|
3
|
+
module Webmachine
|
4
|
+
module Decision
|
5
|
+
# Contains methods concerned with Content Negotiation,
|
6
|
+
# specifically, choosing media types, encodings, character sets
|
7
|
+
# and languages.
|
8
|
+
module Conneg
|
9
|
+
HAS_ENCODING = defined?(::Encoding) # Ruby 1.9 compat
|
10
|
+
|
11
|
+
# Given the 'Accept' header and provided types, chooses an
|
12
|
+
# appropriate media type.
|
13
|
+
# @api private
|
14
|
+
def choose_media_type(provided, header)
|
15
|
+
requested = MediaTypeList.build(header.split(/\s*,\s*/))
|
16
|
+
provided = provided.map do |p| # normalize_provided
|
17
|
+
MediaType.new(*Array(p))
|
18
|
+
end
|
19
|
+
# choose_media_type1
|
20
|
+
chosen = nil
|
21
|
+
requested.each do |_, requested_type|
|
22
|
+
break if chosen = media_match(requested_type, provided)
|
23
|
+
end
|
24
|
+
chosen
|
25
|
+
end
|
26
|
+
|
27
|
+
# Given the 'Accept-Encoding' header and provided encodings, chooses an appropriate
|
28
|
+
# encoding.
|
29
|
+
# @api private
|
30
|
+
def choose_encoding(provided, header)
|
31
|
+
encodings = provided.keys
|
32
|
+
if encoding = do_choose(encodings, header, "identity")
|
33
|
+
response.headers['Content-Encoding'] = encoding unless encoding == 'identity'
|
34
|
+
metadata['Content-Encoding'] = encoding
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Given the 'Accept-Charset' header and provided charsets,
|
39
|
+
# chooses an appropriate charset.
|
40
|
+
# @api private
|
41
|
+
def choose_charset(provided, header)
|
42
|
+
if provided && !provided.empty?
|
43
|
+
charsets = provided.map {|c| c.first }
|
44
|
+
if charset = do_choose(charsets, header, HAS_ENCODING ? Encoding.default_external.name : kcode_charset)
|
45
|
+
metadata['Charset'] = charset
|
46
|
+
end
|
47
|
+
else
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Given the 'Accept-Language' header and provided languages,
|
53
|
+
# chooses an appropriate language.
|
54
|
+
# @api private
|
55
|
+
def choose_language(provided, header)
|
56
|
+
if provided && !provided.empty?
|
57
|
+
requested = PriorityList.build(header.split(/\s*,\s*/))
|
58
|
+
star_priority = requested.priority_of("*")
|
59
|
+
any_ok = star_priority && star_priority > 0.0
|
60
|
+
accepted = requested.find do |priority, range|
|
61
|
+
if priority == 0.0
|
62
|
+
provided.delete_if {|tag| language_match(range, tag) }
|
63
|
+
false
|
64
|
+
else
|
65
|
+
provided.any? {|tag| language_match(range, tag) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
chosen = if accepted
|
69
|
+
provided.find {|tag| language_match(accepted.last, tag) }
|
70
|
+
elsif any_ok
|
71
|
+
provided.first
|
72
|
+
end
|
73
|
+
if chosen
|
74
|
+
metadata['Language'] = chosen
|
75
|
+
response.headers['Content-Language'] = chosen
|
76
|
+
end
|
77
|
+
else
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# RFC2616, section 14.14:
|
83
|
+
#
|
84
|
+
# A language-range matches a language-tag if it exactly
|
85
|
+
# equals the tag, or if it exactly equals a prefix of the
|
86
|
+
# tag such that the first tag character following the prefix
|
87
|
+
# is "-".
|
88
|
+
def language_match(range, tag)
|
89
|
+
range.downcase == tag.downcase || tag =~ /^#{Regexp.escape(range)}\-/i
|
90
|
+
end
|
91
|
+
|
92
|
+
# Makes an conneg choice based what is accepted and what is
|
93
|
+
# provided.
|
94
|
+
# @api private
|
95
|
+
def do_choose(choices, header, default)
|
96
|
+
choices = choices.dup.map {|s| s.downcase }
|
97
|
+
accepted = PriorityList.build(header.split(/\s*,\s/))
|
98
|
+
default_priority = accepted.priority_of(default)
|
99
|
+
star_priority = accepted.priority_of("*")
|
100
|
+
default_ok = (default_priority.nil? && star_priority != 0.0) || default_priority
|
101
|
+
any_ok = star_priority && star_priority > 0.0
|
102
|
+
chosen = accepted.find do |priority, acceptable|
|
103
|
+
if priority == 0.0
|
104
|
+
choices.delete(acceptable.downcase)
|
105
|
+
false
|
106
|
+
else
|
107
|
+
choices.include?(acceptable.downcase)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
(chosen && chosen.last) || # Use the matching one
|
111
|
+
(any_ok && choices.first) || # Or first if "*"
|
112
|
+
(default_ok && choices.include?(default) && default) # Or default
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
# Matches acceptable items that include 'q' values
|
117
|
+
CONNEG_REGEX = /^\s*(\S+);\s*q=(\S*)\s*$/
|
118
|
+
|
119
|
+
# Matches sub-type parameters
|
120
|
+
PARAMS_REGEX = /;([^=]+)=([^;=\s]+)/
|
121
|
+
|
122
|
+
# Matches valid media types
|
123
|
+
MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\S+\s*)*)\s*$/
|
124
|
+
|
125
|
+
# Encapsulates a MIME media type, with logic for matching types.
|
126
|
+
class MediaType
|
127
|
+
# Creates a new MediaType by parsing its string representation.
|
128
|
+
def self.parse(str)
|
129
|
+
if str =~ MEDIA_TYPE_REGEX
|
130
|
+
type, raw_params = $1, $2
|
131
|
+
params = Hash[raw_params.scan(PARAMS_REGEX)]
|
132
|
+
new(type, params)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [String] the MIME media type
|
137
|
+
attr_accessor :type
|
138
|
+
|
139
|
+
# @return [Hash] any type parameters, e.g. charset
|
140
|
+
attr_accessor :params
|
141
|
+
|
142
|
+
def initialize(type, params={})
|
143
|
+
@type, @params = type, params
|
144
|
+
end
|
145
|
+
|
146
|
+
# Detects whether the {MediaType} represents an open wildcard
|
147
|
+
# type, that is, "*/*" without any {#params}.
|
148
|
+
def matches_all?
|
149
|
+
@type == "*/*" && @params.empty?
|
150
|
+
end
|
151
|
+
|
152
|
+
def ==(other)
|
153
|
+
other = self.class.parse(other) if String === other
|
154
|
+
other.type == type && other.params == params
|
155
|
+
end
|
156
|
+
|
157
|
+
# Detects whether this {MediaType} matches the other {MediaType},
|
158
|
+
# taking into account wildcards.
|
159
|
+
def match?(other)
|
160
|
+
type_matches?(other) && other.params == params
|
161
|
+
end
|
162
|
+
|
163
|
+
# Reconstitutes the type into a String
|
164
|
+
def to_s
|
165
|
+
[type, *params.map {|k,v| "#{k}=#{v}" }].join(";")
|
166
|
+
end
|
167
|
+
|
168
|
+
# @return [String] The major type, e.g. "application", "text", "image"
|
169
|
+
def major
|
170
|
+
type.split("/").first
|
171
|
+
end
|
172
|
+
|
173
|
+
# @return [String] the minor or sub-type, e.g. "json", "html", "jpeg"
|
174
|
+
def minor
|
175
|
+
type.split("/").last
|
176
|
+
end
|
177
|
+
|
178
|
+
def type_matches?(other)
|
179
|
+
if ["*", "*/*", type].include?(other.type)
|
180
|
+
true
|
181
|
+
else
|
182
|
+
other.major == major && other.minor == "*"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Matches the requested media type (with potential modifiers)
|
188
|
+
# against the provided types (with potential modifiers).
|
189
|
+
# @param [MediaType] requested the requested media type
|
190
|
+
# @param [Array<MediaType>] provided the provided media
|
191
|
+
# types
|
192
|
+
# @return [MediaType] the first media type that matches
|
193
|
+
def media_match(requested, provided)
|
194
|
+
return provided.first if requested.matches_all?
|
195
|
+
provided.find do |p|
|
196
|
+
p.match?(requested)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Translate a KCODE value to a charset name
|
201
|
+
def kcode_charset
|
202
|
+
case $KCODE
|
203
|
+
when /^U/i
|
204
|
+
"UTF-8"
|
205
|
+
when /^S/i
|
206
|
+
"Shift-JIS"
|
207
|
+
when /^B/i
|
208
|
+
"Big5"
|
209
|
+
else #when /^A/i, nil
|
210
|
+
"ASCII"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# @private
|
215
|
+
# Content-negotiation priority list that takes into account both
|
216
|
+
# assigned priority ("q" value) as well as order, since items
|
217
|
+
# that come earlier in an acceptance list have higher priority
|
218
|
+
# by fiat.
|
219
|
+
class PriorityList
|
220
|
+
# Given an acceptance list, create a PriorityList from them.
|
221
|
+
def self.build(list)
|
222
|
+
new.tap do |plist|
|
223
|
+
list.each {|item| plist.add_header_val(item) }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
include Enumerable
|
228
|
+
|
229
|
+
# Creates a {PriorityList}.
|
230
|
+
# @see PriorityList::build
|
231
|
+
def initialize
|
232
|
+
@hash = Hash.new {|h,k| h[k] = [] }
|
233
|
+
@index = {}
|
234
|
+
end
|
235
|
+
|
236
|
+
# Adds an acceptable item with the given priority to the list.
|
237
|
+
# @param [Float] q the priority
|
238
|
+
# @param [String] choice the acceptable item
|
239
|
+
def add(q, choice)
|
240
|
+
@index[choice] = q
|
241
|
+
@hash[q] << choice
|
242
|
+
end
|
243
|
+
|
244
|
+
# Given a raw acceptable value from an acceptance header,
|
245
|
+
# parse and add it to the list.
|
246
|
+
# @param [String] c the raw acceptable item
|
247
|
+
# @see #add
|
248
|
+
def add_header_val(c)
|
249
|
+
if c =~ CONNEG_REGEX
|
250
|
+
choice, q = $1, $2
|
251
|
+
q = "0" << q if q =~ /^\./ # handle strange FeedBurner Accept
|
252
|
+
add(q.to_f,choice)
|
253
|
+
else
|
254
|
+
add(1.0, c)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# @param [Float] q the priority to lookup
|
259
|
+
# @return [Array<String>] the list of acceptable items at
|
260
|
+
# the given priority
|
261
|
+
def [](q)
|
262
|
+
@hash[q]
|
263
|
+
end
|
264
|
+
|
265
|
+
# @param [String] choice the acceptable item
|
266
|
+
# @return [Float] the priority of that value
|
267
|
+
def priority_of(choice)
|
268
|
+
@index[choice]
|
269
|
+
end
|
270
|
+
|
271
|
+
# Iterates over the list in priority order, that is, taking
|
272
|
+
# into account the order in which items were added as well as
|
273
|
+
# their priorities.
|
274
|
+
# @yield [q,v]
|
275
|
+
# @yieldparam [Float] q the acceptable item's priority
|
276
|
+
# @yieldparam [String] v the acceptable item
|
277
|
+
def each
|
278
|
+
@hash.to_a.sort.reverse_each do |q,l|
|
279
|
+
l.each {|v| yield q, v }
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Like a {PriorityList}, but for {MediaTypes}, since they have
|
285
|
+
# parameters in addition to q.
|
286
|
+
# @private
|
287
|
+
class MediaTypeList < PriorityList
|
288
|
+
include Translation
|
289
|
+
|
290
|
+
# Overrides {PriorityList#add_header_val} to insert
|
291
|
+
# {MediaType} items instead of Strings.
|
292
|
+
# @see PriorityList#add_header_val
|
293
|
+
def add_header_val(c)
|
294
|
+
if mt = MediaType.parse(c)
|
295
|
+
q = mt.params.delete('q') || 1.0
|
296
|
+
add(q.to_f, mt)
|
297
|
+
else
|
298
|
+
raise MalformedRequest, t('invalid_media_type', :type => c)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|