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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +18 -0
  3. data/.gitignore +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +10 -0
  6. data/Guardfile +0 -32
  7. data/README.md +59 -5
  8. data/examples/hello_world/config.ru +3 -0
  9. data/examples/hello_world/hello_world.rb +17 -0
  10. data/lib/xenon.rb +62 -49
  11. data/lib/xenon/auth.rb +63 -0
  12. data/lib/xenon/errors.rb +1 -0
  13. data/lib/xenon/etag.rb +48 -0
  14. data/lib/xenon/headers.rb +2 -4
  15. data/lib/xenon/headers/accept.rb +3 -2
  16. data/lib/xenon/headers/accept_charset.rb +5 -5
  17. data/lib/xenon/headers/accept_encoding.rb +5 -5
  18. data/lib/xenon/headers/accept_language.rb +5 -5
  19. data/lib/xenon/headers/authorization.rb +7 -53
  20. data/lib/xenon/headers/cache_control.rb +3 -3
  21. data/lib/xenon/headers/content_type.rb +1 -1
  22. data/lib/xenon/headers/if_match.rb +53 -0
  23. data/lib/xenon/headers/if_modified_since.rb +22 -0
  24. data/lib/xenon/headers/if_none_match.rb +53 -0
  25. data/lib/xenon/headers/if_range.rb +45 -0
  26. data/lib/xenon/headers/if_unmodified_since.rb +22 -0
  27. data/lib/xenon/headers/user_agent.rb +65 -0
  28. data/lib/xenon/headers/www_authenticate.rb +70 -0
  29. data/lib/xenon/media_type.rb +24 -2
  30. data/lib/xenon/parsers/basic_rules.rb +38 -7
  31. data/lib/xenon/parsers/header_rules.rb +49 -3
  32. data/lib/xenon/parsers/media_type.rb +4 -3
  33. data/lib/xenon/quoted_string.rb +11 -1
  34. data/lib/xenon/routing/directives.rb +14 -0
  35. data/lib/xenon/routing/header_directives.rb +32 -0
  36. data/lib/xenon/routing/method_directives.rb +26 -0
  37. data/lib/xenon/routing/param_directives.rb +22 -0
  38. data/lib/xenon/routing/path_directives.rb +37 -0
  39. data/lib/xenon/routing/route_directives.rb +51 -0
  40. data/lib/xenon/routing/security_directives.rb +20 -0
  41. data/lib/xenon/version.rb +1 -1
  42. data/spec/spec_helper.rb +3 -0
  43. data/spec/xenon/etag_spec.rb +19 -0
  44. data/spec/xenon/headers/if_match_spec.rb +73 -0
  45. data/spec/xenon/headers/if_modified_since_spec.rb +19 -0
  46. data/spec/xenon/headers/if_none_match_spec.rb +79 -0
  47. data/spec/xenon/headers/if_range_spec.rb +45 -0
  48. data/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
  49. data/spec/xenon/headers/user_agent_spec.rb +67 -0
  50. data/spec/xenon/headers/www_authenticate_spec.rb +43 -0
  51. data/xenon.gemspec +4 -3
  52. metadata +60 -10
  53. data/lib/xenon/routing.rb +0 -133
@@ -1,4 +1,5 @@
1
1
  module Xenon
2
2
  class Error < StandardError; end
3
3
  class ParseError < Error; end
4
+ class ProtocolError < Error; end
4
5
  end
@@ -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
@@ -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 }
@@ -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 >> (comma >> media_range).repeat).as(:accept) }
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 BasicRules, WeightRules
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 >> (comma >> charset_range).repeat).as(:accept_charset) }
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 < 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) }
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 BasicRules, WeightRules
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 >> (comma >> coding_range).repeat).maybe.as(:accept_encoding) }
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 < 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) }
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 BasicRules, WeightRules
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 >> (comma >> language_range).repeat).as(:accept_language) }
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 < 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) }
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 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) }
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 < BasicTransform
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 BasicRules
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 >> (comma >> directive).repeat).as(:cache_control) }
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 < BasicTransform
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) }
@@ -12,7 +12,7 @@ module Xenon
12
12
  end
13
13
 
14
14
  def self.parse(s)
15
- ContentType.new(Xenon::ContentType.parse(s))
15
+ new(Xenon::ContentType.parse(s))
16
16
  end
17
17
 
18
18
  def to_s
@@ -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