xenon 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/media_type'
3
+
4
+ module Xenon
5
+ class Headers
6
+ # http://tools.ietf.org/html/rfc7231#section-5.3.2
7
+ class Accept < ListHeader 'Accept'
8
+ def initialize(*media_ranges)
9
+ super(media_ranges.sort_by.with_index { |mr, i| [mr, -i] }.reverse!)
10
+ end
11
+
12
+ alias_method :media_ranges, :values
13
+
14
+ def self.parse(s)
15
+ tree = Parsers::AcceptHeader.new.parse(s)
16
+ Parsers::AcceptHeaderTransform.new.apply(tree)
17
+ end
18
+ end
19
+ end
20
+
21
+ module Parsers
22
+ class AcceptHeader < Parslet::Parser
23
+ include MediaTypeRules
24
+ rule(:accept) { (media_range >> (comma >> media_range).repeat).as(:accept) }
25
+ root(:accept)
26
+ end
27
+
28
+ class AcceptHeaderTransform < MediaTypeTransform
29
+ rule(accept: sequence(:mr)) { Headers::Accept.new(*mr) }
30
+ rule(accept: simple(:mr)) { Headers::Accept.new(mr) }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+
4
+ module Xenon
5
+ class CharsetRange
6
+ attr_reader :charset, :q
7
+
8
+ DEFAULT_Q = 1.0
9
+
10
+ def initialize(charset, q = DEFAULT_Q)
11
+ @charset = charset
12
+ @q = Float(q) || DEFAULT_Q
13
+ end
14
+
15
+ def <=>(other)
16
+ @q <=> other.q
17
+ end
18
+
19
+ def to_s
20
+ s = @charset.dup
21
+ s << "; q=#{@q}" if @q != DEFAULT_Q
22
+ s
23
+ end
24
+ end
25
+
26
+ class Headers
27
+ # http://tools.ietf.org/html/rfc7231#section-5.3.3
28
+ class AcceptCharset < ListHeader 'Accept-Charset'
29
+ def initialize(*charset_ranges)
30
+ super(charset_ranges.sort_by.with_index { |mr, i| [mr, -i] }.reverse!)
31
+ end
32
+
33
+ alias_method :charset_ranges, :values
34
+
35
+ def self.parse(s)
36
+ tree = Parsers::AcceptCharsetHeader.new.parse(s)
37
+ Parsers::AcceptCharsetHeaderTransform.new.apply(tree)
38
+ end
39
+ end
40
+ end
41
+
42
+ module Parsers
43
+ class AcceptCharsetHeader < Parslet::Parser
44
+ include BasicRules, WeightRules
45
+ rule(:charset) { token.as(:charset) >> sp? }
46
+ rule(:wildcard) { str('*').as(:charset) >> sp? }
47
+ rule(:charset_range) { (charset | wildcard) >> weight.maybe }
48
+ rule(:accept_charset) { (charset_range >> (comma >> charset_range).repeat).as(:accept_charset) }
49
+ root(:accept_charset)
50
+ end
51
+
52
+ class AcceptCharsetHeaderTransform < Parslet::Transform
53
+ rule(charset: simple(:c), q: simple(:q)) { CharsetRange.new(c.str, q.str) }
54
+ rule(charset: simple(:c)) { CharsetRange.new(c.str) }
55
+ rule(accept_charset: sequence(:cr)) { Headers::AcceptCharset.new(*cr) }
56
+ rule(accept_charset: simple(:cr)) { Headers::AcceptCharset.new(cr) }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,63 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+
4
+ module Xenon
5
+ class ContentCodingRange
6
+ attr_reader :coding, :q
7
+
8
+ DEFAULT_Q = 1.0
9
+
10
+ def initialize(coding, q = DEFAULT_Q)
11
+ @coding = coding
12
+ @q = Float(q) || DEFAULT_Q
13
+ end
14
+
15
+ def <=>(other)
16
+ @q <=> other.q
17
+ end
18
+
19
+ def to_s
20
+ s = @coding.dup
21
+ s << "; q=#{@q}" if @q != DEFAULT_Q
22
+ s
23
+ end
24
+ end
25
+
26
+ class Headers
27
+ # http://tools.ietf.org/html/rfc7231#section-5.3.4
28
+ class AcceptEncoding < ListHeader 'Accept-Encoding'
29
+ def initialize(*coding_ranges)
30
+ super(coding_ranges.sort_by.with_index { |mr, i| [mr, -i] }.reverse!)
31
+ end
32
+
33
+ alias_method :coding_ranges, :values
34
+
35
+ def self.parse(s)
36
+ tree = Parsers::AcceptEncodingHeader.new.parse(s)
37
+ Parsers::AcceptEncodingHeaderTransform.new.apply(tree)
38
+ end
39
+ end
40
+ end
41
+
42
+ module Parsers
43
+ class AcceptEncodingHeader < Parslet::Parser
44
+ include BasicRules, WeightRules
45
+ %w(identity compress x-compress deflate gzip x-gzip).each do |c|
46
+ rule(c.tr('-', '_').to_sym) { str(c).as(:coding) >> sp? }
47
+ end
48
+ rule(:coding) { compress | x_compress | deflate | gzip | x_gzip }
49
+ rule(:wildcard) { str('*').as(:coding) >> sp? }
50
+ rule(:coding_range) { (coding | identity | wildcard) >> weight.maybe }
51
+ rule(:accept_encoding) { (coding_range >> (comma >> coding_range).repeat).maybe.as(:accept_encoding) }
52
+ root(:accept_encoding)
53
+ end
54
+
55
+ class AcceptEncodingHeaderTransform < Parslet::Transform
56
+ rule(coding: simple(:c), q: simple(:q)) { ContentCodingRange.new(c.str, q.str) }
57
+ rule(coding: simple(:c)) { ContentCodingRange.new(c.str) }
58
+ rule(accept_encoding: sequence(:er)) { Headers::AcceptEncoding.new(*er) }
59
+ rule(accept_encoding: simple(:cc)) { Headers::AcceptEncoding.new(cc) }
60
+ rule(accept_encoding: nil) { Headers::AcceptEncoding.new }
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,59 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+
4
+ module Xenon
5
+ class LanguageRange
6
+ attr_reader :language, :q
7
+
8
+ DEFAULT_Q = 1.0
9
+
10
+ def initialize(language, q = DEFAULT_Q)
11
+ @language = language
12
+ @q = Float(q) || DEFAULT_Q
13
+ end
14
+
15
+ def <=>(other)
16
+ @q <=> other.q
17
+ end
18
+
19
+ def to_s
20
+ s = @language.dup
21
+ s << "; q=#{@q}" if @q != DEFAULT_Q
22
+ s
23
+ end
24
+ end
25
+
26
+ class Headers
27
+ # http://tools.ietf.org/html/rfc7231#section-5.3.5
28
+ class AcceptLanguage < ListHeader 'Accept-Language'
29
+ def initialize(*language_ranges)
30
+ super(language_ranges.sort_by.with_index { |mr, i| [mr, -i] }.reverse!)
31
+ end
32
+
33
+ alias_method :language_ranges, :values
34
+
35
+ def self.parse(s)
36
+ tree = Parsers::AcceptLanguageHeader.new.parse(s)
37
+ Parsers::AcceptLanguageHeaderTransform.new.apply(tree)
38
+ end
39
+ end
40
+ end
41
+
42
+ module Parsers
43
+ class AcceptLanguageHeader < Parslet::Parser
44
+ include BasicRules, WeightRules
45
+ rule(:language) { (alpha.repeat(1, 8) >> (str('-') >> alphanum.repeat(1, 8)).maybe).as(:language) >> sp? }
46
+ rule(:wildcard) { str('*').as(:language) >> sp? }
47
+ rule(:language_range) { (language | wildcard) >> weight.maybe }
48
+ rule(:accept_language) { (language_range >> (comma >> language_range).repeat).as(:accept_language) }
49
+ root(:accept_language)
50
+ end
51
+
52
+ class AcceptLanguageHeaderTransform < Parslet::Transform
53
+ rule(language: simple(:e), q: simple(:q)) { LanguageRange.new(e.str, q.str) }
54
+ rule(language: simple(:e)) { LanguageRange.new(e.str) }
55
+ rule(accept_language: sequence(:lr)) { Headers::AcceptLanguage.new(*lr) }
56
+ rule(accept_language: simple(:lr)) { Headers::AcceptLanguage.new(lr) }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,96 @@
1
+ require 'base64'
2
+ require 'xenon/headers'
3
+ require 'xenon/parsers/header_rules'
4
+ require 'xenon/quoted_string'
5
+
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
+ class Headers
50
+ # http://tools.ietf.org/html/rfc7235#section-4.2
51
+ class Authorization < Header 'Authorization'
52
+ attr_reader :credentials
53
+
54
+ def initialize(credentials)
55
+ @credentials = credentials
56
+ end
57
+
58
+ def self.parse(s)
59
+ tree = Parsers::AuthorizationHeader.new.parse(s)
60
+ Parsers::AuthorizationHeaderTransform.new.apply(tree)
61
+ end
62
+
63
+ def to_s
64
+ @credentials.to_s
65
+ end
66
+ end
67
+ end
68
+
69
+ module Parsers
70
+ class AuthorizationHeader < Parslet::Parser
71
+ include BasicRules
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) }
78
+ rule(:credentials) { auth_scheme >> sp >> (token68 | auth_params) }
79
+ rule(:authorization) { credentials.as(:authorization) }
80
+ root(:authorization)
81
+ end
82
+
83
+ class AuthorizationHeaderTransform < BasicTransform
84
+ rule(auth_param: { name: simple(:n), value: simple(:v) }) { [n, v] }
85
+ rule(auth_params: subtree(:x)) { { foo: x } }
86
+ rule(auth_scheme: simple(:s), token: simple(:t)) {
87
+ case s
88
+ when 'Basic' then BasicCredentials.decode(t)
89
+ else GenericCredentials.new(s, token: t)
90
+ end
91
+ }
92
+ rule(auth_scheme: simple(:s), auth_params: subtree(:p)) { GenericCredentials.new(s, params: Hash[p]) }
93
+ rule(authorization: simple(:c)) { Headers::Authorization.new(c) }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,56 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+ require 'xenon/quoted_string'
4
+
5
+ module Xenon
6
+ class CacheDirective
7
+ using QuotedString
8
+
9
+ attr_reader :name, :value
10
+
11
+ def initialize(name, value = nil)
12
+ @name = name
13
+ @value = value
14
+ end
15
+
16
+ def to_s
17
+ s = @name.dup
18
+ s << '=' << @value.quote if @value
19
+ s
20
+ end
21
+ end
22
+
23
+ class Headers
24
+ # http://tools.ietf.org/html/rfc7234#section-5.2
25
+ class CacheControl < ListHeader 'Cache-Control'
26
+ def initialize(*directives)
27
+ super(directives)
28
+ end
29
+
30
+ alias_method :directives, :values
31
+
32
+ def self.parse(s)
33
+ tree = Parsers::CacheControlHeader.new.parse(s)
34
+ Parsers::CacheControlHeaderTransform.new.apply(tree)
35
+ end
36
+ end
37
+ end
38
+
39
+ module Parsers
40
+ class CacheControlHeader < Parslet::Parser
41
+ include BasicRules
42
+ rule(:name) { token.as(:name) }
43
+ rule(:value) { str('=') >> (token | quoted_string).as(:value) }
44
+ rule(:directive) { (name >> value.maybe).as(:directive) >> sp? }
45
+ rule(:cache_control) { (directive >> (comma >> directive).repeat).as(:cache_control) }
46
+ root(:cache_control)
47
+ end
48
+
49
+ class CacheControlHeaderTransform < BasicTransform
50
+ rule(directive: { name: simple(:n), value: simple(:v) }) { CacheDirective.new(n, v) }
51
+ rule(directive: { name: simple(:n) }) { CacheDirective.new(n) }
52
+ rule(cache_control: sequence(:d)) { Headers::CacheControl.new(*d) }
53
+ rule(cache_control: simple(:d)) { Headers::CacheControl.new(d) }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/media_type'
3
+
4
+ module Xenon
5
+ class Headers
6
+ # http://tools.ietf.org/html/rfc7231#section-3.1.1.5
7
+ class ContentType < Header 'Content-Type'
8
+ attr_reader :content_type
9
+
10
+ def initialize(content_type)
11
+ @content_type = content_type
12
+ end
13
+
14
+ def self.parse(s)
15
+ ContentType.new(Xenon::ContentType.parse(s))
16
+ end
17
+
18
+ def to_s
19
+ @content_type.to_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,140 @@
1
+ require 'xenon/errors'
2
+ require 'xenon/parsers/media_type'
3
+
4
+ module Xenon
5
+
6
+ class MediaType
7
+ attr_reader :type, :subtype, :params
8
+
9
+ def initialize(type, subtype, params = {})
10
+ @type = type
11
+ @subtype = subtype
12
+ @params = params
13
+ end
14
+
15
+ def self.parse(s)
16
+ tree = Parsers::MediaType.new.parse(s)
17
+ Parsers::MediaTypeTransform.new.apply(tree)
18
+ rescue Parslet::ParseFailed
19
+ raise Xenon::ParseError.new("Invalid media type (#{s})")
20
+ end
21
+
22
+ %w(application audio image message multipart text video).each do |type|
23
+ define_method "#{type}?" do
24
+ @type == type
25
+ end
26
+ end
27
+
28
+ def experimental?
29
+ @subtype.start_with?('x.') # not x- see http://tools.ietf.org/html/rfc6838#section-3.4
30
+ end
31
+
32
+ def personal?
33
+ @subtype.start_with?('prs.')
34
+ end
35
+
36
+ def vendor?
37
+ @subtype.start_with?('vnd.')
38
+ end
39
+
40
+ %w(ber der fastinfoset json wbxml xml zip).each do |format|
41
+ define_method "#{format}?" do
42
+ @subtype == format || @subtype.end_with?("+#{format}")
43
+ end
44
+ end
45
+
46
+ def with_q(q)
47
+ MediaRange.new(self, q)
48
+ end
49
+
50
+ def with_charset(charset)
51
+ ContentType.new(self, charset)
52
+ end
53
+
54
+ def to_s
55
+ "#{@type}/#{@subtype}" << @params.map { |n, v| v ? "; #{n}=#{v}" : "; #{n}" }.join
56
+ end
57
+
58
+ JSON = MediaType.new('application', 'json')
59
+ XML = MediaType.new('application', 'xml')
60
+ end
61
+
62
+ class ContentType
63
+ attr_reader :media_type, :charset
64
+
65
+ DEFAULT_CHARSET = 'utf-8' # historically iso-8859-1 but see http://tools.ietf.org/html/rfc7231#appendix-B
66
+
67
+ def initialize(media_type, charset = DEFAULT_CHARSET)
68
+ @media_type = media_type
69
+ @charset = charset
70
+ end
71
+
72
+ def self.parse(s)
73
+ media_type = MediaType.parse(s)
74
+ charset = media_type.params.delete('charset') || DEFAULT_CHARSET
75
+ ContentType.new(media_type, charset)
76
+ end
77
+
78
+ def to_s
79
+ "#{@media_type}; charset=#{@charset}"
80
+ end
81
+ end
82
+
83
+ class MediaRange
84
+ include Comparable
85
+
86
+ DEFAULT_Q = 1.0
87
+
88
+ attr_reader :type, :subtype, :q, :params
89
+
90
+ def initialize(type, subtype, params = {})
91
+ @type = type
92
+ @subtype = subtype
93
+ @q = Float(params.delete('q')) rescue DEFAULT_Q
94
+ @params = params
95
+ end
96
+
97
+ def self.parse(s)
98
+ tree = Parsers::MediaRange.new.parse(s)
99
+ Parsers::MediaTypeTransform.new.apply(tree)
100
+ rescue Parslet::ParseFailed
101
+ raise Xenon::ParseError.new("Invalid media range (#{s})")
102
+ end
103
+
104
+ def <=>(other)
105
+ dt = compare_types(@type, other.type)
106
+ return dt if dt != 0
107
+ ds = compare_types(@subtype, other.subtype)
108
+ return ds if ds != 0
109
+ dp = params.size <=> other.params.size
110
+ return dp if dp != 0
111
+ @q <=> other.q
112
+ end
113
+
114
+ def =~(media_type)
115
+ (type == '*' || type == media_type.type) &&
116
+ (subtype == '*' || subtype == media_type.subtype) &&
117
+ params.all? { |n, v| media_type.params[n] == v }
118
+ end
119
+
120
+ alias_method :===, :=~
121
+
122
+ def to_s
123
+ s = "#{@type}/#{@subtype}"
124
+ s << @params.map { |n, v| v ? "; #{n}=#{v}" : "; #{n}" }.join
125
+ s << "; q=#{@q}" if @q != DEFAULT_Q
126
+ s
127
+ end
128
+
129
+ private
130
+
131
+ def compare_types(a, b)
132
+ if a == b then 0
133
+ elsif a == '*' then -1
134
+ elsif b == '*' then 1
135
+ else 0
136
+ end
137
+ end
138
+ end
139
+
140
+ end