xenon 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ require 'parslet'
2
+
3
+ module Xenon
4
+ module Parsers
5
+
6
+ module BasicRules
7
+ include Parslet
8
+
9
+ # http://tools.ietf.org/html/rfc5234#appendix-B.1
10
+ rule(:alpha) { match(/[a-z]/i) }
11
+ rule(:bit) { match(/[01]/) }
12
+ rule(:char) { match(/[\u0001-\u007f]/) }
13
+ rule(:digit) { match(/[0-9]/) }
14
+ rule(:hexdig) { match(/[a-f0-9]/i)}
15
+ rule(:vchar) { match(/[\u0021-\u007e]/) }
16
+ rule(:alphanum) { alpha | digit }
17
+
18
+ rule(:sp) { str(' ') }
19
+ rule(:sp?) { sp.repeat }
20
+ rule(:htab) { str("\t") }
21
+ rule(:wsp) { sp | htab }
22
+ rule(:lwsp) { (crlf.maybe >> wsp).repeat }
23
+
24
+ rule(:cr) { str("\r") }
25
+ rule(:lf) { str("\n") }
26
+ rule(:crlf) { cr >> lf }
27
+ rule(:dquote) { str('"') }
28
+
29
+ # http://tools.ietf.org/html/rfc7230#section-3.2.3
30
+ rule(:ows) { wsp.repeat }
31
+ rule(:rws) { wsp.repeat(1) }
32
+ rule(:bws) { wsp.repeat }
33
+
34
+ # http://tools.ietf.org/html/rfc7230#section-3.2.6
35
+ rule(:tchar) { alpha | digit | match(/[!#\$%&'\*\+\-\.\^_`\|~]/) }
36
+ rule(:token) { tchar.repeat(1) }
37
+
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) }
42
+
43
+ # extras -- TODO: move these into header rules?
44
+ rule(:comma) { str(',') >> sp? }
45
+ rule(:semicolon) { str(';') >> sp? }
46
+ end
47
+
48
+ class BasicTransform < Parslet::Transform
49
+ rule(simple(:v)) { v.respond_to?(:str) ? v.str : v }
50
+
51
+ rule(quoted_string: simple(:qs)) { qs[1..-2].gsub(/\\(.)/, '\1') }
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,14 @@
1
+ require 'xenon/parsers/basic_rules'
2
+
3
+ module Xenon
4
+ module Parsers
5
+
6
+ # http://tools.ietf.org/html/rfc7231#section-5.3.1
7
+ module WeightRules
8
+ include Parslet, BasicRules
9
+ rule(:weight_value) { (digit >> (str('.') >> digit.repeat(0, 3)).maybe).as(:q) }
10
+ rule(:weight) { semicolon >> str('q') >> sp? >> str('=') >> sp? >> weight_value >> sp? }
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,52 @@
1
+ require 'xenon/parsers/basic_rules'
2
+
3
+ module Xenon
4
+ module Parsers
5
+
6
+ # note this uses the rules from https://tools.ietf.org/html/rfc6838 not http://tools.ietf.org/html/rfc7231
7
+ # because the latter is slightly ambiguous, e.g. a token can include * so */json would parse correctly
8
+ module MediaTypeRules
9
+ include Parslet, BasicRules
10
+
11
+ rule(:restricted_name_first) { match(/[a-zA-Z0-9]/) }
12
+ rule(:restricted_name_chars) { match(/[a-zA-Z0-9!#\$&\-\^_\.\+]/).repeat(0, 126) }
13
+ rule(:restricted_name) { restricted_name_first >> restricted_name_chars }
14
+
15
+ rule(:type) { restricted_name.as(:type) }
16
+ rule(:slash) { str('/') }
17
+ rule(:subtype) { restricted_name.as(:subtype) >> sp? }
18
+
19
+ rule(:param_name) { restricted_name.as(:param_name) >> sp? }
20
+ rule(:equals) { str('=') >> sp? }
21
+ 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(:params) { param.repeat.as(:params) }
24
+
25
+ rule(:media_type) { (type >> slash >> subtype >> params).as(:media_type) >> sp? }
26
+
27
+ rule(:wildcard) { str('*') }
28
+ rule(:wild_media_range) { wildcard.as(:type) >> slash >> wildcard.as(:subtype) >> params }
29
+ rule(:root_media_range) { type >> slash >> (wildcard.as(:subtype) | subtype) >> params }
30
+ rule(:media_range) { (wild_media_range | root_media_range).as(:media_range) >> sp? }
31
+ end
32
+
33
+ class MediaType < Parslet::Parser
34
+ include MediaTypeRules
35
+ root(:media_type)
36
+ end
37
+
38
+ class MediaRange < Parslet::Parser
39
+ include MediaTypeRules
40
+ root(:media_range)
41
+ end
42
+
43
+ class MediaTypeTransform < Parslet::Transform
44
+ rule(param_name: simple(:n), param_value: simple(:v)) { [n.str, v.str] }
45
+ rule(param_name: simple(:n)) { [n.str, nil] }
46
+ 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])}
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,10 @@
1
+ module Xenon
2
+ module QuotedString
3
+ refine String do
4
+ def quote
5
+ qs = self.gsub(/([\\"])/, '\\\\\1')
6
+ self == qs ? self : %{"#{qs}"}
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,133 @@
1
+ module Xenon
2
+ module Routing
3
+
4
+ module RouteDirectives
5
+ def map_request(map)
6
+ context.branch do
7
+ context.request = map.respond_to?(:call) ? map.call(context.request) : context.request.copy(map)
8
+ yield
9
+ end
10
+ end
11
+
12
+ def map_response(map)
13
+ context.branch do
14
+ context.response = map.respond_to?(:call) ? map.call(context.response) : context.response.copy(map)
15
+ yield
16
+ end
17
+ end
18
+
19
+ def complete(status, body)
20
+ map_response complete: true, status: status, body: body do
21
+ throw :complete
22
+ end
23
+ end
24
+
25
+ def reject(rejection)
26
+ context.rejections << rejection unless rejection.nil?
27
+ end
28
+
29
+ def extract(lambda)
30
+ yield lambda.call(context)
31
+ end
32
+
33
+ def extract_request(lambda = nil)
34
+ yield lambda ? lambda.call(context.request) : context.request
35
+ end
36
+ end
37
+
38
+ module HeaderDirectives
39
+ def optional_header(name)
40
+ extract_request do |request|
41
+ yield request.header(name)
42
+ end
43
+ end
44
+
45
+ def header(name)
46
+ optional_header(name) do |value|
47
+ if value
48
+ yield value
49
+ else
50
+ reject Rejection.new(Rejection::HEADER, { required: name })
51
+ end
52
+ end
53
+ end
54
+
55
+ def respond_with_header(header)
56
+ map_response -> r { r.copy(headers: r.headers.add(header)) } do
57
+ yield
58
+ end
59
+ end
60
+ end
61
+
62
+ module MethodDirectives
63
+ include RouteDirectives
64
+
65
+ def request_method(method)
66
+ extract_request do |request|
67
+ if request.request_method == method
68
+ yield
69
+ else
70
+ reject Rejection.new(Rejection::METHOD, { supported: method })
71
+ end
72
+ end
73
+ end
74
+
75
+ %i(delete get head options patch post put).each do |method|
76
+ define_method(method) do |&inner|
77
+ request_method(method, &inner)
78
+ end
79
+ end
80
+ end
81
+
82
+ module ParamDirectives
83
+ def form_hash
84
+ extract_request do |request|
85
+ yield request.form_hash
86
+ end
87
+ end
88
+
89
+ def query_hash
90
+ extract_request do |request|
91
+ yield request.query_hash
92
+ end
93
+ end
94
+ end
95
+
96
+ module PathDirectives
97
+ include RouteDirectives
98
+
99
+ def path_prefix(pattern)
100
+ extract_request do |request|
101
+ match = request.unmatched_path.match(pattern)
102
+ if match && match.pre_match == ''
103
+ map_request unmatched_path: match.post_match do
104
+ yield *match.captures
105
+ end
106
+ else
107
+ reject nil # path rejections are nil to allow more specific rejections to be seen
108
+ end
109
+ end
110
+ end
111
+
112
+ def path_end(&inner)
113
+ path_prefix(/\Z/, &inner)
114
+ end
115
+
116
+ def path(pattern, &inner)
117
+ path_prefix(pattern) do |*captures|
118
+ path_end do
119
+ inner.call(*captures)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ module Directives
126
+ include HeaderDirectives
127
+ include MethodDirectives
128
+ include ParamDirectives
129
+ include PathDirectives
130
+ end
131
+
132
+ end
133
+ end
data/lib/xenon/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Xenon
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,91 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # The `.rspec` file also contains a few flags that are not defaults but that
16
+ # users commonly want.
17
+ #
18
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19
+ RSpec.configure do |config|
20
+ # rspec-expectations config goes here. You can use an alternate
21
+ # assertion/expectation library such as wrong or the stdlib/minitest
22
+ # assertions if you prefer.
23
+ config.expect_with :rspec do |expectations|
24
+ # This option will default to `true` in RSpec 4. It makes the `description`
25
+ # and `failure_message` of custom matchers include text for helper methods
26
+ # defined using `chain`, e.g.:
27
+ # be_bigger_than(2).and_smaller_than(4).description
28
+ # # => "be bigger than 2 and smaller than 4"
29
+ # ...rather than:
30
+ # # => "be bigger than 2"
31
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
32
+ end
33
+
34
+ # rspec-mocks config goes here. You can use an alternate test double
35
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
36
+ config.mock_with :rspec do |mocks|
37
+ # Prevents you from mocking or stubbing a method that does not exist on
38
+ # a real object. This is generally recommended, and will default to
39
+ # `true` in RSpec 4.
40
+ mocks.verify_partial_doubles = true
41
+ end
42
+
43
+ # The settings below are suggested to provide a good initial experience
44
+ # with RSpec, but feel free to customize to your heart's content.
45
+ =begin
46
+ # These two settings work together to allow you to limit a spec run
47
+ # to individual examples or groups you care about by tagging them with
48
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
49
+ # get run.
50
+ config.filter_run :focus
51
+ config.run_all_when_everything_filtered = true
52
+
53
+ # Limits the available syntax to the non-monkey patched syntax that is
54
+ # recommended. For more details, see:
55
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
56
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
57
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
58
+ config.disable_monkey_patching!
59
+
60
+ # This setting enables warnings. It's recommended, but in some cases may
61
+ # be too noisy due to issues in dependencies.
62
+ config.warnings = true
63
+
64
+ # Many RSpec users commonly either run the entire suite or an individual
65
+ # file, and it's useful to allow more verbose output when running an
66
+ # individual spec file.
67
+ if config.files_to_run.one?
68
+ # Use the documentation formatter for detailed output,
69
+ # unless a formatter has already been configured
70
+ # (e.g. via a command-line flag).
71
+ config.default_formatter = 'doc'
72
+ end
73
+
74
+ # Print the 10 slowest examples and example groups at the
75
+ # end of the spec run, to help surface which specs are running
76
+ # particularly slow.
77
+ config.profile_examples = 10
78
+
79
+ # Run specs in random order to surface order dependencies. If you find an
80
+ # order dependency and want to debug it, you can fix the order by providing
81
+ # the seed, which is printed after each run.
82
+ # --seed 1234
83
+ config.order = :random
84
+
85
+ # Seed global randomization in this process using the `--seed` CLI option.
86
+ # Setting this allows you to use `--seed` to deterministically reproduce
87
+ # test failures related to randomization by passing the same `--seed` value
88
+ # as the one that triggered the failure.
89
+ Kernel.srand config.seed
90
+ =end
91
+ end
@@ -0,0 +1,31 @@
1
+ require 'xenon/headers/accept_charset'
2
+
3
+ describe Xenon::Headers::AcceptCharset do
4
+
5
+ context '::parse' do
6
+ it 'can parse a basic charset range' do
7
+ header = Xenon::Headers::AcceptCharset.parse('unicode-1-1;q=0.8')
8
+ expect(header.charset_ranges.size).to eq(1)
9
+ expect(header.charset_ranges[0].to_s).to eq('unicode-1-1; q=0.8')
10
+ end
11
+
12
+ it 'can parse the example from RFC 7231 § 5.3.3 with the right precedence' do
13
+ header = Xenon::Headers::AcceptCharset.parse('iso-8859-5, unicode-1-1;q=0.8')
14
+ expect(header.charset_ranges.size).to eq(2)
15
+ expect(header.charset_ranges[0].to_s).to eq('iso-8859-5')
16
+ expect(header.charset_ranges[1].to_s).to eq('unicode-1-1; q=0.8')
17
+ end
18
+ end
19
+
20
+ context '#merge' do
21
+ it 'can merge two headers with the right precedence' do
22
+ h1 = Xenon::Headers::AcceptCharset.parse('unicode-1-1;q=0.8')
23
+ h2 = Xenon::Headers::AcceptCharset.parse('iso-8859-5')
24
+ header = h1.merge(h2)
25
+ expect(header.charset_ranges.size).to eq(2)
26
+ expect(header.charset_ranges[0].to_s).to eq('iso-8859-5')
27
+ expect(header.charset_ranges[1].to_s).to eq('unicode-1-1; q=0.8')
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,40 @@
1
+ require 'xenon/headers/accept_encoding'
2
+
3
+ describe Xenon::Headers::AcceptEncoding do
4
+
5
+ context '::parse' do
6
+ %w(identity compress x-compress deflate gzip x-gzip *).each do |cc|
7
+ it "can parse the #{cc} content coding" do
8
+ header = Xenon::Headers::AcceptEncoding.parse(cc)
9
+ expect(header.coding_ranges.size).to eq(1)
10
+ expect(header.coding_ranges[0].to_s).to eq(cc)
11
+ end
12
+ end
13
+
14
+ it 'can parse the fifth example from RFC 7231 § 5.3.4 with the right precedence' do
15
+ header = Xenon::Headers::AcceptEncoding.parse('gzip;q=1.0, identity; q=0.5, *;q=0')
16
+ expect(header.coding_ranges.size).to eq(3)
17
+ expect(header.coding_ranges[0].to_s).to eq('gzip')
18
+ expect(header.coding_ranges[1].to_s).to eq('identity; q=0.5')
19
+ expect(header.coding_ranges[2].to_s).to eq('*; q=0.0')
20
+ end
21
+
22
+ it 'parses an empty header as containing no codings' do
23
+ header = Xenon::Headers::AcceptEncoding.parse('')
24
+ expect(header.coding_ranges.size).to eq(0)
25
+ end
26
+ end
27
+
28
+ context '#merge' do
29
+ it 'can merge two headers with the right precedence' do
30
+ h1 = Xenon::Headers::AcceptEncoding.parse('identity; q=0.5')
31
+ h2 = Xenon::Headers::AcceptEncoding.parse('gzip;q=1.0, *;q=0')
32
+ header = h1.merge(h2)
33
+ expect(header.coding_ranges.size).to eq(3)
34
+ expect(header.coding_ranges[0].to_s).to eq('gzip')
35
+ expect(header.coding_ranges[1].to_s).to eq('identity; q=0.5')
36
+ expect(header.coding_ranges[2].to_s).to eq('*; q=0.0')
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,33 @@
1
+ require 'xenon/headers/accept_language'
2
+
3
+ describe Xenon::Headers::AcceptLanguage do
4
+
5
+ context '::parse' do
6
+ it 'can parse a basic language range' do
7
+ header = Xenon::Headers::AcceptLanguage.parse('en-gb;q=0.8')
8
+ expect(header.language_ranges.size).to eq(1)
9
+ expect(header.language_ranges[0].to_s).to eq('en-gb; q=0.8')
10
+ end
11
+
12
+ it 'can parse the example from RFC 7231 § 5.3.5 with the right precedence' do
13
+ header = Xenon::Headers::AcceptLanguage.parse('da, en-gb;q=0.8, en;q=0.7')
14
+ expect(header.language_ranges.size).to eq(3)
15
+ expect(header.language_ranges[0].to_s).to eq('da')
16
+ expect(header.language_ranges[1].to_s).to eq('en-gb; q=0.8')
17
+ expect(header.language_ranges[2].to_s).to eq('en; q=0.7')
18
+ end
19
+ end
20
+
21
+ context '#merge' do
22
+ it 'can merge two headers with the right precedence' do
23
+ h1 = Xenon::Headers::AcceptLanguage.parse('da, en;q=0.7')
24
+ h2 = Xenon::Headers::AcceptLanguage.parse('en-gb;q=0.8')
25
+ header = h1.merge(h2)
26
+ expect(header.language_ranges.size).to eq(3)
27
+ expect(header.language_ranges[0].to_s).to eq('da')
28
+ expect(header.language_ranges[1].to_s).to eq('en-gb; q=0.8')
29
+ expect(header.language_ranges[2].to_s).to eq('en; q=0.7')
30
+ end
31
+ end
32
+
33
+ end