xenon 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|