webmachine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![travis](https://secure.travis-ci.org/seancribbs/webmachine-ruby.png)](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
|