xenon-http 0.0.4
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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +18 -0
- data/.gitignore +25 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +20 -0
- data/Guardfile +16 -0
- data/LICENSE +22 -0
- data/README.md +116 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/examples/hello_world/config.ru +3 -0
- data/examples/hello_world/hello_world.rb +27 -0
- data/xenon-http/lib/xenon/auth.rb +63 -0
- data/xenon-http/lib/xenon/errors.rb +5 -0
- data/xenon-http/lib/xenon/etag.rb +48 -0
- data/xenon-http/lib/xenon/headers.rb +112 -0
- data/xenon-http/lib/xenon/headers/accept.rb +34 -0
- data/xenon-http/lib/xenon/headers/accept_charset.rb +59 -0
- data/xenon-http/lib/xenon/headers/accept_encoding.rb +63 -0
- data/xenon-http/lib/xenon/headers/accept_language.rb +59 -0
- data/xenon-http/lib/xenon/headers/authorization.rb +50 -0
- data/xenon-http/lib/xenon/headers/cache_control.rb +56 -0
- data/xenon-http/lib/xenon/headers/content_type.rb +23 -0
- data/xenon-http/lib/xenon/headers/if_match.rb +53 -0
- data/xenon-http/lib/xenon/headers/if_modified_since.rb +22 -0
- data/xenon-http/lib/xenon/headers/if_none_match.rb +53 -0
- data/xenon-http/lib/xenon/headers/if_range.rb +45 -0
- data/xenon-http/lib/xenon/headers/if_unmodified_since.rb +22 -0
- data/xenon-http/lib/xenon/headers/user_agent.rb +65 -0
- data/xenon-http/lib/xenon/headers/www_authenticate.rb +71 -0
- data/xenon-http/lib/xenon/http.rb +7 -0
- data/xenon-http/lib/xenon/http_version.rb +3 -0
- data/xenon-http/lib/xenon/media_type.rb +162 -0
- data/xenon-http/lib/xenon/parsers/basic_rules.rb +86 -0
- data/xenon-http/lib/xenon/parsers/header_rules.rb +60 -0
- data/xenon-http/lib/xenon/parsers/media_type.rb +53 -0
- data/xenon-http/lib/xenon/quoted_string.rb +20 -0
- data/xenon-http/spec/spec_helper.rb +94 -0
- data/xenon-http/spec/xenon/etag_spec.rb +19 -0
- data/xenon-http/spec/xenon/headers/accept_charset_spec.rb +31 -0
- data/xenon-http/spec/xenon/headers/accept_encoding_spec.rb +40 -0
- data/xenon-http/spec/xenon/headers/accept_language_spec.rb +33 -0
- data/xenon-http/spec/xenon/headers/accept_spec.rb +54 -0
- data/xenon-http/spec/xenon/headers/authorization_spec.rb +47 -0
- data/xenon-http/spec/xenon/headers/cache_control_spec.rb +64 -0
- data/xenon-http/spec/xenon/headers/if_match_spec.rb +73 -0
- data/xenon-http/spec/xenon/headers/if_modified_since_spec.rb +19 -0
- data/xenon-http/spec/xenon/headers/if_none_match_spec.rb +79 -0
- data/xenon-http/spec/xenon/headers/if_range_spec.rb +45 -0
- data/xenon-http/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
- data/xenon-http/spec/xenon/headers/user_agent_spec.rb +67 -0
- data/xenon-http/spec/xenon/headers/www_authenticate_spec.rb +43 -0
- data/xenon-http/spec/xenon/media_type_spec.rb +267 -0
- data/xenon-http/xenon-http.gemspec +25 -0
- data/xenon-routing/lib/xenon/api.rb +118 -0
- data/xenon-routing/lib/xenon/marshallers.rb +48 -0
- data/xenon-routing/lib/xenon/request.rb +40 -0
- data/xenon-routing/lib/xenon/response.rb +29 -0
- data/xenon-routing/lib/xenon/routing.rb +6 -0
- data/xenon-routing/lib/xenon/routing/context.rb +35 -0
- data/xenon-routing/lib/xenon/routing/directives.rb +14 -0
- data/xenon-routing/lib/xenon/routing/header_directives.rb +32 -0
- data/xenon-routing/lib/xenon/routing/method_directives.rb +26 -0
- data/xenon-routing/lib/xenon/routing/param_directives.rb +22 -0
- data/xenon-routing/lib/xenon/routing/path_directives.rb +37 -0
- data/xenon-routing/lib/xenon/routing/route_directives.rb +51 -0
- data/xenon-routing/lib/xenon/routing/security_directives.rb +34 -0
- data/xenon-routing/lib/xenon/routing_version.rb +3 -0
- data/xenon-routing/spec/spec_helper.rb +94 -0
- data/xenon-routing/xenon-routing.gemspec +25 -0
- data/xenon.gemspec +26 -0
- metadata +160 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'xenon/headers'
|
2
|
+
require 'xenon/parsers/header_rules'
|
3
|
+
require 'xenon/errors'
|
4
|
+
require 'xenon/etag'
|
5
|
+
|
6
|
+
module Xenon
|
7
|
+
class Headers
|
8
|
+
# http://tools.ietf.org/html/rfc7232#section-3.1
|
9
|
+
class IfMatch < ListHeader 'If-Match'
|
10
|
+
def initialize(*etags)
|
11
|
+
super(etags)
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :etags, :values
|
15
|
+
|
16
|
+
def self.wildcard
|
17
|
+
new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.parse(s)
|
21
|
+
tree = Parsers::IfMatchHeader.new.parse(s)
|
22
|
+
Parsers::IfMatchHeaderTransform.new.apply(tree)
|
23
|
+
end
|
24
|
+
|
25
|
+
def wildcard?
|
26
|
+
etags.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def merge(other)
|
30
|
+
raise Xenon::ProtocolError.new('Cannot merge wildcard headers') if wildcard? || other.wildcard?
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
wildcard? ? '*' : super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Parsers
|
41
|
+
class IfMatchHeader < Parslet::Parser
|
42
|
+
include ETagHeaderRules
|
43
|
+
rule(:if_match) { (wildcard | (etag >> (list_sep >> etag).repeat)).as(:if_match) }
|
44
|
+
root(:if_match)
|
45
|
+
end
|
46
|
+
|
47
|
+
class IfMatchHeaderTransform < ETagHeaderTransform
|
48
|
+
rule(if_match: { wildcard: simple(:w) }) { Headers::IfMatch.new }
|
49
|
+
rule(if_match: sequence(:et)) { Headers::IfMatch.new(*et) }
|
50
|
+
rule(if_match: simple(:et)) { Headers::IfMatch.new(et) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'xenon/headers'
|
2
|
+
|
3
|
+
module Xenon
|
4
|
+
class Headers
|
5
|
+
# http://tools.ietf.org/html/rfc7232#section-3.3
|
6
|
+
class IfModifiedSince < Header 'If-Modified-Since'
|
7
|
+
attr_reader :date
|
8
|
+
|
9
|
+
def initialize(date)
|
10
|
+
@date = date
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.parse(s)
|
14
|
+
new(Time.httpdate(s))
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
@date.httpdate
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'xenon/headers'
|
2
|
+
require 'xenon/parsers/header_rules'
|
3
|
+
require 'xenon/errors'
|
4
|
+
require 'xenon/etag'
|
5
|
+
|
6
|
+
module Xenon
|
7
|
+
class Headers
|
8
|
+
# http://tools.ietf.org/html/rfc7232#section-3.2
|
9
|
+
class IfNoneMatch < ListHeader 'If-None-Match'
|
10
|
+
def initialize(*etags)
|
11
|
+
super(etags)
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :etags, :values
|
15
|
+
|
16
|
+
def self.wildcard
|
17
|
+
new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.parse(s)
|
21
|
+
tree = Parsers::IfNoneMatchHeader.new.parse(s)
|
22
|
+
Parsers::IfNoneMatchHeaderTransform.new.apply(tree)
|
23
|
+
end
|
24
|
+
|
25
|
+
def wildcard?
|
26
|
+
etags.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def merge(other)
|
30
|
+
raise Xenon::ProtocolError.new('Cannot merge wildcard headers') if wildcard? || other.wildcard?
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
wildcard? ? '*' : super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Parsers
|
41
|
+
class IfNoneMatchHeader < Parslet::Parser
|
42
|
+
include ETagHeaderRules
|
43
|
+
rule(:if_none_match) { (wildcard | (etag >> (list_sep >> etag).repeat)).as(:if_none_match) }
|
44
|
+
root(:if_none_match)
|
45
|
+
end
|
46
|
+
|
47
|
+
class IfNoneMatchHeaderTransform < ETagHeaderTransform
|
48
|
+
rule(if_none_match: { wildcard: simple(:w) }) { Headers::IfNoneMatch.new }
|
49
|
+
rule(if_none_match: sequence(:et)) { Headers::IfNoneMatch.new(*et) }
|
50
|
+
rule(if_none_match: simple(:et)) { Headers::IfNoneMatch.new(et) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'xenon/headers'
|
2
|
+
require 'xenon/parsers/header_rules'
|
3
|
+
require 'xenon/errors'
|
4
|
+
require 'xenon/etag'
|
5
|
+
|
6
|
+
module Xenon
|
7
|
+
class Headers
|
8
|
+
# http://tools.ietf.org/html/rfc7233#section-3.2
|
9
|
+
class IfRange < Header 'If-Range'
|
10
|
+
attr_reader :date, :etag
|
11
|
+
|
12
|
+
def initialize(value)
|
13
|
+
case value
|
14
|
+
when Time, DateTime, Date then @date = value
|
15
|
+
when ETag then @etag = value
|
16
|
+
when String then @etag = ETag.parse(value)
|
17
|
+
else raise ArgumentError, 'Value must be a time or an etag.'
|
18
|
+
end
|
19
|
+
|
20
|
+
raise ProtocolError, 'If-Range headers must use strong ETags.' if @etag && @etag.weak?
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse(s)
|
24
|
+
tree = Parsers::IfRangeHeader.new.parse(s)
|
25
|
+
Parsers::IfRangeHeaderTransform.new.apply(tree)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
@etag ? @etag.to_s : @date.httpdate
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module Parsers
|
35
|
+
class IfRangeHeader < Parslet::Parser
|
36
|
+
include ETagHeaderRules
|
37
|
+
rule(:if_range) { (etag | http_date).as(:if_range) }
|
38
|
+
root(:if_range)
|
39
|
+
end
|
40
|
+
|
41
|
+
class IfRangeHeaderTransform < ETagHeaderTransform
|
42
|
+
rule(if_range: simple(:v)) { Headers::IfRange.new(v) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'xenon/headers'
|
2
|
+
|
3
|
+
module Xenon
|
4
|
+
class Headers
|
5
|
+
# http://tools.ietf.org/html/rfc7232#section-3.4
|
6
|
+
class IfUnmodifiedSince < Header 'If-Unmodified-Since'
|
7
|
+
attr_reader :date
|
8
|
+
|
9
|
+
def initialize(date)
|
10
|
+
@date = date
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.parse(s)
|
14
|
+
new(Time.httpdate(s))
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
@date.httpdate
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'xenon/headers'
|
2
|
+
require 'xenon/parsers/header_rules'
|
3
|
+
|
4
|
+
module Xenon
|
5
|
+
class Product
|
6
|
+
attr_reader :name, :version, :comment
|
7
|
+
|
8
|
+
def initialize(name, version = nil, comment = nil)
|
9
|
+
@name = name
|
10
|
+
@version = version
|
11
|
+
@comment = comment
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
s = ''
|
16
|
+
s << @name if @name
|
17
|
+
s << '/' << @version if @version
|
18
|
+
if @comment
|
19
|
+
s << ' ' unless s.empty?
|
20
|
+
s << '(' << @comment << ')'
|
21
|
+
end
|
22
|
+
s
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Headers
|
27
|
+
# http://tools.ietf.org/html/rfc7231#section-5.5.3
|
28
|
+
class UserAgent < Header 'User-Agent'
|
29
|
+
attr_reader :products
|
30
|
+
|
31
|
+
def initialize(*products)
|
32
|
+
@products = products
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parse(s)
|
36
|
+
tree = Parsers::UserAgentHeader.new.parse(s)
|
37
|
+
Parsers::UserAgentHeaderTransform.new.apply(tree)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@products.map(&:to_s).join(' ')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module Parsers
|
47
|
+
class UserAgentHeader < Parslet::Parser
|
48
|
+
include HeaderRules
|
49
|
+
rule(:product) { (token.as(:name) >> (str('/') >> token.as(:version)).maybe >> (rws >> comment.as(:comment)).maybe).as(:product) }
|
50
|
+
rule(:product_comment) { comment.as(:product_comment) }
|
51
|
+
rule(:user_agent) { (product >> (rws >> (product | product_comment)).repeat).as(:user_agent) }
|
52
|
+
root(:user_agent)
|
53
|
+
end
|
54
|
+
|
55
|
+
class UserAgentHeaderTransform < HeaderTransform
|
56
|
+
rule(product: { name: simple(:p), version: simple(:v), comment: simple(:c) }) { Product.new(p, v, c) }
|
57
|
+
rule(product: { name: simple(:p), version: simple(:v) }) { Product.new(p, v) }
|
58
|
+
rule(product: { name: simple(:p), comment: simple(:c) }) { Product.new(p, nil, c) }
|
59
|
+
rule(product: { name: simple(:p) }) { Product.new(p) }
|
60
|
+
rule(product_comment: simple(:c)) { Product.new(nil, nil, c) }
|
61
|
+
rule(user_agent: sequence(:p)) { Headers::UserAgent.new(*p) }
|
62
|
+
rule(user_agent: simple(:p)) { Headers::UserAgent.new(p) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'xenon/headers'
|
4
|
+
require 'xenon/parsers/header_rules'
|
5
|
+
require 'xenon/quoted_string'
|
6
|
+
|
7
|
+
module Xenon
|
8
|
+
class Headers
|
9
|
+
class Challenge
|
10
|
+
extend Forwardable
|
11
|
+
using QuotedString
|
12
|
+
|
13
|
+
attr_reader :auth_scheme
|
14
|
+
def_delegators :@params, :key?, :include?, :[]
|
15
|
+
|
16
|
+
def initialize(auth_scheme, params = {})
|
17
|
+
@auth_scheme = auth_scheme
|
18
|
+
@params = params.with_indifferent_access
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(name, *args, &block)
|
22
|
+
name = name.to_sym
|
23
|
+
@params.key?(name) ? @params[name] : super
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_to_missing?(name, include_all)
|
27
|
+
@params.key?(name.to_sym) || super
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
param_string = @params.map { |k, v| "#{k}=#{v.quote}"}.join(', ')
|
32
|
+
"#{@auth_scheme} #{param_string}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# https://tools.ietf.org/html/rfc7235#section-4.1
|
37
|
+
class WWWAuthenticate < ListHeader 'WWW-Authenticate'
|
38
|
+
def initialize(*challenges)
|
39
|
+
super(challenges)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :challenges, :values
|
43
|
+
|
44
|
+
def self.parse(s)
|
45
|
+
tree = Parsers::WWWAuthenticateHeader.new.parse(s)
|
46
|
+
Parsers::WWWAuthenticateHeaderTransform.new.apply(tree)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
challenges.map(&:to_s).join(', ')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module Parsers
|
56
|
+
class WWWAuthenticateHeader < Parslet::Parser
|
57
|
+
include AuthHeaderRules
|
58
|
+
rule(:challenge) { (auth_scheme >> sp >> (auth_params | token68)).as(:challenge) }
|
59
|
+
rule(:www_authenticate) { (challenge >> (comma >> challenge).repeat).as(:www_authenticate) }
|
60
|
+
root(:www_authenticate)
|
61
|
+
end
|
62
|
+
|
63
|
+
class WWWAuthenticateHeaderTransform < HeaderTransform
|
64
|
+
rule(auth_param: { name: simple(:n), value: simple(:v) }) { Tuple.new(n, v) }
|
65
|
+
rule(challenge: { auth_scheme: simple(:s), auth_params: simple(:p) }) { Headers::Challenge.new(s, Hash[*p.to_a]) }
|
66
|
+
rule(challenge: { auth_scheme: simple(:s), auth_params: sequence(:p) }) { Headers::Challenge.new(s, Hash[p.map(&:to_a)]) }
|
67
|
+
rule(www_authenticate: simple(:c)) { Headers::WWWAuthenticate.new(c) }
|
68
|
+
rule(www_authenticate: sequence(:c)) { Headers::WWWAuthenticate.new(*c) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'xenon/errors'
|
2
|
+
require 'xenon/parsers/media_type'
|
3
|
+
|
4
|
+
module Xenon
|
5
|
+
|
6
|
+
# A media type.
|
7
|
+
#
|
8
|
+
# @see ContentType
|
9
|
+
# @see MediaRange
|
10
|
+
class MediaType
|
11
|
+
attr_reader :type, :subtype, :params
|
12
|
+
|
13
|
+
# Initializes a new instance of MediaType.
|
14
|
+
#
|
15
|
+
# @param type [String] The main type, e.g. 'application'.
|
16
|
+
# @param subtype [String] The subtype, e.g. 'json'.
|
17
|
+
# @param params [Hash] Any params for the media type; don't use 'q' or 'charset'.
|
18
|
+
def initialize(type, subtype, params = {})
|
19
|
+
@type = type
|
20
|
+
@subtype = subtype
|
21
|
+
@params = params
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parses a media type.
|
25
|
+
#
|
26
|
+
# @param s [String] The media type string.
|
27
|
+
# @return [MediaType] The media type.
|
28
|
+
def self.parse(s)
|
29
|
+
tree = Parsers::MediaType.new.parse(s)
|
30
|
+
Parsers::MediaTypeTransform.new.apply(tree)
|
31
|
+
rescue Parslet::ParseFailed
|
32
|
+
raise Xenon::ParseError.new("Invalid media type (#{s}).")
|
33
|
+
end
|
34
|
+
|
35
|
+
%w(application audio image message multipart text video).each do |type|
|
36
|
+
define_method "#{type}?" do
|
37
|
+
@type == type
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def experimental?
|
42
|
+
@subtype.start_with?('x.') # not x- see http://tools.ietf.org/html/rfc6838#section-3.4
|
43
|
+
end
|
44
|
+
|
45
|
+
def personal?
|
46
|
+
@subtype.start_with?('prs.')
|
47
|
+
end
|
48
|
+
|
49
|
+
def vendor?
|
50
|
+
@subtype.start_with?('vnd.')
|
51
|
+
end
|
52
|
+
|
53
|
+
%w(ber der fastinfoset json wbxml xml zip).each do |format|
|
54
|
+
define_method "#{format}?" do
|
55
|
+
@subtype == format || @subtype.end_with?("+#{format}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Creates a {MediaRange} using this media type with a quality factor.
|
60
|
+
#
|
61
|
+
# @param q [Numeric] A value between 1.0 (most desirable) and 0.0 (not acceptable).
|
62
|
+
# @return [MediaRange] The media range.
|
63
|
+
def with_q(q)
|
64
|
+
MediaRange.new(self, q)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Creates a {ContentType} using this media type with a charset.
|
68
|
+
#
|
69
|
+
# @param charset [String] The desired charset, e.g. 'utf-8'.
|
70
|
+
# @return [ContentType] The content type.
|
71
|
+
def with_charset(charset)
|
72
|
+
ContentType.new(self, charset)
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_s
|
76
|
+
"#{@type}/#{@subtype}" << @params.map { |n, v| v ? "; #{n}=#{v}" : "; #{n}" }.join
|
77
|
+
end
|
78
|
+
|
79
|
+
JSON = MediaType.new('application', 'json')
|
80
|
+
XML = MediaType.new('application', 'xml')
|
81
|
+
end
|
82
|
+
|
83
|
+
# A content type.
|
84
|
+
class ContentType
|
85
|
+
attr_reader :media_type, :charset
|
86
|
+
|
87
|
+
DEFAULT_CHARSET = 'utf-8' # historically iso-8859-1 but see http://tools.ietf.org/html/rfc7231#appendix-B
|
88
|
+
|
89
|
+
def initialize(media_type, charset = DEFAULT_CHARSET)
|
90
|
+
@media_type = media_type
|
91
|
+
@charset = charset
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.parse(s)
|
95
|
+
media_type = MediaType.parse(s)
|
96
|
+
charset = media_type.params.delete('charset') || DEFAULT_CHARSET
|
97
|
+
ContentType.new(media_type, charset)
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_s
|
101
|
+
"#{@media_type}; charset=#{@charset}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class MediaRange
|
106
|
+
include Comparable
|
107
|
+
|
108
|
+
DEFAULT_Q = 1.0
|
109
|
+
|
110
|
+
attr_reader :type, :subtype, :q, :params
|
111
|
+
|
112
|
+
def initialize(type, subtype, params = {})
|
113
|
+
@type = type
|
114
|
+
@subtype = subtype
|
115
|
+
@q = Float(params.delete('q')) rescue DEFAULT_Q
|
116
|
+
@params = params
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.parse(s)
|
120
|
+
tree = Parsers::MediaRange.new.parse(s)
|
121
|
+
Parsers::MediaTypeTransform.new.apply(tree)
|
122
|
+
rescue Parslet::ParseFailed
|
123
|
+
raise Xenon::ParseError.new("Invalid media range (#{s})")
|
124
|
+
end
|
125
|
+
|
126
|
+
def <=>(other)
|
127
|
+
dt = compare_types(@type, other.type)
|
128
|
+
return dt if dt != 0
|
129
|
+
ds = compare_types(@subtype, other.subtype)
|
130
|
+
return ds if ds != 0
|
131
|
+
dp = params.size <=> other.params.size
|
132
|
+
return dp if dp != 0
|
133
|
+
@q <=> other.q
|
134
|
+
end
|
135
|
+
|
136
|
+
def =~(media_type)
|
137
|
+
(type == '*' || type == media_type.type) &&
|
138
|
+
(subtype == '*' || subtype == media_type.subtype) &&
|
139
|
+
params.all? { |n, v| media_type.params[n] == v }
|
140
|
+
end
|
141
|
+
|
142
|
+
alias_method :===, :=~
|
143
|
+
|
144
|
+
def to_s
|
145
|
+
s = "#{@type}/#{@subtype}"
|
146
|
+
s << @params.map { |n, v| v ? "; #{n}=#{v}" : "; #{n}" }.join
|
147
|
+
s << "; q=#{@q}" if @q != DEFAULT_Q
|
148
|
+
s
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def compare_types(a, b)
|
154
|
+
if a == b then 0
|
155
|
+
elsif a == '*' then -1
|
156
|
+
elsif b == '*' then 1
|
157
|
+
else 0
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|