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,26 @@
1
+ require 'xenon/routing/route_directives'
2
+
3
+ module Xenon
4
+ module Routing
5
+ module MethodDirectives
6
+ include RouteDirectives
7
+
8
+ def request_method(method)
9
+ extract_request do |request|
10
+ if request.request_method == method
11
+ yield
12
+ else
13
+ reject Rejection.new(:method, { supported: method })
14
+ end
15
+ end
16
+ end
17
+
18
+ %i(delete get head options patch post put).each do |method|
19
+ define_method(method) do |&inner|
20
+ request_method(method, &inner)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ require 'xenon/routing/route_directives'
2
+
3
+ module Xenon
4
+ module Routing
5
+ module ParamDirectives
6
+ include RouteDirectives
7
+
8
+ def param_hash
9
+ extract_request do |request|
10
+ yield request.param_hash
11
+ end
12
+ end
13
+
14
+ def params(*names)
15
+ param_hash do |hash|
16
+ yield *hash.slice(*names).values
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ require 'xenon/routing/route_directives'
2
+
3
+ module Xenon
4
+ module Routing
5
+ module PathDirectives
6
+ include RouteDirectives
7
+
8
+ def path_prefix(pattern)
9
+ extract_request do |request|
10
+ match = request.unmatched_path.match(pattern)
11
+ if match && match.pre_match == ''
12
+ map_request unmatched_path: match.post_match do
13
+ yield *match.captures
14
+ end
15
+ else
16
+ reject nil # path rejections are nil to allow more specific rejections to be seen
17
+ end
18
+ end
19
+ end
20
+
21
+ def path_end
22
+ path_prefix(/\Z/) do
23
+ yield
24
+ end
25
+ end
26
+
27
+ def path(pattern)
28
+ path_prefix(pattern) do |*captures|
29
+ path_end do
30
+ yield *captures
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ require 'rack/utils'
2
+
3
+ module Xenon
4
+ module Routing
5
+ module RouteDirectives
6
+
7
+ def map_request(map)
8
+ context.branch do
9
+ context.request = map.respond_to?(:call) ? map.call(context.request) : context.request.copy(map)
10
+ yield
11
+ end
12
+ end
13
+
14
+ def map_response(map)
15
+ context.branch do
16
+ context.response = map.respond_to?(:call) ? map.call(context.response) : context.response.copy(map)
17
+ yield
18
+ end
19
+ end
20
+
21
+ def complete(status, body)
22
+ map_response complete: true, status: Rack::Utils.status_code(status), body: body do
23
+ throw :complete
24
+ end
25
+ end
26
+
27
+ def reject(rejection, info = {})
28
+ return if rejection.nil?
29
+ rejection = Rejection.new(rejection, info) unless rejection.is_a?(Rejection)
30
+ context.rejections << rejection
31
+ end
32
+
33
+ def fail(status, developer_message = nil)
34
+ body = {
35
+ status: status,
36
+ developer_message: developer_message || Rack::Utils::HTTP_STATUS_CODES[status]
37
+ }
38
+ complete status, body
39
+ end
40
+
41
+ def extract(lambda)
42
+ yield lambda.call(context)
43
+ end
44
+
45
+ def extract_request(lambda = nil)
46
+ yield lambda ? lambda.call(context.request) : context.request
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,20 @@
1
+ require 'xenon/routing/route_directives'
2
+
3
+ module Xenon
4
+ module Routing
5
+ module SecurityDirectives
6
+ include RouteDirectives
7
+
8
+ def authenticate(authenticator)
9
+ extract_request(authenticator) do |user|
10
+ if user
11
+ yield user
12
+ else
13
+ reject :unauthorized, { scheme: authenticator.scheme }.merge(authenticator.auth_params)
14
+ end
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Xenon
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -1,3 +1,6 @@
1
+ require "codeclimate-test-reporter"
2
+ CodeClimate::TestReporter.start
3
+
1
4
  # This file was generated by the `rspec --init` command. Conventionally, all
2
5
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
6
  # The generated `.rspec` file contains `--require spec_helper` which will cause
@@ -0,0 +1,19 @@
1
+ require 'xenon/etag'
2
+
3
+ describe Xenon::ETag do
4
+
5
+ describe '::parse' do
6
+ it 'can parse a strong etag' do
7
+ etag = Xenon::ETag.parse('"xyzzy"')
8
+ expect(etag.opaque_tag).to eq 'xyzzy'
9
+ expect(etag).to be_strong
10
+ end
11
+
12
+ it 'can parse a weak etag' do
13
+ etag = Xenon::ETag.parse('W/"xyzzy"')
14
+ expect(etag.opaque_tag).to eq 'xyzzy'
15
+ expect(etag).to be_weak
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,73 @@
1
+ require 'xenon/headers/if_match'
2
+
3
+ describe Xenon::Headers::IfMatch do
4
+
5
+ context '::parse' do
6
+ it 'can parse a single etag' do
7
+ header = Xenon::Headers::IfMatch.parse('"xyzzy"')
8
+ expect(header.etags.size).to eq(1)
9
+ expect(header.etags[0]).to eq(Xenon::ETag.new('xyzzy'))
10
+ end
11
+
12
+ it 'can parse multiple etags' do
13
+ header = Xenon::Headers::IfMatch.parse('"xyzzy", "r2d2xxxx", "c3piozzzz"')
14
+ expect(header.etags.size).to eq(3)
15
+ expect(header.etags[0]).to eq(Xenon::ETag.new('xyzzy'))
16
+ expect(header.etags[1]).to eq(Xenon::ETag.new('r2d2xxxx'))
17
+ expect(header.etags[2]).to eq(Xenon::ETag.new('c3piozzzz'))
18
+ end
19
+
20
+ it 'can parse a wildcard header' do
21
+ header = Xenon::Headers::IfMatch.parse('*')
22
+ expect(header.etags.size).to eq(0)
23
+ end
24
+ end
25
+
26
+ context '#merge' do
27
+ it 'can merge two headers and maintain etag order' do
28
+ h1 = Xenon::Headers::IfMatch.parse('"xyzzy", "r2d2xxxx"')
29
+ h2 = Xenon::Headers::IfMatch.parse('"c3piozzzz"')
30
+ header = h1.merge(h2)
31
+ expect(header.etags.size).to eq(3)
32
+ expect(header.etags[0]).to eq(Xenon::ETag.new('xyzzy'))
33
+ expect(header.etags[1]).to eq(Xenon::ETag.new('r2d2xxxx'))
34
+ expect(header.etags[2]).to eq(Xenon::ETag.new('c3piozzzz'))
35
+ end
36
+
37
+ it 'raises a protocol error when trying to merge into a wildcard header' do
38
+ h1 = Xenon::Headers::IfMatch.parse('*')
39
+ h2 = Xenon::Headers::IfMatch.parse('"c3piozzzz"')
40
+ expect { h1.merge(h2) }.to raise_error(Xenon::ProtocolError)
41
+ end
42
+
43
+ it 'raises a protocol error when trying to merge a wildcard into a header' do
44
+ h1 = Xenon::Headers::IfMatch.parse('"xyzzy"')
45
+ h2 = Xenon::Headers::IfMatch.parse('*')
46
+ expect { h1.merge(h2) }.to raise_error(Xenon::ProtocolError)
47
+ end
48
+
49
+ it 'raises a protocol error when trying to merge two wildcard headers' do
50
+ h1 = Xenon::Headers::IfMatch.parse('*')
51
+ h2 = Xenon::Headers::IfMatch.parse('*')
52
+ expect { h1.merge(h2) }.to raise_error(Xenon::ProtocolError)
53
+ end
54
+ end
55
+
56
+ context '#to_s' do
57
+ it 'returns the string representation a single etag' do
58
+ header = Xenon::Headers::IfMatch.parse('"xyzzy"')
59
+ expect(header.to_s).to eq('"xyzzy"')
60
+ end
61
+
62
+ it 'returns the string representation of multiple etags' do
63
+ header = Xenon::Headers::IfMatch.parse('"xyzzy", "r2d2xxxx", "c3piozzzz"')
64
+ expect(header.to_s).to eq('"xyzzy", "r2d2xxxx", "c3piozzzz"')
65
+ end
66
+
67
+ it 'returns the string representation of a wildcard header' do
68
+ header = Xenon::Headers::IfMatch.wildcard
69
+ expect(header.to_s).to eq('*')
70
+ end
71
+ end
72
+
73
+ end
@@ -0,0 +1,19 @@
1
+ require 'xenon/headers/if_modified_since'
2
+
3
+ describe Xenon::Headers::IfModifiedSince do
4
+
5
+ context '::parse' do
6
+ it 'can parse an http date' do
7
+ header = Xenon::Headers::IfModifiedSince.parse('Sat, 29 Oct 1994 19:43:31 GMT')
8
+ expect(header.date).to eq(Time.utc(1994, 10, 29, 19, 43, 31))
9
+ end
10
+ end
11
+
12
+ context '#to_s' do
13
+ it 'returns the http date format' do
14
+ header = Xenon::Headers::IfModifiedSince.new(Time.utc(1994, 10, 29, 19, 43, 31))
15
+ expect(header.to_s).to eq('Sat, 29 Oct 1994 19:43:31 GMT')
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,79 @@
1
+ require 'xenon/headers/if_none_match'
2
+
3
+ describe Xenon::Headers::IfNoneMatch do
4
+
5
+ context '::parse' do
6
+ it 'can parse a single strong etag' do
7
+ header = Xenon::Headers::IfNoneMatch.parse('"xyzzy"')
8
+ expect(header.etags.size).to eq(1)
9
+ expect(header.etags[0]).to eq(Xenon::ETag.new('xyzzy'))
10
+ end
11
+
12
+ it 'can parse a single weak etag' do
13
+ header = Xenon::Headers::IfNoneMatch.parse('W/"xyzzy"')
14
+ expect(header.etags.size).to eq(1)
15
+ expect(header.etags[0]).to eq(Xenon::ETag.new('xyzzy', weak: true))
16
+ end
17
+
18
+ it 'can parse multiple etags' do
19
+ header = Xenon::Headers::IfNoneMatch.parse('"xyzzy", W/"r2d2xxxx", "c3piozzzz"')
20
+ expect(header.etags.size).to eq(3)
21
+ expect(header.etags[0]).to eq(Xenon::ETag.new('xyzzy'))
22
+ expect(header.etags[1]).to eq(Xenon::ETag.new('r2d2xxxx', weak: true))
23
+ expect(header.etags[2]).to eq(Xenon::ETag.new('c3piozzzz'))
24
+ end
25
+
26
+ it 'can parse a wildcard header' do
27
+ header = Xenon::Headers::IfNoneMatch.parse('*')
28
+ expect(header.etags.size).to eq(0)
29
+ end
30
+ end
31
+
32
+ context '#merge' do
33
+ it 'can merge two headers and maintain etag order' do
34
+ h1 = Xenon::Headers::IfNoneMatch.parse('"xyzzy", W/"r2d2xxxx"')
35
+ h2 = Xenon::Headers::IfNoneMatch.parse('"c3piozzzz"')
36
+ header = h1.merge(h2)
37
+ expect(header.etags.size).to eq(3)
38
+ expect(header.etags[0]).to eq(Xenon::ETag.new('xyzzy'))
39
+ expect(header.etags[1]).to eq(Xenon::ETag.new('r2d2xxxx', weak: true))
40
+ expect(header.etags[2]).to eq(Xenon::ETag.new('c3piozzzz'))
41
+ end
42
+
43
+ it 'raises a protocol error when trying to merge into a wildcard header' do
44
+ h1 = Xenon::Headers::IfNoneMatch.parse('*')
45
+ h2 = Xenon::Headers::IfNoneMatch.parse('"c3piozzzz"')
46
+ expect { h1.merge(h2) }.to raise_error(Xenon::ProtocolError)
47
+ end
48
+
49
+ it 'raises a protocol error when trying to merge a wildcard into a header' do
50
+ h1 = Xenon::Headers::IfNoneMatch.parse('"xyzzy"')
51
+ h2 = Xenon::Headers::IfNoneMatch.parse('*')
52
+ expect { h1.merge(h2) }.to raise_error(Xenon::ProtocolError)
53
+ end
54
+
55
+ it 'raises a protocol error when trying to merge two wildcard headers' do
56
+ h1 = Xenon::Headers::IfNoneMatch.parse('*')
57
+ h2 = Xenon::Headers::IfNoneMatch.parse('*')
58
+ expect { h1.merge(h2) }.to raise_error(Xenon::ProtocolError)
59
+ end
60
+ end
61
+
62
+ context '#to_s' do
63
+ it 'returns the string representation a single etag' do
64
+ header = Xenon::Headers::IfNoneMatch.parse('"xyzzy"')
65
+ expect(header.to_s).to eq('"xyzzy"')
66
+ end
67
+
68
+ it 'returns the string representation of multiple etags' do
69
+ header = Xenon::Headers::IfNoneMatch.parse('"xyzzy", W/"r2d2xxxx", "c3piozzzz"')
70
+ expect(header.to_s).to eq('"xyzzy", W/"r2d2xxxx", "c3piozzzz"')
71
+ end
72
+
73
+ it 'returns the string representation of a wildcard header' do
74
+ header = Xenon::Headers::IfNoneMatch.wildcard
75
+ expect(header.to_s).to eq('*')
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,45 @@
1
+ require 'xenon/headers/if_range'
2
+
3
+ describe Xenon::Headers::IfRange do
4
+
5
+ context '::parse' do
6
+ it 'can parse an http date' do
7
+ header = Xenon::Headers::IfRange.parse('Sat, 29 Oct 1994 19:43:31 GMT')
8
+ expect(header.date).to eq(Time.utc(1994, 10, 29, 19, 43, 31))
9
+ end
10
+
11
+ it 'can parse an obsolete RFC 850 date' do
12
+ header = Xenon::Headers::IfRange.parse('Sunday, 06-Nov-94 08:49:37 GMT')
13
+ expect(header.date).to eq(Time.utc(1994, 11, 6, 8, 49, 37))
14
+ end
15
+
16
+ it 'can parse an obsolete asctime date' do
17
+ header = Xenon::Headers::IfRange.parse('Sun Nov 6 08:49:37 1994')
18
+ expect(header.date).to eq(Time.utc(1994, 11, 6, 8, 49, 37))
19
+ end
20
+
21
+ it 'can parse a strong etag' do
22
+ header = Xenon::Headers::IfRange.parse('"xyzzy"')
23
+ expect(header.etag).to_not be_nil
24
+ expect(header.etag.opaque_tag).to eq 'xyzzy'
25
+ expect(header.etag).to be_strong
26
+ end
27
+
28
+ it 'should raise a ProtocolError if the etag is weak' do
29
+ expect { Xenon::Headers::IfRange.parse('W/"xyzzy"') }.to raise_error Xenon::ProtocolError
30
+ end
31
+ end
32
+
33
+ context '#to_s' do
34
+ it 'returns the http date format for dates' do
35
+ header = Xenon::Headers::IfRange.new(Time.utc(1994, 10, 29, 19, 43, 31))
36
+ expect(header.to_s).to eq('Sat, 29 Oct 1994 19:43:31 GMT')
37
+ end
38
+
39
+ it 'returns the etag format for etags' do
40
+ header = Xenon::Headers::IfRange.new(Xenon::ETag.new('xyzzy'))
41
+ expect(header.to_s).to eq('"xyzzy"')
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,19 @@
1
+ require 'xenon/headers/if_unmodified_since'
2
+
3
+ describe Xenon::Headers::IfUnmodifiedSince do
4
+
5
+ context '::parse' do
6
+ it 'can parse an http date' do
7
+ header = Xenon::Headers::IfUnmodifiedSince.parse('Sat, 29 Oct 1994 19:43:31 GMT')
8
+ expect(header.date).to eq(Time.utc(1994, 10, 29, 19, 43, 31))
9
+ end
10
+ end
11
+
12
+ context '#to_s' do
13
+ it 'returns the http date format' do
14
+ header = Xenon::Headers::IfUnmodifiedSince.new(Time.utc(1994, 10, 29, 19, 43, 31))
15
+ expect(header.to_s).to eq('Sat, 29 Oct 1994 19:43:31 GMT')
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,67 @@
1
+ require 'xenon/headers/user_agent'
2
+
3
+ describe Xenon::Headers::UserAgent do
4
+
5
+ context '::parse' do
6
+ it 'can parse a user agent with a product name' do
7
+ header = Xenon::Headers::UserAgent.parse('Mozilla')
8
+ expect(header.products.size).to eq(1)
9
+ expect(header.products[0].name).to eq('Mozilla')
10
+ expect(header.products[0].version).to be_nil
11
+ expect(header.products[0].comment).to be_nil
12
+ end
13
+
14
+ it 'can parse a user agent with a product name and version' do
15
+ header = Xenon::Headers::UserAgent.parse('Mozilla/5.0')
16
+ expect(header.products.size).to eq(1)
17
+ expect(header.products[0].name).to eq('Mozilla')
18
+ expect(header.products[0].version).to eq('5.0')
19
+ expect(header.products[0].comment).to be_nil
20
+ end
21
+
22
+ it 'can parse a user agent with a product name and comment' do
23
+ header = Xenon::Headers::UserAgent.parse('Mozilla (Macintosh; Intel Mac OS X 10_10_2)')
24
+ expect(header.products.size).to eq(1)
25
+ expect(header.products[0].name).to eq('Mozilla')
26
+ expect(header.products[0].version).to be_nil
27
+ expect(header.products[0].comment).to eq('Macintosh; Intel Mac OS X 10_10_2')
28
+ end
29
+
30
+ it 'can parse a user agent with a product name, version and comment' do
31
+ header = Xenon::Headers::UserAgent.parse('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2)')
32
+ expect(header.products.size).to eq(1)
33
+ expect(header.products[0].name).to eq('Mozilla')
34
+ expect(header.products[0].version).to eq('5.0')
35
+ expect(header.products[0].comment).to eq('Macintosh; Intel Mac OS X 10_10_2')
36
+ end
37
+
38
+ it 'can parse a user agent with multiple comments' do
39
+ header = Xenon::Headers::UserAgent.parse('Mozilla/5.0 (Macintosh) (Intel Mac OS X 10_10_2)')
40
+ expect(header.products.size).to eq(2)
41
+ expect(header.products[0].name).to eq('Mozilla')
42
+ expect(header.products[0].version).to eq('5.0')
43
+ expect(header.products[0].comment).to eq('Macintosh')
44
+ expect(header.products[1].name).to be_nil
45
+ expect(header.products[1].version).to be_nil
46
+ expect(header.products[1].comment).to eq('Intel Mac OS X 10_10_2')
47
+ end
48
+
49
+ it 'can parse a typical Chrome user agent' do
50
+ header = Xenon::Headers::UserAgent.parse('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36')
51
+ expect(header.products.size).to eq(4)
52
+ expect(header.products[0].name).to eq('Mozilla')
53
+ expect(header.products[0].version).to eq('5.0')
54
+ expect(header.products[0].comment).to eq('Macintosh; Intel Mac OS X 10_10_2')
55
+ expect(header.products[1].name).to eq('AppleWebKit')
56
+ expect(header.products[1].version).to eq('537.36')
57
+ expect(header.products[1].comment).to eq('KHTML, like Gecko')
58
+ expect(header.products[2].name).to eq('Chrome')
59
+ expect(header.products[2].version).to eq('41.0.2272.89')
60
+ expect(header.products[2].comment).to be_nil
61
+ expect(header.products[3].name).to eq('Safari')
62
+ expect(header.products[3].version).to eq('537.36')
63
+ expect(header.products[3].comment).to be_nil
64
+ end
65
+ end
66
+
67
+ end