xenon 0.0.2 → 0.0.3
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 +4 -4
- data/.codeclimate.yml +18 -0
- data/.gitignore +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +10 -0
- data/Guardfile +0 -32
- data/README.md +59 -5
- data/examples/hello_world/config.ru +3 -0
- data/examples/hello_world/hello_world.rb +17 -0
- data/lib/xenon.rb +62 -49
- data/lib/xenon/auth.rb +63 -0
- data/lib/xenon/errors.rb +1 -0
- data/lib/xenon/etag.rb +48 -0
- data/lib/xenon/headers.rb +2 -4
- data/lib/xenon/headers/accept.rb +3 -2
- data/lib/xenon/headers/accept_charset.rb +5 -5
- data/lib/xenon/headers/accept_encoding.rb +5 -5
- data/lib/xenon/headers/accept_language.rb +5 -5
- data/lib/xenon/headers/authorization.rb +7 -53
- data/lib/xenon/headers/cache_control.rb +3 -3
- data/lib/xenon/headers/content_type.rb +1 -1
- data/lib/xenon/headers/if_match.rb +53 -0
- data/lib/xenon/headers/if_modified_since.rb +22 -0
- data/lib/xenon/headers/if_none_match.rb +53 -0
- data/lib/xenon/headers/if_range.rb +45 -0
- data/lib/xenon/headers/if_unmodified_since.rb +22 -0
- data/lib/xenon/headers/user_agent.rb +65 -0
- data/lib/xenon/headers/www_authenticate.rb +70 -0
- data/lib/xenon/media_type.rb +24 -2
- data/lib/xenon/parsers/basic_rules.rb +38 -7
- data/lib/xenon/parsers/header_rules.rb +49 -3
- data/lib/xenon/parsers/media_type.rb +4 -3
- data/lib/xenon/quoted_string.rb +11 -1
- data/lib/xenon/routing/directives.rb +14 -0
- data/lib/xenon/routing/header_directives.rb +32 -0
- data/lib/xenon/routing/method_directives.rb +26 -0
- data/lib/xenon/routing/param_directives.rb +22 -0
- data/lib/xenon/routing/path_directives.rb +37 -0
- data/lib/xenon/routing/route_directives.rb +51 -0
- data/lib/xenon/routing/security_directives.rb +20 -0
- data/lib/xenon/version.rb +1 -1
- data/spec/spec_helper.rb +3 -0
- data/spec/xenon/etag_spec.rb +19 -0
- data/spec/xenon/headers/if_match_spec.rb +73 -0
- data/spec/xenon/headers/if_modified_since_spec.rb +19 -0
- data/spec/xenon/headers/if_none_match_spec.rb +79 -0
- data/spec/xenon/headers/if_range_spec.rb +45 -0
- data/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
- data/spec/xenon/headers/user_agent_spec.rb +67 -0
- data/spec/xenon/headers/www_authenticate_spec.rb +43 -0
- data/xenon.gemspec +4 -3
- metadata +60 -10
- data/lib/xenon/routing.rb +0 -133
data/lib/xenon/errors.rb
CHANGED
data/lib/xenon/etag.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'xenon/parsers/header_rules'
|
2
|
+
|
3
|
+
module Xenon
|
4
|
+
class ETag
|
5
|
+
attr_reader :opaque_tag
|
6
|
+
|
7
|
+
def initialize(opaque_tag, weak: false)
|
8
|
+
@opaque_tag = opaque_tag
|
9
|
+
@weak = weak
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.parse(s)
|
13
|
+
tree = Parsers::ETag.new.etag.parse(s)
|
14
|
+
Parsers::ETagHeaderTransform.new.apply(tree)
|
15
|
+
end
|
16
|
+
|
17
|
+
def weak?
|
18
|
+
@weak
|
19
|
+
end
|
20
|
+
|
21
|
+
def strong?
|
22
|
+
!weak?
|
23
|
+
end
|
24
|
+
|
25
|
+
def strong_eq?(other)
|
26
|
+
strong? && other.strong? && @opaque_tag == other.opaque_tag
|
27
|
+
end
|
28
|
+
|
29
|
+
def weak_eq?(other)
|
30
|
+
@opaque_tag == other.opaque_tag
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
strong? == other.strong? && @opaque_tag == other.opaque_tag
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
s = weak? ? "W/" : ""
|
39
|
+
s << '"' << @opaque_tag << '"'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Parsers
|
44
|
+
class ETag < Parslet::Parser
|
45
|
+
include ETagHeaderRules
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/xenon/headers.rb
CHANGED
@@ -106,9 +106,7 @@ module Xenon
|
|
106
106
|
@value
|
107
107
|
end
|
108
108
|
end
|
109
|
-
|
110
|
-
[:Accept, :AcceptCharset, :AcceptEncoding, :CacheControl, :ContentType].each do |sym|
|
111
|
-
autoload sym, "xenon/headers/#{sym.to_s.underscore}"
|
112
|
-
end
|
113
109
|
end
|
114
110
|
end
|
111
|
+
|
112
|
+
Dir[File.join(__dir__, 'headers', '*.rb')].each { |f| require f }
|
data/lib/xenon/headers/accept.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'xenon/headers'
|
2
|
+
require 'xenon/parsers/header_rules'
|
2
3
|
require 'xenon/parsers/media_type'
|
3
4
|
|
4
5
|
module Xenon
|
@@ -20,8 +21,8 @@ module Xenon
|
|
20
21
|
|
21
22
|
module Parsers
|
22
23
|
class AcceptHeader < Parslet::Parser
|
23
|
-
include MediaTypeRules
|
24
|
-
rule(:accept) { (media_range >> (
|
24
|
+
include HeaderRules, MediaTypeRules
|
25
|
+
rule(:accept) { (media_range >> (list_sep >> media_range).repeat).as(:accept) }
|
25
26
|
root(:accept)
|
26
27
|
end
|
27
28
|
|
@@ -41,17 +41,17 @@ module Xenon
|
|
41
41
|
|
42
42
|
module Parsers
|
43
43
|
class AcceptCharsetHeader < Parslet::Parser
|
44
|
-
include
|
44
|
+
include HeaderRules
|
45
45
|
rule(:charset) { token.as(:charset) >> sp? }
|
46
46
|
rule(:wildcard) { str('*').as(:charset) >> sp? }
|
47
47
|
rule(:charset_range) { (charset | wildcard) >> weight.maybe }
|
48
|
-
rule(:accept_charset) { (charset_range >> (
|
48
|
+
rule(:accept_charset) { (charset_range >> (list_sep >> charset_range).repeat).as(:accept_charset) }
|
49
49
|
root(:accept_charset)
|
50
50
|
end
|
51
51
|
|
52
|
-
class AcceptCharsetHeaderTransform <
|
53
|
-
rule(charset: simple(:c), q: simple(:q)) { CharsetRange.new(c
|
54
|
-
rule(charset: simple(:c)) { CharsetRange.new(c
|
52
|
+
class AcceptCharsetHeaderTransform < HeaderTransform
|
53
|
+
rule(charset: simple(:c), q: simple(:q)) { CharsetRange.new(c, q) }
|
54
|
+
rule(charset: simple(:c)) { CharsetRange.new(c) }
|
55
55
|
rule(accept_charset: sequence(:cr)) { Headers::AcceptCharset.new(*cr) }
|
56
56
|
rule(accept_charset: simple(:cr)) { Headers::AcceptCharset.new(cr) }
|
57
57
|
end
|
@@ -41,20 +41,20 @@ module Xenon
|
|
41
41
|
|
42
42
|
module Parsers
|
43
43
|
class AcceptEncodingHeader < Parslet::Parser
|
44
|
-
include
|
44
|
+
include HeaderRules
|
45
45
|
%w(identity compress x-compress deflate gzip x-gzip).each do |c|
|
46
46
|
rule(c.tr('-', '_').to_sym) { str(c).as(:coding) >> sp? }
|
47
47
|
end
|
48
48
|
rule(:coding) { compress | x_compress | deflate | gzip | x_gzip }
|
49
49
|
rule(:wildcard) { str('*').as(:coding) >> sp? }
|
50
50
|
rule(:coding_range) { (coding | identity | wildcard) >> weight.maybe }
|
51
|
-
rule(:accept_encoding) { (coding_range >> (
|
51
|
+
rule(:accept_encoding) { (coding_range >> (list_sep >> coding_range).repeat).maybe.as(:accept_encoding) }
|
52
52
|
root(:accept_encoding)
|
53
53
|
end
|
54
54
|
|
55
|
-
class AcceptEncodingHeaderTransform <
|
56
|
-
rule(coding: simple(:c), q: simple(:q)) { ContentCodingRange.new(c
|
57
|
-
rule(coding: simple(:c)) { ContentCodingRange.new(c
|
55
|
+
class AcceptEncodingHeaderTransform < HeaderTransform
|
56
|
+
rule(coding: simple(:c), q: simple(:q)) { ContentCodingRange.new(c, q) }
|
57
|
+
rule(coding: simple(:c)) { ContentCodingRange.new(c) }
|
58
58
|
rule(accept_encoding: sequence(:er)) { Headers::AcceptEncoding.new(*er) }
|
59
59
|
rule(accept_encoding: simple(:cc)) { Headers::AcceptEncoding.new(cc) }
|
60
60
|
rule(accept_encoding: nil) { Headers::AcceptEncoding.new }
|
@@ -41,17 +41,17 @@ module Xenon
|
|
41
41
|
|
42
42
|
module Parsers
|
43
43
|
class AcceptLanguageHeader < Parslet::Parser
|
44
|
-
include
|
44
|
+
include HeaderRules
|
45
45
|
rule(:language) { (alpha.repeat(1, 8) >> (str('-') >> alphanum.repeat(1, 8)).maybe).as(:language) >> sp? }
|
46
46
|
rule(:wildcard) { str('*').as(:language) >> sp? }
|
47
47
|
rule(:language_range) { (language | wildcard) >> weight.maybe }
|
48
|
-
rule(:accept_language) { (language_range >> (
|
48
|
+
rule(:accept_language) { (language_range >> (list_sep >> language_range).repeat).as(:accept_language) }
|
49
49
|
root(:accept_language)
|
50
50
|
end
|
51
51
|
|
52
|
-
class AcceptLanguageHeaderTransform <
|
53
|
-
rule(language: simple(:e), q: simple(:q)) { LanguageRange.new(e
|
54
|
-
rule(language: simple(:e)) { LanguageRange.new(e
|
52
|
+
class AcceptLanguageHeaderTransform < HeaderTransform
|
53
|
+
rule(language: simple(:e), q: simple(:q)) { LanguageRange.new(e, q) }
|
54
|
+
rule(language: simple(:e)) { LanguageRange.new(e) }
|
55
55
|
rule(accept_language: sequence(:lr)) { Headers::AcceptLanguage.new(*lr) }
|
56
56
|
rule(accept_language: simple(:lr)) { Headers::AcceptLanguage.new(lr) }
|
57
57
|
end
|
@@ -1,51 +1,9 @@
|
|
1
1
|
require 'base64'
|
2
|
+
require 'xenon/auth'
|
2
3
|
require 'xenon/headers'
|
3
4
|
require 'xenon/parsers/header_rules'
|
4
|
-
require 'xenon/quoted_string'
|
5
5
|
|
6
6
|
module Xenon
|
7
|
-
class BasicCredentials
|
8
|
-
attr_reader :username, :password
|
9
|
-
|
10
|
-
def initialize(username, password)
|
11
|
-
@username = username
|
12
|
-
@password = password
|
13
|
-
end
|
14
|
-
|
15
|
-
def token
|
16
|
-
Base64.strict_encode64("#{@username}:#{@password}")
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.decode(s)
|
20
|
-
str = Base64.strict_decode64(s)
|
21
|
-
username, password = str.split(':', 2)
|
22
|
-
BasicCredentials.new(username, password)
|
23
|
-
end
|
24
|
-
|
25
|
-
def to_s
|
26
|
-
"Basic #{token}"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
class GenericCredentials
|
31
|
-
using QuotedString
|
32
|
-
|
33
|
-
attr_reader :scheme, :token, :params
|
34
|
-
|
35
|
-
def initialize(scheme, token: nil, params: {})
|
36
|
-
@scheme = scheme
|
37
|
-
@token = token
|
38
|
-
@params = params
|
39
|
-
end
|
40
|
-
|
41
|
-
def to_s
|
42
|
-
s = @scheme.dup
|
43
|
-
s << ' ' << @token if @token
|
44
|
-
s << ' ' << @params.map { |n, v| "#{n}=#{v.quote}" }.join(', ')
|
45
|
-
s
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
7
|
class Headers
|
50
8
|
# http://tools.ietf.org/html/rfc7235#section-4.2
|
51
9
|
class Authorization < Header 'Authorization'
|
@@ -58,6 +16,8 @@ module Xenon
|
|
58
16
|
def self.parse(s)
|
59
17
|
tree = Parsers::AuthorizationHeader.new.parse(s)
|
60
18
|
Parsers::AuthorizationHeaderTransform.new.apply(tree)
|
19
|
+
rescue Parslet::ParseFailed
|
20
|
+
raise Xenon::ParseError.new("Invalid Authorization header (#{s}).")
|
61
21
|
end
|
62
22
|
|
63
23
|
def to_s
|
@@ -68,25 +28,19 @@ module Xenon
|
|
68
28
|
|
69
29
|
module Parsers
|
70
30
|
class AuthorizationHeader < Parslet::Parser
|
71
|
-
include
|
72
|
-
rule(:token68) { ((alpha | digit | match(/[\-\._~\+\/]/)) >> str('=').repeat).repeat(1).as(:token) }
|
73
|
-
rule(:auth_scheme) { token.as(:auth_scheme) }
|
74
|
-
rule(:name) { token.as(:name) }
|
75
|
-
rule(:value) { token.as(:value) }
|
76
|
-
rule(:auth_param) { (name >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:auth_param) }
|
77
|
-
rule(:auth_params) { (auth_param.maybe >> (ows >> comma >> ows >> auth_param).repeat).as(:auth_params) }
|
31
|
+
include AuthHeaderRules
|
78
32
|
rule(:credentials) { auth_scheme >> sp >> (token68 | auth_params) }
|
79
33
|
rule(:authorization) { credentials.as(:authorization) }
|
80
34
|
root(:authorization)
|
81
35
|
end
|
82
36
|
|
83
|
-
class AuthorizationHeaderTransform <
|
37
|
+
class AuthorizationHeaderTransform < HeaderTransform
|
84
38
|
rule(auth_param: { name: simple(:n), value: simple(:v) }) { [n, v] }
|
85
39
|
rule(auth_params: subtree(:x)) { { foo: x } }
|
86
|
-
rule(auth_scheme: simple(:s), token: simple(:t)) {
|
40
|
+
rule(auth_scheme: simple(:s), token: simple(:t)) {
|
87
41
|
case s
|
88
42
|
when 'Basic' then BasicCredentials.decode(t)
|
89
|
-
else GenericCredentials.new(s, token: t)
|
43
|
+
else GenericCredentials.new(s, token: t)
|
90
44
|
end
|
91
45
|
}
|
92
46
|
rule(auth_scheme: simple(:s), auth_params: subtree(:p)) { GenericCredentials.new(s, params: Hash[p]) }
|
@@ -38,15 +38,15 @@ module Xenon
|
|
38
38
|
|
39
39
|
module Parsers
|
40
40
|
class CacheControlHeader < Parslet::Parser
|
41
|
-
include
|
41
|
+
include HeaderRules
|
42
42
|
rule(:name) { token.as(:name) }
|
43
43
|
rule(:value) { str('=') >> (token | quoted_string).as(:value) }
|
44
44
|
rule(:directive) { (name >> value.maybe).as(:directive) >> sp? }
|
45
|
-
rule(:cache_control) { (directive >> (
|
45
|
+
rule(:cache_control) { (directive >> (list_sep >> directive).repeat).as(:cache_control) }
|
46
46
|
root(:cache_control)
|
47
47
|
end
|
48
48
|
|
49
|
-
class CacheControlHeaderTransform <
|
49
|
+
class CacheControlHeaderTransform < HeaderTransform
|
50
50
|
rule(directive: { name: simple(:n), value: simple(:v) }) { CacheDirective.new(n, v) }
|
51
51
|
rule(directive: { name: simple(:n) }) { CacheDirective.new(n) }
|
52
52
|
rule(cache_control: sequence(:d)) { Headers::CacheControl.new(*d) }
|
@@ -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
|