xenon-http 0.0.4

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 (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 +160 -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