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,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