xenon-routing 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +18 -0
  3. data/.gitignore +25 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +20 -0
  7. data/Guardfile +16 -0
  8. data/LICENSE +22 -0
  9. data/README.md +116 -0
  10. data/Rakefile +40 -0
  11. data/VERSION +1 -0
  12. data/examples/hello_world/config.ru +3 -0
  13. data/examples/hello_world/hello_world.rb +27 -0
  14. data/xenon-http/lib/xenon/auth.rb +63 -0
  15. data/xenon-http/lib/xenon/errors.rb +5 -0
  16. data/xenon-http/lib/xenon/etag.rb +48 -0
  17. data/xenon-http/lib/xenon/headers.rb +112 -0
  18. data/xenon-http/lib/xenon/headers/accept.rb +34 -0
  19. data/xenon-http/lib/xenon/headers/accept_charset.rb +59 -0
  20. data/xenon-http/lib/xenon/headers/accept_encoding.rb +63 -0
  21. data/xenon-http/lib/xenon/headers/accept_language.rb +59 -0
  22. data/xenon-http/lib/xenon/headers/authorization.rb +50 -0
  23. data/xenon-http/lib/xenon/headers/cache_control.rb +56 -0
  24. data/xenon-http/lib/xenon/headers/content_type.rb +23 -0
  25. data/xenon-http/lib/xenon/headers/if_match.rb +53 -0
  26. data/xenon-http/lib/xenon/headers/if_modified_since.rb +22 -0
  27. data/xenon-http/lib/xenon/headers/if_none_match.rb +53 -0
  28. data/xenon-http/lib/xenon/headers/if_range.rb +45 -0
  29. data/xenon-http/lib/xenon/headers/if_unmodified_since.rb +22 -0
  30. data/xenon-http/lib/xenon/headers/user_agent.rb +65 -0
  31. data/xenon-http/lib/xenon/headers/www_authenticate.rb +71 -0
  32. data/xenon-http/lib/xenon/http.rb +7 -0
  33. data/xenon-http/lib/xenon/http_version.rb +3 -0
  34. data/xenon-http/lib/xenon/media_type.rb +162 -0
  35. data/xenon-http/lib/xenon/parsers/basic_rules.rb +86 -0
  36. data/xenon-http/lib/xenon/parsers/header_rules.rb +60 -0
  37. data/xenon-http/lib/xenon/parsers/media_type.rb +53 -0
  38. data/xenon-http/lib/xenon/quoted_string.rb +20 -0
  39. data/xenon-http/spec/spec_helper.rb +94 -0
  40. data/xenon-http/spec/xenon/etag_spec.rb +19 -0
  41. data/xenon-http/spec/xenon/headers/accept_charset_spec.rb +31 -0
  42. data/xenon-http/spec/xenon/headers/accept_encoding_spec.rb +40 -0
  43. data/xenon-http/spec/xenon/headers/accept_language_spec.rb +33 -0
  44. data/xenon-http/spec/xenon/headers/accept_spec.rb +54 -0
  45. data/xenon-http/spec/xenon/headers/authorization_spec.rb +47 -0
  46. data/xenon-http/spec/xenon/headers/cache_control_spec.rb +64 -0
  47. data/xenon-http/spec/xenon/headers/if_match_spec.rb +73 -0
  48. data/xenon-http/spec/xenon/headers/if_modified_since_spec.rb +19 -0
  49. data/xenon-http/spec/xenon/headers/if_none_match_spec.rb +79 -0
  50. data/xenon-http/spec/xenon/headers/if_range_spec.rb +45 -0
  51. data/xenon-http/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
  52. data/xenon-http/spec/xenon/headers/user_agent_spec.rb +67 -0
  53. data/xenon-http/spec/xenon/headers/www_authenticate_spec.rb +43 -0
  54. data/xenon-http/spec/xenon/media_type_spec.rb +267 -0
  55. data/xenon-http/xenon-http.gemspec +25 -0
  56. data/xenon-routing/lib/xenon/api.rb +118 -0
  57. data/xenon-routing/lib/xenon/marshallers.rb +48 -0
  58. data/xenon-routing/lib/xenon/request.rb +40 -0
  59. data/xenon-routing/lib/xenon/response.rb +29 -0
  60. data/xenon-routing/lib/xenon/routing.rb +6 -0
  61. data/xenon-routing/lib/xenon/routing/context.rb +35 -0
  62. data/xenon-routing/lib/xenon/routing/directives.rb +14 -0
  63. data/xenon-routing/lib/xenon/routing/header_directives.rb +32 -0
  64. data/xenon-routing/lib/xenon/routing/method_directives.rb +26 -0
  65. data/xenon-routing/lib/xenon/routing/param_directives.rb +22 -0
  66. data/xenon-routing/lib/xenon/routing/path_directives.rb +37 -0
  67. data/xenon-routing/lib/xenon/routing/route_directives.rb +51 -0
  68. data/xenon-routing/lib/xenon/routing/security_directives.rb +34 -0
  69. data/xenon-routing/lib/xenon/routing_version.rb +3 -0
  70. data/xenon-routing/spec/spec_helper.rb +94 -0
  71. data/xenon-routing/xenon-routing.gemspec +25 -0
  72. data/xenon.gemspec +26 -0
  73. metadata +145 -0
@@ -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
@@ -0,0 +1,22 @@
1
+ require 'xenon/headers'
2
+
3
+ module Xenon
4
+ class Headers
5
+ # http://tools.ietf.org/html/rfc7232#section-3.4
6
+ class IfUnmodifiedSince < Header 'If-Unmodified-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,65 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+
4
+ module Xenon
5
+ class Product
6
+ attr_reader :name, :version, :comment
7
+
8
+ def initialize(name, version = nil, comment = nil)
9
+ @name = name
10
+ @version = version
11
+ @comment = comment
12
+ end
13
+
14
+ def to_s
15
+ s = ''
16
+ s << @name if @name
17
+ s << '/' << @version if @version
18
+ if @comment
19
+ s << ' ' unless s.empty?
20
+ s << '(' << @comment << ')'
21
+ end
22
+ s
23
+ end
24
+ end
25
+
26
+ class Headers
27
+ # http://tools.ietf.org/html/rfc7231#section-5.5.3
28
+ class UserAgent < Header 'User-Agent'
29
+ attr_reader :products
30
+
31
+ def initialize(*products)
32
+ @products = products
33
+ end
34
+
35
+ def self.parse(s)
36
+ tree = Parsers::UserAgentHeader.new.parse(s)
37
+ Parsers::UserAgentHeaderTransform.new.apply(tree)
38
+ end
39
+
40
+ def to_s
41
+ @products.map(&:to_s).join(' ')
42
+ end
43
+ end
44
+ end
45
+
46
+ module Parsers
47
+ class UserAgentHeader < Parslet::Parser
48
+ include HeaderRules
49
+ rule(:product) { (token.as(:name) >> (str('/') >> token.as(:version)).maybe >> (rws >> comment.as(:comment)).maybe).as(:product) }
50
+ rule(:product_comment) { comment.as(:product_comment) }
51
+ rule(:user_agent) { (product >> (rws >> (product | product_comment)).repeat).as(:user_agent) }
52
+ root(:user_agent)
53
+ end
54
+
55
+ class UserAgentHeaderTransform < HeaderTransform
56
+ rule(product: { name: simple(:p), version: simple(:v), comment: simple(:c) }) { Product.new(p, v, c) }
57
+ rule(product: { name: simple(:p), version: simple(:v) }) { Product.new(p, v) }
58
+ rule(product: { name: simple(:p), comment: simple(:c) }) { Product.new(p, nil, c) }
59
+ rule(product: { name: simple(:p) }) { Product.new(p) }
60
+ rule(product_comment: simple(:c)) { Product.new(nil, nil, c) }
61
+ rule(user_agent: sequence(:p)) { Headers::UserAgent.new(*p) }
62
+ rule(user_agent: simple(:p)) { Headers::UserAgent.new(p) }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,71 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'forwardable'
3
+ require 'xenon/headers'
4
+ require 'xenon/parsers/header_rules'
5
+ require 'xenon/quoted_string'
6
+
7
+ module Xenon
8
+ class Headers
9
+ class Challenge
10
+ extend Forwardable
11
+ using QuotedString
12
+
13
+ attr_reader :auth_scheme
14
+ def_delegators :@params, :key?, :include?, :[]
15
+
16
+ def initialize(auth_scheme, params = {})
17
+ @auth_scheme = auth_scheme
18
+ @params = params.with_indifferent_access
19
+ end
20
+
21
+ def method_missing(name, *args, &block)
22
+ name = name.to_sym
23
+ @params.key?(name) ? @params[name] : super
24
+ end
25
+
26
+ def respond_to_missing?(name, include_all)
27
+ @params.key?(name.to_sym) || super
28
+ end
29
+
30
+ def to_s
31
+ param_string = @params.map { |k, v| "#{k}=#{v.quote}"}.join(', ')
32
+ "#{@auth_scheme} #{param_string}"
33
+ end
34
+ end
35
+
36
+ # https://tools.ietf.org/html/rfc7235#section-4.1
37
+ class WWWAuthenticate < ListHeader 'WWW-Authenticate'
38
+ def initialize(*challenges)
39
+ super(challenges)
40
+ end
41
+
42
+ alias_method :challenges, :values
43
+
44
+ def self.parse(s)
45
+ tree = Parsers::WWWAuthenticateHeader.new.parse(s)
46
+ Parsers::WWWAuthenticateHeaderTransform.new.apply(tree)
47
+ end
48
+
49
+ def to_s
50
+ challenges.map(&:to_s).join(', ')
51
+ end
52
+ end
53
+ end
54
+
55
+ module Parsers
56
+ class WWWAuthenticateHeader < Parslet::Parser
57
+ include AuthHeaderRules
58
+ rule(:challenge) { (auth_scheme >> sp >> (auth_params | token68)).as(:challenge) }
59
+ rule(:www_authenticate) { (challenge >> (comma >> challenge).repeat).as(:www_authenticate) }
60
+ root(:www_authenticate)
61
+ end
62
+
63
+ class WWWAuthenticateHeaderTransform < HeaderTransform
64
+ rule(auth_param: { name: simple(:n), value: simple(:v) }) { Tuple.new(n, v) }
65
+ rule(challenge: { auth_scheme: simple(:s), auth_params: simple(:p) }) { Headers::Challenge.new(s, Hash[*p.to_a]) }
66
+ rule(challenge: { auth_scheme: simple(:s), auth_params: sequence(:p) }) { Headers::Challenge.new(s, Hash[p.map(&:to_a)]) }
67
+ rule(www_authenticate: simple(:c)) { Headers::WWWAuthenticate.new(c) }
68
+ rule(www_authenticate: sequence(:c)) { Headers::WWWAuthenticate.new(*c) }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,7 @@
1
+ require 'xenon/http_version'
2
+ require 'xenon/auth'
3
+ require 'xenon/errors'
4
+ require 'xenon/etag'
5
+ require 'xenon/headers'
6
+ require 'xenon/media_type'
7
+ require 'xenon/quoted_string'
@@ -0,0 +1,3 @@
1
+ module Xenon
2
+ HTTP_VERSION = File.read(File.join(__dir__, '..', '..', '..', 'VERSION'))
3
+ end
@@ -0,0 +1,162 @@
1
+ require 'xenon/errors'
2
+ require 'xenon/parsers/media_type'
3
+
4
+ module Xenon
5
+
6
+ # A media type.
7
+ #
8
+ # @see ContentType
9
+ # @see MediaRange
10
+ class MediaType
11
+ attr_reader :type, :subtype, :params
12
+
13
+ # Initializes a new instance of MediaType.
14
+ #
15
+ # @param type [String] The main type, e.g. 'application'.
16
+ # @param subtype [String] The subtype, e.g. 'json'.
17
+ # @param params [Hash] Any params for the media type; don't use 'q' or 'charset'.
18
+ def initialize(type, subtype, params = {})
19
+ @type = type
20
+ @subtype = subtype
21
+ @params = params
22
+ end
23
+
24
+ # Parses a media type.
25
+ #
26
+ # @param s [String] The media type string.
27
+ # @return [MediaType] The media type.
28
+ def self.parse(s)
29
+ tree = Parsers::MediaType.new.parse(s)
30
+ Parsers::MediaTypeTransform.new.apply(tree)
31
+ rescue Parslet::ParseFailed
32
+ raise Xenon::ParseError.new("Invalid media type (#{s}).")
33
+ end
34
+
35
+ %w(application audio image message multipart text video).each do |type|
36
+ define_method "#{type}?" do
37
+ @type == type
38
+ end
39
+ end
40
+
41
+ def experimental?
42
+ @subtype.start_with?('x.') # not x- see http://tools.ietf.org/html/rfc6838#section-3.4
43
+ end
44
+
45
+ def personal?
46
+ @subtype.start_with?('prs.')
47
+ end
48
+
49
+ def vendor?
50
+ @subtype.start_with?('vnd.')
51
+ end
52
+
53
+ %w(ber der fastinfoset json wbxml xml zip).each do |format|
54
+ define_method "#{format}?" do
55
+ @subtype == format || @subtype.end_with?("+#{format}")
56
+ end
57
+ end
58
+
59
+ # Creates a {MediaRange} using this media type with a quality factor.
60
+ #
61
+ # @param q [Numeric] A value between 1.0 (most desirable) and 0.0 (not acceptable).
62
+ # @return [MediaRange] The media range.
63
+ def with_q(q)
64
+ MediaRange.new(self, q)
65
+ end
66
+
67
+ # Creates a {ContentType} using this media type with a charset.
68
+ #
69
+ # @param charset [String] The desired charset, e.g. 'utf-8'.
70
+ # @return [ContentType] The content type.
71
+ def with_charset(charset)
72
+ ContentType.new(self, charset)
73
+ end
74
+
75
+ def to_s
76
+ "#{@type}/#{@subtype}" << @params.map { |n, v| v ? "; #{n}=#{v}" : "; #{n}" }.join
77
+ end
78
+
79
+ JSON = MediaType.new('application', 'json')
80
+ XML = MediaType.new('application', 'xml')
81
+ end
82
+
83
+ # A content type.
84
+ class ContentType
85
+ attr_reader :media_type, :charset
86
+
87
+ DEFAULT_CHARSET = 'utf-8' # historically iso-8859-1 but see http://tools.ietf.org/html/rfc7231#appendix-B
88
+
89
+ def initialize(media_type, charset = DEFAULT_CHARSET)
90
+ @media_type = media_type
91
+ @charset = charset
92
+ end
93
+
94
+ def self.parse(s)
95
+ media_type = MediaType.parse(s)
96
+ charset = media_type.params.delete('charset') || DEFAULT_CHARSET
97
+ ContentType.new(media_type, charset)
98
+ end
99
+
100
+ def to_s
101
+ "#{@media_type}; charset=#{@charset}"
102
+ end
103
+ end
104
+
105
+ class MediaRange
106
+ include Comparable
107
+
108
+ DEFAULT_Q = 1.0
109
+
110
+ attr_reader :type, :subtype, :q, :params
111
+
112
+ def initialize(type, subtype, params = {})
113
+ @type = type
114
+ @subtype = subtype
115
+ @q = Float(params.delete('q')) rescue DEFAULT_Q
116
+ @params = params
117
+ end
118
+
119
+ def self.parse(s)
120
+ tree = Parsers::MediaRange.new.parse(s)
121
+ Parsers::MediaTypeTransform.new.apply(tree)
122
+ rescue Parslet::ParseFailed
123
+ raise Xenon::ParseError.new("Invalid media range (#{s})")
124
+ end
125
+
126
+ def <=>(other)
127
+ dt = compare_types(@type, other.type)
128
+ return dt if dt != 0
129
+ ds = compare_types(@subtype, other.subtype)
130
+ return ds if ds != 0
131
+ dp = params.size <=> other.params.size
132
+ return dp if dp != 0
133
+ @q <=> other.q
134
+ end
135
+
136
+ def =~(media_type)
137
+ (type == '*' || type == media_type.type) &&
138
+ (subtype == '*' || subtype == media_type.subtype) &&
139
+ params.all? { |n, v| media_type.params[n] == v }
140
+ end
141
+
142
+ alias_method :===, :=~
143
+
144
+ def to_s
145
+ s = "#{@type}/#{@subtype}"
146
+ s << @params.map { |n, v| v ? "; #{n}=#{v}" : "; #{n}" }.join
147
+ s << "; q=#{@q}" if @q != DEFAULT_Q
148
+ s
149
+ end
150
+
151
+ private
152
+
153
+ def compare_types(a, b)
154
+ if a == b then 0
155
+ elsif a == '*' then -1
156
+ elsif b == '*' then 1
157
+ else 0
158
+ end
159
+ end
160
+ end
161
+
162
+ end