xenon 0.0.1 → 0.0.2

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.
@@ -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