xenon 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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