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
@@ -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,70 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'xenon/headers'
3
+ require 'xenon/parsers/header_rules'
4
+ require 'xenon/quoted_string'
5
+
6
+ module Xenon
7
+ class Headers
8
+ class Challenge
9
+ extend Forwardable
10
+ using QuotedString
11
+
12
+ attr_reader :auth_scheme
13
+ def_delegators :@params, :key?, :include?, :[]
14
+
15
+ def initialize(auth_scheme, params = {})
16
+ @auth_scheme = auth_scheme
17
+ @params = params.with_indifferent_access
18
+ end
19
+
20
+ def method_missing(name, *args, &block)
21
+ name = name.to_sym
22
+ @params.key?(name) ? @params[name] : super
23
+ end
24
+
25
+ def respond_to_missing?(name, include_all)
26
+ @params.key?(name.to_sym) || super
27
+ end
28
+
29
+ def to_s
30
+ param_string = @params.map { |k, v| "#{k}=#{v.quote}"}.join(', ')
31
+ "#{@auth_scheme} #{param_string}"
32
+ end
33
+ end
34
+
35
+ # https://tools.ietf.org/html/rfc7235#section-4.1
36
+ class WWWAuthenticate < ListHeader 'WWW-Authenticate'
37
+ def initialize(*challenges)
38
+ super(challenges)
39
+ end
40
+
41
+ alias_method :challenges, :values
42
+
43
+ def self.parse(s)
44
+ tree = Parsers::WWWAuthenticateHeader.new.parse(s)
45
+ Parsers::WWWAuthenticateHeaderTransform.new.apply(tree)
46
+ end
47
+
48
+ def to_s
49
+ challenges.map(&:to_s).join(', ')
50
+ end
51
+ end
52
+ end
53
+
54
+ module Parsers
55
+ class WWWAuthenticateHeader < Parslet::Parser
56
+ include AuthHeaderRules
57
+ rule(:challenge) { (auth_scheme >> sp >> (auth_params | token68)).as(:challenge) }
58
+ rule(:www_authenticate) { (challenge >> (comma >> challenge).repeat).as(:www_authenticate) }
59
+ root(:www_authenticate)
60
+ end
61
+
62
+ class WWWAuthenticateHeaderTransform < HeaderTransform
63
+ rule(auth_param: { name: simple(:n), value: simple(:v) }) { Tuple.new(n, v) }
64
+ rule(challenge: { auth_scheme: simple(:s), auth_params: simple(:p) }) { Headers::Challenge.new(s, Hash[*p.to_a]) }
65
+ rule(challenge: { auth_scheme: simple(:s), auth_params: sequence(:p) }) { Headers::Challenge.new(s, Hash[p.map(&:to_a)]) }
66
+ rule(www_authenticate: simple(:c)) { Headers::WWWAuthenticate.new(c) }
67
+ rule(www_authenticate: sequence(:c)) { Headers::WWWAuthenticate.new(*c) }
68
+ end
69
+ end
70
+ end
@@ -3,20 +3,33 @@ require 'xenon/parsers/media_type'
3
3
 
4
4
  module Xenon
5
5
 
6
+ # A media type.
7
+ #
8
+ # @see ContentType
9
+ # @see MediaRange
6
10
  class MediaType
7
11
  attr_reader :type, :subtype, :params
8
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'.
9
18
  def initialize(type, subtype, params = {})
10
19
  @type = type
11
20
  @subtype = subtype
12
21
  @params = params
13
22
  end
14
23
 
24
+ # Parses a media type.
25
+ #
26
+ # @param s [String] The media type string.
27
+ # @return [MediaType] The media type.
15
28
  def self.parse(s)
16
29
  tree = Parsers::MediaType.new.parse(s)
17
30
  Parsers::MediaTypeTransform.new.apply(tree)
18
31
  rescue Parslet::ParseFailed
19
- raise Xenon::ParseError.new("Invalid media type (#{s})")
32
+ raise Xenon::ParseError.new("Invalid media type (#{s}).")
20
33
  end
21
34
 
22
35
  %w(application audio image message multipart text video).each do |type|
@@ -43,10 +56,18 @@ module Xenon
43
56
  end
44
57
  end
45
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.
46
63
  def with_q(q)
47
64
  MediaRange.new(self, q)
48
65
  end
49
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.
50
71
  def with_charset(charset)
51
72
  ContentType.new(self, charset)
52
73
  end
@@ -59,6 +80,7 @@ module Xenon
59
80
  XML = MediaType.new('application', 'xml')
60
81
  end
61
82
 
83
+ # A content type.
62
84
  class ContentType
63
85
  attr_reader :media_type, :charset
64
86
 
@@ -109,7 +131,7 @@ module Xenon
109
131
  dp = params.size <=> other.params.size
110
132
  return dp if dp != 0
111
133
  @q <=> other.q
112
- end
134
+ end
113
135
 
114
136
  def =~(media_type)
115
137
  (type == '*' || type == media_type.type) &&
@@ -3,6 +3,19 @@ require 'parslet'
3
3
  module Xenon
4
4
  module Parsers
5
5
 
6
+ # Parslet doesn't match sequence of sequences (i.e. [['foo', 'bar']]) as a sequence(:v) in transform
7
+ # rules so this is a little wrapper class that allows smuggling an array through the matcher rules,
8
+ # for example above would be [Tuple.new('foo', 'bar')], when no 'proper' class is required.
9
+ class Tuple
10
+ def initialize(*values)
11
+ @values = values
12
+ end
13
+
14
+ def to_a
15
+ @values
16
+ end
17
+ end
18
+
6
19
  module BasicRules
7
20
  include Parslet
8
21
 
@@ -35,21 +48,39 @@ module Xenon
35
48
  rule(:tchar) { alpha | digit | match(/[!#\$%&'\*\+\-\.\^_`\|~]/) }
36
49
  rule(:token) { tchar.repeat(1) }
37
50
 
38
- rule(:obs_text) { match(/[\u0080-\u00ff]/)}
39
- rule(:qdtext) { htab | sp | match(/[\u0021\u0023-\u005b\u005d-\u007e]/) | obs_text }
40
- rule(:quoted_pair) { str('\\') >> (htab | sp | vchar | obs_text) }
41
- rule(:quoted_string) { (dquote >> (qdtext | quoted_pair).repeat >> dquote).as(:quoted_string) }
51
+ # http://tools.ietf.org/html/rfc7231#section-7.1.1.1
52
+ rule(:day_name) { str('Mon') | str('Tue') | str('Wed') | str('Thu') | str('Fri') | str('Sat') | str('Sun') }
53
+ rule(:day) { digit.repeat(2) }
54
+ rule(:month) { (str('Jan') | str('Feb') | str('Mar') | str('Apr') | str('May') | str('Jun') | str('Jul') | str('Aug') | str('Sep') | str('Oct') | str('Nov') | str('Dec')) }
55
+ rule(:year) { digit.repeat(4) }
56
+ rule(:date1) { day >> sp >> month >> sp >> year }
57
+ rule(:gmt) { str('GMT') }
58
+ rule(:hour) { digit.repeat(2) }
59
+ rule(:minute) { digit.repeat(2) }
60
+ rule(:second) { digit.repeat(2) }
61
+ rule(:time_of_day) { hour >> str(':') >> minute >> str(':') >> second }
62
+ rule(:imf_fixdate) { day_name >> str(',') >> sp >> date1 >> sp >> time_of_day >> sp >> gmt }
63
+ rule(:day_name_l) { str('Monday') | str('Tuesday') | str('Wednesday') | str('Thursday') | str('Friday') | str('Saturday') | str('Sunday') }
64
+ rule(:year2) { digit.repeat(2) }
65
+ rule(:date2) { day >> str('-') >> month >> str('-') >> year2 }
66
+ rule(:rfc850_date) { day_name_l >> str(',') >> sp >> date2 >> sp >> time_of_day >> sp >> gmt }
67
+ rule(:day1) { sp >> digit }
68
+ rule(:date3) { month >> sp >> (day | day1) }
69
+ rule(:asctime_date) { day_name >> sp >> date3 >> sp >> time_of_day >> sp >> year }
70
+ rule(:obs_date) { rfc850_date | asctime_date }
71
+ rule(:http_date) { (imf_fixdate | obs_date).as(:http_date) }
42
72
 
43
- # extras -- TODO: move these into header rules?
73
+ # extras -- TODO: move these into header rules?
44
74
  rule(:comma) { str(',') >> sp? }
45
75
  rule(:semicolon) { str(';') >> sp? }
46
76
  end
47
77
 
48
78
  class BasicTransform < Parslet::Transform
49
79
  rule(simple(:v)) { v.respond_to?(:str) ? v.str : v }
50
-
80
+
51
81
  rule(quoted_string: simple(:qs)) { qs[1..-2].gsub(/\\(.)/, '\1') }
82
+ rule(http_date: simple(:str)) { Time.httpdate(str) }
52
83
  end
53
-
84
+
54
85
  end
55
86
  end
@@ -1,13 +1,59 @@
1
+ require 'xenon/quoted_string'
1
2
  require 'xenon/parsers/basic_rules'
2
3
 
3
4
  module Xenon
4
5
  module Parsers
5
6
 
6
- # http://tools.ietf.org/html/rfc7231#section-5.3.1
7
- module WeightRules
7
+ module HeaderRules
8
8
  include Parslet, BasicRules
9
+
10
+ # http://tools.ietf.org/html/rfc7230#section-3.2.6
11
+ rule(:list_sep) { str(',') >> sp? }
12
+ rule(:param_sep) { str(';') >> sp? }
13
+ rule(:obs_text) { match(/[\u0080-\u00ff]/)}
14
+ rule(:qdtext) { htab | sp | match(/[\u0021\u0023-\u005b\u005d-\u007e]/) | obs_text }
15
+ rule(:quoted_pair) { str('\\') >> (htab | sp | vchar | obs_text) }
16
+ rule(:quoted_string) { (dquote >> (qdtext | quoted_pair).repeat >> dquote).as(:quoted_string) }
17
+ rule(:ctext) { htab | sp | match(/[\u0021-\u0027\u002a-\u005b\u005d-\u007e]/) | obs_text }
18
+ rule(:comment) { (str('(') >> (ctext | quoted_pair | comment).repeat >> str(')')).as(:comment) }
19
+
20
+ # http://tools.ietf.org/html/rfc7231#section-5.3.1
9
21
  rule(:weight_value) { (digit >> (str('.') >> digit.repeat(0, 3)).maybe).as(:q) }
10
- rule(:weight) { semicolon >> str('q') >> sp? >> str('=') >> sp? >> weight_value >> sp? }
22
+ rule(:weight) { param_sep >> str('q') >> sp? >> str('=') >> sp? >> weight_value >> sp? }
23
+ end
24
+
25
+ module AuthHeaderRules
26
+ include Parslet, HeaderRules
27
+
28
+ rule(:token68) { ((alpha | digit | match(/[\-\._~\+\/]/)) >> str('=').repeat).repeat(1).as(:token) }
29
+ rule(:auth_scheme) { token.as(:auth_scheme) }
30
+ rule(:name) { token.as(:name) }
31
+ rule(:value) { token.as(:value) }
32
+ rule(:auth_param) { (name >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:auth_param) }
33
+ rule(:auth_params) { (auth_param.maybe >> (ows >> comma >> ows >> auth_param).repeat).as(:auth_params) }
34
+ end
35
+
36
+ module ETagHeaderRules
37
+ include Parslet, HeaderRules
38
+
39
+ # http://tools.ietf.org/html/rfc7232#section-2.3
40
+ rule(:wildcard) { str('*').as(:wildcard) }
41
+ rule(:weak) { str('W/').as(:weak) }
42
+ rule(:etagc) { str('!') | match(/[\u0023-\u007e#-~]/) | obs_text }
43
+ rule(:opaque_tag) { dquote >> etagc.repeat.as(:opaque_tag) >> dquote }
44
+ rule(:etag) { (weak.maybe >> opaque_tag).as(:etag) }
45
+ end
46
+
47
+ class HeaderTransform < BasicTransform
48
+ using QuotedString
49
+
50
+ rule(quoted_string: simple(:qs)) { qs.unquote }
51
+ rule(comment: simple(:c)) { c.uncomment }
52
+ end
53
+
54
+ class ETagHeaderTransform < HeaderTransform
55
+ rule(etag: { opaque_tag: simple(:t), weak: simple(:w) }) { Xenon::ETag.new(t, weak: true) }
56
+ rule(etag: { opaque_tag: simple(:t) }) { Xenon::ETag.new(t) }
11
57
  end
12
58
 
13
59
  end
@@ -16,10 +16,11 @@ module Xenon
16
16
  rule(:slash) { str('/') }
17
17
  rule(:subtype) { restricted_name.as(:subtype) >> sp? }
18
18
 
19
+ rule(:param_sep) { str(';') >> sp? }
19
20
  rule(:param_name) { restricted_name.as(:param_name) >> sp? }
20
21
  rule(:equals) { str('=') >> sp? }
21
22
  rule(:param_value) { token.as(:param_value) >> sp? } # not quite correct but probably correct enough
22
- rule(:param) { semicolon >> param_name >> (equals >> param_value).maybe >> sp? }
23
+ rule(:param) { param_sep >> param_name >> (equals >> param_value).maybe >> sp? }
23
24
  rule(:params) { param.repeat.as(:params) }
24
25
 
25
26
  rule(:media_type) { (type >> slash >> subtype >> params).as(:media_type) >> sp? }
@@ -44,8 +45,8 @@ module Xenon
44
45
  rule(param_name: simple(:n), param_value: simple(:v)) { [n.str, v.str] }
45
46
  rule(param_name: simple(:n)) { [n.str, nil] }
46
47
  rule(type: simple(:t), subtype: simple(:s), params: subtree(:p)) { { type: t.str, subtype: s.str, params: Hash[p] } }
47
- rule(media_type: subtree(:mt)) { ::Xenon::MediaType.new(mt[:type], mt[:subtype], mt[:params])}
48
- rule(media_range: subtree(:mr)) { ::Xenon::MediaRange.new(mr[:type], mr[:subtype], mr[:params])}
48
+ rule(media_type: subtree(:mt)) { Xenon::MediaType.new(mt[:type], mt[:subtype], mt[:params])}
49
+ rule(media_range: subtree(:mr)) { Xenon::MediaRange.new(mr[:type], mr[:subtype], mr[:params])}
49
50
  end
50
51
 
51
52
  end
@@ -2,9 +2,19 @@ module Xenon
2
2
  module QuotedString
3
3
  refine String do
4
4
  def quote
5
- qs = self.gsub(/([\\"])/, '\\\\\1')
5
+ qs = gsub(/([\\"])/, '\\\\\1')
6
6
  self == qs ? self : %{"#{qs}"}
7
7
  end
8
+
9
+ def unquote
10
+ qs = start_with?('"') && end_with?('"') ? self[1..-2] : self
11
+ qs.gsub(/\\(.)/, '\1')
12
+ end
13
+
14
+ def uncomment
15
+ qs = start_with?('(') && end_with?(')') ? self[1..-2] : self
16
+ qs.gsub(/\\(.)/, '\1')
17
+ end
8
18
  end
9
19
  end
10
20
  end
@@ -0,0 +1,14 @@
1
+ Dir[File.join(__dir__, '*_directives.rb')].each { |f| require f }
2
+
3
+ module Xenon
4
+ module Routing
5
+ module Directives
6
+ include RouteDirectives
7
+ include HeaderDirectives
8
+ include MethodDirectives
9
+ include ParamDirectives
10
+ include PathDirectives
11
+ include SecurityDirectives
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ require 'xenon/routing/route_directives'
2
+
3
+ module Xenon
4
+ module Routing
5
+ module HeaderDirectives
6
+ include RouteDirectives
7
+
8
+ def optional_header(name)
9
+ extract_request do |request|
10
+ yield request.header(name)
11
+ end
12
+ end
13
+
14
+ def header(name)
15
+ optional_header(name) do |value|
16
+ if value
17
+ yield value
18
+ else
19
+ reject Rejection.new(:header, { required: name })
20
+ end
21
+ end
22
+ end
23
+
24
+ def respond_with_header(header)
25
+ map_response -> r { r.copy(headers: r.headers.add(header)) } do
26
+ yield
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end