xenon-routing 0.0.4

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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +18 -0
  3. data/.gitignore +25 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +20 -0
  7. data/Guardfile +16 -0
  8. data/LICENSE +22 -0
  9. data/README.md +116 -0
  10. data/Rakefile +40 -0
  11. data/VERSION +1 -0
  12. data/examples/hello_world/config.ru +3 -0
  13. data/examples/hello_world/hello_world.rb +27 -0
  14. data/xenon-http/lib/xenon/auth.rb +63 -0
  15. data/xenon-http/lib/xenon/errors.rb +5 -0
  16. data/xenon-http/lib/xenon/etag.rb +48 -0
  17. data/xenon-http/lib/xenon/headers.rb +112 -0
  18. data/xenon-http/lib/xenon/headers/accept.rb +34 -0
  19. data/xenon-http/lib/xenon/headers/accept_charset.rb +59 -0
  20. data/xenon-http/lib/xenon/headers/accept_encoding.rb +63 -0
  21. data/xenon-http/lib/xenon/headers/accept_language.rb +59 -0
  22. data/xenon-http/lib/xenon/headers/authorization.rb +50 -0
  23. data/xenon-http/lib/xenon/headers/cache_control.rb +56 -0
  24. data/xenon-http/lib/xenon/headers/content_type.rb +23 -0
  25. data/xenon-http/lib/xenon/headers/if_match.rb +53 -0
  26. data/xenon-http/lib/xenon/headers/if_modified_since.rb +22 -0
  27. data/xenon-http/lib/xenon/headers/if_none_match.rb +53 -0
  28. data/xenon-http/lib/xenon/headers/if_range.rb +45 -0
  29. data/xenon-http/lib/xenon/headers/if_unmodified_since.rb +22 -0
  30. data/xenon-http/lib/xenon/headers/user_agent.rb +65 -0
  31. data/xenon-http/lib/xenon/headers/www_authenticate.rb +71 -0
  32. data/xenon-http/lib/xenon/http.rb +7 -0
  33. data/xenon-http/lib/xenon/http_version.rb +3 -0
  34. data/xenon-http/lib/xenon/media_type.rb +162 -0
  35. data/xenon-http/lib/xenon/parsers/basic_rules.rb +86 -0
  36. data/xenon-http/lib/xenon/parsers/header_rules.rb +60 -0
  37. data/xenon-http/lib/xenon/parsers/media_type.rb +53 -0
  38. data/xenon-http/lib/xenon/quoted_string.rb +20 -0
  39. data/xenon-http/spec/spec_helper.rb +94 -0
  40. data/xenon-http/spec/xenon/etag_spec.rb +19 -0
  41. data/xenon-http/spec/xenon/headers/accept_charset_spec.rb +31 -0
  42. data/xenon-http/spec/xenon/headers/accept_encoding_spec.rb +40 -0
  43. data/xenon-http/spec/xenon/headers/accept_language_spec.rb +33 -0
  44. data/xenon-http/spec/xenon/headers/accept_spec.rb +54 -0
  45. data/xenon-http/spec/xenon/headers/authorization_spec.rb +47 -0
  46. data/xenon-http/spec/xenon/headers/cache_control_spec.rb +64 -0
  47. data/xenon-http/spec/xenon/headers/if_match_spec.rb +73 -0
  48. data/xenon-http/spec/xenon/headers/if_modified_since_spec.rb +19 -0
  49. data/xenon-http/spec/xenon/headers/if_none_match_spec.rb +79 -0
  50. data/xenon-http/spec/xenon/headers/if_range_spec.rb +45 -0
  51. data/xenon-http/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
  52. data/xenon-http/spec/xenon/headers/user_agent_spec.rb +67 -0
  53. data/xenon-http/spec/xenon/headers/www_authenticate_spec.rb +43 -0
  54. data/xenon-http/spec/xenon/media_type_spec.rb +267 -0
  55. data/xenon-http/xenon-http.gemspec +25 -0
  56. data/xenon-routing/lib/xenon/api.rb +118 -0
  57. data/xenon-routing/lib/xenon/marshallers.rb +48 -0
  58. data/xenon-routing/lib/xenon/request.rb +40 -0
  59. data/xenon-routing/lib/xenon/response.rb +29 -0
  60. data/xenon-routing/lib/xenon/routing.rb +6 -0
  61. data/xenon-routing/lib/xenon/routing/context.rb +35 -0
  62. data/xenon-routing/lib/xenon/routing/directives.rb +14 -0
  63. data/xenon-routing/lib/xenon/routing/header_directives.rb +32 -0
  64. data/xenon-routing/lib/xenon/routing/method_directives.rb +26 -0
  65. data/xenon-routing/lib/xenon/routing/param_directives.rb +22 -0
  66. data/xenon-routing/lib/xenon/routing/path_directives.rb +37 -0
  67. data/xenon-routing/lib/xenon/routing/route_directives.rb +51 -0
  68. data/xenon-routing/lib/xenon/routing/security_directives.rb +34 -0
  69. data/xenon-routing/lib/xenon/routing_version.rb +3 -0
  70. data/xenon-routing/spec/spec_helper.rb +94 -0
  71. data/xenon-routing/xenon-routing.gemspec +25 -0
  72. data/xenon.gemspec +26 -0
  73. metadata +145 -0
@@ -0,0 +1,43 @@
1
+ require 'xenon/headers/www_authenticate'
2
+
3
+ describe Xenon::Headers::WWWAuthenticate do
4
+
5
+ context '::parse' do
6
+
7
+ it 'can parse a Basic challenge with a realm' do
8
+ header = Xenon::Headers::WWWAuthenticate.parse('Basic realm="simple"')
9
+ expect(header.challenges.size).to eq(1)
10
+ expect(header.challenges[0].auth_scheme).to eq('Basic')
11
+ expect(header.challenges[0].realm).to eq('simple')
12
+ end
13
+
14
+ it 'can parse a Digest challenge with a realm' do
15
+ header = Xenon::Headers::WWWAuthenticate.parse('Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"')
16
+ expect(header.challenges.size).to eq(1)
17
+ expect(header.challenges[0].auth_scheme).to eq('Digest')
18
+ expect(header.challenges[0].realm).to eq('testrealm@host.com')
19
+ expect(header.challenges[0].qop).to eq('auth,auth-int')
20
+ expect(header.challenges[0].nonce).to eq('dcd98b7102dd2f0e8b11d0f600bfb0c093')
21
+ expect(header.challenges[0].opaque).to eq('5ccc069c403ebaf9f0171e9517f40e41')
22
+ end
23
+
24
+ it 'can parse a custom challenge' do
25
+ header = Xenon::Headers::WWWAuthenticate.parse('Newauth realm="apps", type=1, title="Login to \"apps\""')
26
+ expect(header.challenges.size).to eq(1)
27
+ expect(header.challenges[0].auth_scheme).to eq('Newauth')
28
+ expect(header.challenges[0].realm).to eq('apps')
29
+ expect(header.challenges[0].type).to eq('1')
30
+ expect(header.challenges[0].title).to eq('Login to "apps"')
31
+ end
32
+
33
+ it 'can parse multiple challenges' do
34
+ header = Xenon::Headers::WWWAuthenticate.parse('Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41", Basic realm="simple", Newauth realm="apps", type=1, title="Login to \"apps\""')
35
+ expect(header.challenges.size).to eq(3)
36
+ expect(header.challenges[0].auth_scheme).to eq('Digest')
37
+ expect(header.challenges[1].auth_scheme).to eq('Basic')
38
+ expect(header.challenges[2].auth_scheme).to eq('Newauth')
39
+ end
40
+
41
+
42
+ end
43
+ end
@@ -0,0 +1,267 @@
1
+ require 'xenon/media_type'
2
+
3
+ describe Xenon::MediaType do
4
+
5
+ context '::parse' do
6
+ it 'can parse basic media types' do
7
+ mt = Xenon::MediaType.parse('application/json')
8
+ expect(mt.type).to eq('application')
9
+ expect(mt.subtype).to eq('json')
10
+ end
11
+ it 'can parse media types with a subtype suffix' do
12
+ mt = Xenon::MediaType.parse('application/rss+xml')
13
+ expect(mt.type).to eq('application')
14
+ expect(mt.subtype).to eq('rss+xml')
15
+ end
16
+
17
+ it 'can parse media types with parameters' do
18
+ mt = Xenon::MediaType.parse('text/plain; format=flowed; paged')
19
+ expect(mt.type).to eq('text')
20
+ expect(mt.subtype).to eq('plain')
21
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
22
+ end
23
+
24
+ it 'strips whitespace around separators' do
25
+ mt = Xenon::MediaType.parse('text/plain ; format = flowed ; paged')
26
+ expect(mt.type).to eq('text')
27
+ expect(mt.subtype).to eq('plain')
28
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
29
+ end
30
+
31
+ it 'raises an error when the media type contains wildcards' do
32
+ expect { Xenon::MediaType.parse('*/*') }.to raise_error(Xenon::ParseError)
33
+ expect { Xenon::MediaType.parse('application/*') }.to raise_error(Xenon::ParseError)
34
+ end
35
+
36
+ it 'raises an error when the media type is invalid' do
37
+ expect { Xenon::MediaType.parse('application') }.to raise_error(Xenon::ParseError)
38
+ expect { Xenon::MediaType.parse('application; foo=bar') }.to raise_error(Xenon::ParseError)
39
+ expect { Xenon::MediaType.parse('/json') }.to raise_error(Xenon::ParseError)
40
+ expect { Xenon::MediaType.parse('/json; foo=bar') }.to raise_error(Xenon::ParseError)
41
+ end
42
+ end
43
+
44
+ %w(application audio image message multipart text video).each do |type|
45
+ context "#{type}?" do
46
+ it "returns true when the root type is '#{type}'" do
47
+ mt = Xenon::MediaType.new(type, 'dummy')
48
+ expect(mt.send("#{type}?")).to eq(true)
49
+ end
50
+
51
+ it "returns false when the root type is not '#{type}'" do
52
+ mt = Xenon::MediaType.new('dummy', 'dummy')
53
+ expect(mt.send("#{type}?")).to eq(false)
54
+ end
55
+ end
56
+ end
57
+
58
+ { experimental?: 'x', personal?: 'prs', vendor?: 'vnd' }.each do |method, prefix|
59
+ context method do
60
+ it "returns true when the subtype starts with '#{prefix}.'" do
61
+ mt = Xenon::MediaType.new('application', "#{prefix}.dummy")
62
+ expect(mt.send(method)).to eq(true)
63
+ end
64
+
65
+ it "returns false when the subtype does not start with '#{prefix}.'" do
66
+ mt = Xenon::MediaType.new('application', "dummy.dummy")
67
+ expect(mt.send(method)).to eq(false)
68
+ end
69
+ end
70
+ end
71
+
72
+ %w(ber der fastinfoset json wbxml xml zip).each do |format|
73
+ context "#{format}?" do
74
+ it "returns true when the subtype is '#{format}'" do
75
+ mt = Xenon::MediaType.new('application', format)
76
+ expect(mt.send("#{format}?")).to eq(true)
77
+ end
78
+
79
+ it "returns true when the subtype ends with '+#{format}'" do
80
+ mt = Xenon::MediaType.new('application', "dummy+#{format}")
81
+ expect(mt.send("#{format}?")).to eq(true)
82
+ end
83
+
84
+ it "returns false when the subtype is not '#{format}' and does not end with '+#{format}'" do
85
+ mt = Xenon::MediaType.new('dummy', 'dummy+dummy')
86
+ expect(mt.send("#{format}?")).to eq(false)
87
+ end
88
+ end
89
+ end
90
+
91
+ context '#to_s' do
92
+ it 'returns the string representation of a media type' do
93
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
94
+ expect(mt.to_s).to eq('text/plain; format=flowed; paged')
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ describe Xenon::MediaRange do
101
+
102
+ context '::parse' do
103
+ it 'can parse basic media ranges' do
104
+ mt = Xenon::MediaRange.parse('application/json')
105
+ expect(mt.type).to eq('application')
106
+ expect(mt.subtype).to eq('json')
107
+ end
108
+
109
+ it 'can parse media ranges with parameters' do
110
+ mt = Xenon::MediaRange.parse('text/plain; format=flowed; paged')
111
+ expect(mt.type).to eq('text')
112
+ expect(mt.subtype).to eq('plain')
113
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
114
+ end
115
+
116
+ it 'strips whitespace around separators' do
117
+ mt = Xenon::MediaRange.parse('text/plain ; format = flowed ; paged')
118
+ expect(mt.type).to eq('text')
119
+ expect(mt.subtype).to eq('plain')
120
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
121
+ end
122
+
123
+ it 'can parse media ranges with subtype wildcards' do
124
+ mt = Xenon::MediaRange.parse('application/*')
125
+ expect(mt.type).to eq('application')
126
+ expect(mt.subtype).to eq('*')
127
+ end
128
+
129
+ it 'can parse media ranges with type and subtype wildcards' do
130
+ mt = Xenon::MediaRange.parse('*/*')
131
+ expect(mt.type).to eq('*')
132
+ expect(mt.subtype).to eq('*')
133
+ end
134
+
135
+ it 'extracts q from the parameters' do
136
+ mt = Xenon::MediaRange.parse('text/plain; q=0.8; format=flowed; paged')
137
+ expect(mt.type).to eq('text')
138
+ expect(mt.subtype).to eq('plain')
139
+ expect(mt.q).to eq(0.8)
140
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
141
+ end
142
+
143
+ it 'uses the default value for q if the value is not numeric' do
144
+ mt = Xenon::MediaRange.parse('application/json; q=foo')
145
+ expect(mt.type).to eq('application')
146
+ expect(mt.subtype).to eq('json')
147
+ expect(mt.q).to eq(Xenon::MediaRange::DEFAULT_Q)
148
+ end
149
+
150
+ it 'raises an error when the media range is invalid' do
151
+ expect { Xenon::MediaRange.parse('application') }.to raise_error(Xenon::ParseError)
152
+ expect { Xenon::MediaRange.parse('application; foo=bar') }.to raise_error(Xenon::ParseError)
153
+ expect { Xenon::MediaRange.parse('*/json') }.to raise_error(Xenon::ParseError)
154
+ expect { Xenon::MediaRange.parse('/json') }.to raise_error(Xenon::ParseError)
155
+ expect { Xenon::MediaRange.parse('/json; foo=bar') }.to raise_error(Xenon::ParseError)
156
+ end
157
+ end
158
+
159
+ context '#<=>' do
160
+ it 'considers a wildcard type less than a regular type' do
161
+ mr1 = Xenon::MediaRange.new('*', '*')
162
+ mr2 = Xenon::MediaRange.new('text', '*')
163
+ expect(mr1 <=> mr2).to eq(-1)
164
+ end
165
+
166
+ it 'considers a wildcard subtype less than a regular subtype' do
167
+ mr1 = Xenon::MediaRange.new('application', '*')
168
+ mr2 = Xenon::MediaRange.new('text', 'plain')
169
+ expect(mr1 <=> mr2).to eq(-1)
170
+ end
171
+
172
+ it 'considers media ranges with type and subtype equal' do
173
+ mr1 = Xenon::MediaRange.new('application', 'json')
174
+ mr2 = Xenon::MediaRange.new('text', 'plain')
175
+ expect(mr1 <=> mr2).to eq(0)
176
+ end
177
+
178
+ it 'considers a media range with parameters greater than one without' do
179
+ mr1 = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
180
+ mr2 = Xenon::MediaRange.new('text', 'plain')
181
+ expect(mr1 <=> mr2).to eq(1)
182
+ end
183
+
184
+ it 'does not consider the quality when one media range is more specific' do
185
+ mr1 = Xenon::MediaRange.new('application', '*', 'q' => '0.3')
186
+ mr2 = Xenon::MediaRange.new('*', '*', 'q' => '0.5')
187
+ expect(mr1 <=> mr2).to eq(1)
188
+ end
189
+
190
+ it 'considers the quality when media ranges are equally specific' do
191
+ mr1 = Xenon::MediaRange.new('application', 'json', 'q' => '0.8')
192
+ mr2 = Xenon::MediaRange.new('application', 'xml')
193
+ expect(mr1 <=> mr2).to eq(-1)
194
+ end
195
+ end
196
+
197
+ %i(=~ ===).each do |name|
198
+ context "##{name}" do
199
+ it 'returns true when the type and subtype are wildcards' do
200
+ mr = Xenon::MediaRange.new('*', '*')
201
+ mt = Xenon::MediaType.new('application', 'json')
202
+ expect(mr.send(name, mt)).to eq(true)
203
+ end
204
+
205
+ it 'returns true when the type matches and subtype is a wildcard' do
206
+ mr = Xenon::MediaRange.new('application', '*')
207
+ mt = Xenon::MediaType.new('application', 'json')
208
+ expect(mr.send(name, mt)).to eq(true)
209
+ end
210
+
211
+ it 'returns true when the type and subtype match exactly' do
212
+ mr = Xenon::MediaRange.new('application', 'json')
213
+ mt = Xenon::MediaType.new('application', 'json')
214
+ expect(mr.send(name, mt)).to eq(true)
215
+ end
216
+
217
+ it 'returns true when the type, subtype and parameters match exactly' do
218
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
219
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
220
+ expect(mr.send(name, mt)).to eq(true)
221
+ end
222
+
223
+ it 'returns true when the the media type has more specific parameters' do
224
+ mr = Xenon::MediaRange.new('text', 'plain')
225
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
226
+ expect(mr.send(name, mt)).to eq(true)
227
+ end
228
+
229
+ it 'returns false when the type is different' do
230
+ mr = Xenon::MediaRange.new('text', 'json')
231
+ mt = Xenon::MediaType.new('application', 'json')
232
+ expect(mr.send(name, mt)).to eq(false)
233
+ end
234
+
235
+ it 'returns false when the type matches but subtype is different' do
236
+ mr = Xenon::MediaRange.new('application', 'xml')
237
+ mt = Xenon::MediaType.new('application', 'json')
238
+ expect(mr.send(name, mt)).to eq(false)
239
+ end
240
+
241
+ it 'returns false when the media range has more specific parameters' do
242
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
243
+ mt = Xenon::MediaType.new('text', 'plain')
244
+ expect(mr.send(name, mt)).to eq(false)
245
+ end
246
+
247
+ it 'returns false when the media range has a different parameter value' do
248
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
249
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'linear')
250
+ expect(mr.send(name, mt)).to eq(false)
251
+ end
252
+ end
253
+ end
254
+
255
+ context '#to_s' do
256
+ it 'returns the string representation of a media range' do
257
+ mt = Xenon::MediaRange.new('text', 'plain', 'q' => 0.8, 'format' => 'flowed', 'paged' => nil)
258
+ expect(mt.to_s).to eq('text/plain; format=flowed; paged; q=0.8')
259
+ end
260
+
261
+ it 'omits the q parameter when it is 1.0' do
262
+ mt = Xenon::MediaRange.new('application', 'json', 'q' => 1.0)
263
+ expect(mt.to_s).to eq('application/json')
264
+ end
265
+ end
266
+
267
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'xenon/http_version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'xenon-http'
8
+ spec.version = Xenon::HTTP_VERSION
9
+ spec.authors = ['Greg Beech']
10
+ spec.email = ['greg@gregbeech.com']
11
+ spec.summary = %q{An HTTP framework for building RESTful APIs.}
12
+ spec.description = %q{Provides a model for the HTTP protocol and a tree-based routing syntax.}
13
+ spec.homepage = 'https://github.com/gregbeech/xenon'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^xenon-http/bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^xenon-http/(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.required_ruby_version = '>= 2.2.0'
22
+
23
+ spec.add_runtime_dependency 'activesupport', '~> 4.0'
24
+ spec.add_runtime_dependency 'parslet', '~> 1.7'
25
+ end
@@ -0,0 +1,118 @@
1
+ require 'json'
2
+ require 'rack'
3
+ require 'xenon/routing'
4
+
5
+ module Xenon
6
+ class API
7
+ include Xenon::Routing::Directives
8
+
9
+ DEFAULT_MARSHALLERS = [JsonMarshaller.new]
10
+
11
+ class << self
12
+ def marshallers(*marshallers)
13
+ @marshallers = marshallers unless marshallers.nil? || marshallers.empty?
14
+ (@marshallers.nil? || @marshallers.empty?) ? DEFAULT_MARSHALLERS : @marshallers
15
+ end
16
+
17
+ def select_marshaller(media_ranges)
18
+ weighted = marshallers.map do |marshaller|
19
+ media_range = media_ranges.find { |media_range| marshaller.marshal_to?(media_range) }
20
+ [marshaller, media_range ? media_range.q : 0]
21
+ end
22
+ weighted.select { |_, q| q > 0 }.sort_by { |_, q| q }.map { |m, _| m }.last
23
+ end
24
+ end
25
+
26
+ attr_reader :context
27
+
28
+ class << self
29
+ def routes
30
+ @routes ||= []
31
+ end
32
+
33
+ def method_missing(name, *args, &block)
34
+ if instance_methods.include?(name)
35
+ routes << [name, args, block]
36
+ else
37
+ super
38
+ end
39
+ end
40
+ end
41
+
42
+ def call(env)
43
+ dup.call!(env)
44
+ end
45
+
46
+ def call!(env)
47
+ @context = Routing::Context.new(Request.new(Rack::Request.new(env)), Response.new)
48
+
49
+ accept = @context.request.header('Accept')
50
+ marshaller = accept ? self.class.select_marshaller(accept.media_ranges) : self.class.marshallers.first
51
+
52
+ catch (:complete) do
53
+ begin
54
+ if marshaller
55
+ self.class.routes.each do |route|
56
+ name, args, block = route
57
+ route_block = proc { instance_eval(&block) }
58
+ send(name, *args, &route_block)
59
+ end
60
+ else
61
+ reject :accept, supported: self.class.marshallers.map(&:media_type)
62
+ end
63
+ handle_rejections(@context.rejections)
64
+ rescue => e
65
+ handle_error(e)
66
+ end
67
+ end
68
+
69
+ marshaller ||= self.class.marshallers.first
70
+ resp = @context.response.copy(
71
+ headers: @context.response.headers.set(Headers::ContentType.new(marshaller.content_type)),
72
+ body: marshaller.marshal(@context.response.body))
73
+ [resp.status, resp.headers.map { |h| [h.name, h.to_s] }.to_h, resp.body]
74
+ end
75
+
76
+ def handle_error(e)
77
+ puts "handle_error: #{e.class}: #{e}\n #{e.backtrace.join("\n ")}"
78
+ case e
79
+ when ParseError
80
+ fail 400, e.message
81
+ else
82
+ fail 500, e.message # TODO: Only if verbose errors configured
83
+ end
84
+ end
85
+
86
+ def handle_rejections(rejections)
87
+ puts "handle_rejections: #{rejections}"
88
+ if rejections.empty?
89
+ fail 404
90
+ else
91
+ rejection = rejections.first
92
+ case rejection.reason
93
+ when :accept
94
+ fail 406, "Supported media types: #{rejection[:supported].join(", ")}"
95
+ when :forbidden
96
+ fail 403
97
+ when :header
98
+ fail 400, "Missing required header: #{rejection[:required]}"
99
+ when :method
100
+ supported = rejections.take_while { |r| r.reason == :method }.map { |r| r[:supported].upcase }
101
+ fail 405, "Supported methods: #{supported.join(", ")}"
102
+ when :unauthorized
103
+ if rejection[:scheme]
104
+ challenge = Headers::Challenge.new(rejection[:scheme], rejection.info.except(:scheme))
105
+ respond_with_header Headers::WWWAuthenticate.new(challenge) do
106
+ fail 401
107
+ end
108
+ else
109
+ fail 401
110
+ end
111
+ else
112
+ fail 500
113
+ end
114
+ end
115
+ end
116
+
117
+ end
118
+ end