response 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +16 -0
  4. data/Changelog.md +3 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.devtools +60 -0
  7. data/Guardfile +18 -0
  8. data/LICENSE +20 -0
  9. data/README.md +39 -0
  10. data/Rakefile +2 -0
  11. data/TODO +2 -0
  12. data/config/devtools.yml +2 -0
  13. data/config/flay.yml +3 -0
  14. data/config/flog.yml +2 -0
  15. data/config/mutant.yml +3 -0
  16. data/config/reek.yml +92 -0
  17. data/config/roodi.yml +18 -0
  18. data/config/yardstick.yml +2 -0
  19. data/lib/response/html.rb +36 -0
  20. data/lib/response/json.rb +36 -0
  21. data/lib/response/redirect.rb +52 -0
  22. data/lib/response/status.rb +28 -0
  23. data/lib/response/text.rb +37 -0
  24. data/lib/response/xml.rb +36 -0
  25. data/lib/response.rb +271 -0
  26. data/response.gemspec +22 -0
  27. data/spec/rcov.opts +7 -0
  28. data/spec/shared/functional_command_method_behavior.rb +11 -0
  29. data/spec/spec_helper.rb +8 -0
  30. data/spec/unit/response/body_spec.rb +22 -0
  31. data/spec/unit/response/cache_control_spec.rb +25 -0
  32. data/spec/unit/response/class_methods/build_spec.rb +31 -0
  33. data/spec/unit/response/content_type_spec.rb +26 -0
  34. data/spec/unit/response/headers_spec.rb +23 -0
  35. data/spec/unit/response/html/class_methods/build_spec.rb +18 -0
  36. data/spec/unit/response/json/class_methods/build_spec.rb +18 -0
  37. data/spec/unit/response/last_modified_spec.rb +25 -0
  38. data/spec/unit/response/merge_headers_spec.rb +20 -0
  39. data/spec/unit/response/rack_array_spec.rb +15 -0
  40. data/spec/unit/response/redirect/class_methods/build_spec.rb +28 -0
  41. data/spec/unit/response/status_spec.rb +22 -0
  42. data/spec/unit/response/text/class_methods/build_spec.rb +18 -0
  43. data/spec/unit/response/to_rack_response_spec.rb +26 -0
  44. data/spec/unit/response/valid_predicate_spec.rb +37 -0
  45. data/spec/unit/response/with_body_spec.rb +17 -0
  46. data/spec/unit/response/with_headers_spec.rb +17 -0
  47. data/spec/unit/response/with_status_spec.rb +17 -0
  48. data/spec/unit/response/xml/class_methods/build_spec.rb +18 -0
  49. data/tasks/metrics/ci.rake +7 -0
  50. data/tasks/metrics/flay.rake +47 -0
  51. data/tasks/metrics/flog.rake +43 -0
  52. data/tasks/metrics/heckle.rake +208 -0
  53. data/tasks/metrics/metric_fu.rake +29 -0
  54. data/tasks/metrics/reek.rake +15 -0
  55. data/tasks/metrics/roodi.rake +15 -0
  56. data/tasks/metrics/yardstick.rake +23 -0
  57. data/tasks/spec.rake +45 -0
  58. data/tasks/yard.rake +9 -0
  59. metadata +187 -0
data/lib/response.rb ADDED
@@ -0,0 +1,271 @@
1
+ #encoding: utf-8
2
+ require 'time'
3
+ require 'ice_nine'
4
+ require 'adamantium'
5
+ require 'concord'
6
+ require 'abstract_type'
7
+ require 'equalizer'
8
+
9
+ # Library to build rack compatible responses in a functional style
10
+ class Response
11
+ include Adamantium::Flat, Concord.new(:status, :headers, :body)
12
+
13
+ # Error raised when finalizing responses with undefined components
14
+ class InvalidError < RuntimeError; end
15
+
16
+ TEXT_PLAIN = 'text/plain; charset=UTF-8'.freeze
17
+
18
+ # Undefined response component
19
+ #
20
+ # A class to get nice #inspect behavior ootb
21
+ #
22
+ Undefined = Class.new.freeze
23
+
24
+ # Return status code
25
+ #
26
+ # @return [Response::Status]
27
+ # if set
28
+ #
29
+ # @return [undefined]
30
+ # otherwise
31
+ #
32
+ # @api private
33
+ #
34
+ attr_reader :status
35
+
36
+ # Return headers code
37
+ #
38
+ # @return [Hash]
39
+ # if set
40
+ #
41
+ # @return [undefined]
42
+ # otherwise
43
+ #
44
+ # @api private
45
+ #
46
+ attr_reader :headers
47
+
48
+ # Return status code
49
+ #
50
+ # @return [#each]
51
+ # if set
52
+ #
53
+ # @return [undefined]
54
+ # otherwise
55
+ #
56
+ # @api private
57
+ #
58
+ attr_reader :body
59
+
60
+ # Return response with new status
61
+ #
62
+ # @param [Fixnum] status
63
+ # the status for the response
64
+ #
65
+ # @return [Response]
66
+ # new response object with status set
67
+ #
68
+ # @example
69
+ #
70
+ # response = Response.new
71
+ # response = response.with_status(200)
72
+ # response.status # => 200
73
+ #
74
+ # @api public
75
+ #
76
+ def with_status(status)
77
+ self.class.new(status, headers, body)
78
+ end
79
+
80
+ # Return response with new body
81
+ #
82
+ # @param [Object] body
83
+ # the body for the response
84
+ #
85
+ # @return [Response]
86
+ # new response with body set
87
+ #
88
+ # @example
89
+ #
90
+ # response = Response.new
91
+ # response = response.with_body('Hello')
92
+ # response.body # => 'Hello'
93
+ #
94
+ # @api public
95
+ #
96
+ def with_body(body)
97
+ self.class.new(status, headers, body)
98
+ end
99
+
100
+ # Return response with new headers
101
+ #
102
+ # @param [Hash] headers
103
+ # the header for the response
104
+ #
105
+ # @return [Response]
106
+ # new response with headers set
107
+ #
108
+ # @example
109
+ #
110
+ # response = Response.new
111
+ # response = response.with_header({'Foo' => 'Bar'})
112
+ # response.headers # => {'Foo' => 'Bar'}
113
+ #
114
+ # @api public
115
+ #
116
+ def with_headers(headers)
117
+ self.class.new(status, headers, body)
118
+ end
119
+
120
+ # Return response with merged headers
121
+ #
122
+ # @param [Hash] headers
123
+ # the headers to merge
124
+ #
125
+ # @example
126
+ #
127
+ # response = Response.new(200, {'Foo' => 'Baz', 'John' => 'Doe'})
128
+ # response = response.merge_header({'Foo' => 'Bar'})
129
+ # response.headers # => {'Foo' => 'Bar', 'John' => 'Doe'}
130
+ #
131
+ # @api public
132
+ #
133
+ # @return [Response]
134
+ # returns new response with merged header
135
+ #
136
+ def merge_headers(headers)
137
+ self.class.new(status, self.headers.merge(headers), body)
138
+ end
139
+
140
+ # Return rack compatible array after asserting response is valid
141
+ #
142
+ # @return [Array]
143
+ # rack compatible array
144
+ #
145
+ # @raise InvalidError
146
+ # raises InvalidError when request containts undefined components
147
+ #
148
+ # @api private
149
+ #
150
+ def to_rack_response
151
+ assert_valid
152
+ rack_array
153
+ end
154
+
155
+ # Test if object is a valid response
156
+ #
157
+ # @return [true]
158
+ # if all required fields are present
159
+ #
160
+ # @return [false]
161
+ # otherwise
162
+ #
163
+ # @api private
164
+ #
165
+ def valid?
166
+ ![status, headers, body].any? { |item| item.equal?(Undefined) }
167
+ end
168
+ memoize :valid?
169
+
170
+ # Return rack compatible array
171
+ #
172
+ # @return [Array]
173
+ # rack compatible array
174
+ #
175
+ # @example
176
+ #
177
+ # response = Response.new(200, {'Foo' => 'Bar'}, 'Hello World')
178
+ # response.rack_array # => [200, {'Foo' => 'Bar'}, 'Hello World']
179
+ #
180
+ # @api public
181
+ #
182
+ def rack_array
183
+ [status.code, headers, body]
184
+ end
185
+ memoize :rack_array
186
+
187
+ # Return contents of content type header
188
+ #
189
+ # @return [String]
190
+ #
191
+ # @api private
192
+ #
193
+ def content_type
194
+ headers['Content-Type']
195
+ end
196
+
197
+ # Return last modified
198
+ #
199
+ # @return [Time]
200
+ # if last modified header is present
201
+ #
202
+ # @return [nil]
203
+ # otherwise
204
+ #
205
+ # @api private
206
+ #
207
+ def last_modified
208
+ value = headers.fetch('Last-Modified') { return }
209
+ Time.httpdate(value)
210
+ end
211
+ memoize :last_modified
212
+
213
+ # Return contents of cache control header
214
+ #
215
+ # @return [String]
216
+ #
217
+ # @api private
218
+ #
219
+ def cache_control
220
+ headers['Cache-Control']
221
+ end
222
+
223
+ # Build response with a dsl like interface
224
+ #
225
+ # @example
226
+ #
227
+ # response = Response.build(200) do |response|
228
+ # response.
229
+ # with_headers('Foo' => 'Bar').
230
+ # with_body('Hello')
231
+ # end
232
+ #
233
+ # response.status # => 200
234
+ # response.headers # => {'Foo' => 'Bar' }
235
+ # response.body # => 'Hello'
236
+ #
237
+ # @return [Array]
238
+ # rack compatible array
239
+ #
240
+ # @api public
241
+ #
242
+ def self.build(status = Undefined, headers = {}, body = Undefined)
243
+ response = new(status, headers, body)
244
+ response = yield response if block_given?
245
+ response
246
+ end
247
+
248
+ private
249
+
250
+ # Raise error when request containts undefined components
251
+ #
252
+ # @raise InvalidError
253
+ # raises InvalidError when request containts undefined components
254
+ #
255
+ # @return [undefined]
256
+ #
257
+ # @api private
258
+ #
259
+ def assert_valid
260
+ unless valid?
261
+ raise InvalidError, "Not a valid response: #{self.inspect}"
262
+ end
263
+ end
264
+ end
265
+
266
+ require 'response/status'
267
+ require 'response/redirect'
268
+ require 'response/html'
269
+ require 'response/xml'
270
+ require 'response/json'
271
+ require 'response/text'
data/response.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'response'
5
+ gem.version = '0.0.2'
6
+ gem.authors = [ 'Markus Schirp' ]
7
+ gem.email = [ 'mbj@seonic.net' ]
8
+ gem.description = 'Build rack responses with functional style'
9
+ gem.summary = gem.description
10
+ gem.homepage = 'https://github.com/mbj/response'
11
+
12
+ gem.require_paths = [ 'lib' ]
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
15
+ gem.extra_rdoc_files = %w[LICENSE README.md TODO]
16
+
17
+ gem.add_dependency('ice_nine', '~> 0.7.0')
18
+ gem.add_dependency('adamantium', '~> 0.0.7')
19
+ gem.add_dependency('equalizer', '~> 0.0.5')
20
+ gem.add_dependency('abstract_type', '~> 0.0.5')
21
+ gem.add_dependency('concord', '~> 0.1.0')
22
+ end
data/spec/rcov.opts ADDED
@@ -0,0 +1,7 @@
1
+ --exclude-only "spec/,^/"
2
+ --sort coverage
3
+ --callsites
4
+ --xrefs
5
+ --profile
6
+ --text-summary
7
+ --failure-threshold 100
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ shared_examples_for 'a functional command method' do
4
+ it 'returns object of described_class' do
5
+ should be_kind_of(described_class)
6
+ end
7
+
8
+ it 'should return a new object' do
9
+ should_not be(object)
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ require 'response'
4
+ require 'devtools'
5
+ Devtools.init_spec_helper
6
+
7
+ RSpec.configure do |config|
8
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#body' do
4
+
5
+ subject { object.body }
6
+
7
+ context 'when unset' do
8
+ let(:object) { described_class.build }
9
+
10
+ it { should be(Response::Undefined) }
11
+ end
12
+
13
+ context 'when set' do
14
+ let(:body) { mock('Body') }
15
+
16
+ let(:object) { described_class.build.with_body(body) }
17
+
18
+ it { should be(body) }
19
+
20
+ it_should_behave_like 'an idempotent method'
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#cache_control' do
4
+ subject { object.cache_control }
5
+
6
+ let(:object) { Response.build.with_headers(header) }
7
+
8
+ context 'when Cache-Control header is present' do
9
+ let(:header) { { 'Cache-Control' => value } }
10
+
11
+ let(:value) { mock('Value') }
12
+
13
+ it { should be(value) }
14
+
15
+ it_should_behave_like 'an idempotent method'
16
+ end
17
+
18
+ context 'when Cache-Control header is not present' do
19
+ let(:header) { {} }
20
+
21
+ it { should be(nil) }
22
+
23
+ it_should_behave_like 'an idempotent method'
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '.build' do
4
+ subject { object.build(&block) }
5
+
6
+ let(:object) { described_class }
7
+
8
+ context 'without arguments and block' do
9
+ let(:block) { nil }
10
+
11
+ its(:valid?) { should be(false) }
12
+ its(:headers) { should eql({}) }
13
+ its(:status) { should be(Response::Undefined) }
14
+ its(:body) { should be(Response::Undefined) }
15
+ end
16
+
17
+ context 'with block' do
18
+ let(:yields) { [] }
19
+ let(:value) { mock('Value') }
20
+ let(:block) { lambda { |response| yields << response; value } }
21
+
22
+ it 'should return value from block' do
23
+ should be(value)
24
+ end
25
+
26
+ it 'should call block with response' do
27
+ subject
28
+ yields.should eql([Response.build])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#content_type' do
4
+ subject { object.content_type }
5
+
6
+ let(:object) { Response.build.with_headers(header) }
7
+
8
+ context 'when Content-Type header is present' do
9
+ let(:header) { { 'Content-Type' => value } }
10
+
11
+ let(:value) { mock('Value') }
12
+
13
+ it { should be(value) }
14
+
15
+ it_should_behave_like 'an idempotent method'
16
+ end
17
+
18
+ context 'when Content-Type header is not present' do
19
+ let(:header) { {} }
20
+
21
+ it { should be(nil) }
22
+
23
+ it_should_behave_like 'an idempotent method'
24
+ end
25
+ end
26
+
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#headers' do
4
+
5
+ subject { object.headers }
6
+
7
+ context 'when unset' do
8
+ let(:object) { described_class.build }
9
+
10
+ it { should eql({}) }
11
+ end
12
+
13
+ context 'when set' do
14
+ let(:headers) { mock('Headers') }
15
+
16
+ let(:object) { described_class.build.with_headers(headers) }
17
+
18
+ it { should be(headers) }
19
+
20
+ it_should_behave_like 'an idempotent method'
21
+ end
22
+
23
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response::HTML, '.build' do
4
+ subject { object.build(body) }
5
+
6
+ let(:body) { mock('Body') }
7
+ let(:object) { described_class }
8
+
9
+ its(:status) { should be(Response::Status::OK) }
10
+ its(:body) { should be(body) }
11
+ its(:headers) { should eql('Content-Type' => 'text/html; charset=UTF-8') }
12
+
13
+ it 'allows to modify response' do
14
+ object.build(body) do |response|
15
+ response.with_status(Response::Status::NOT_FOUND)
16
+ end.status.should be(Response::Status::NOT_FOUND)
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response::JSON, '.build' do
4
+ subject { object.build(body) }
5
+
6
+ let(:body) { mock('Body') }
7
+ let(:object) { described_class }
8
+
9
+ its(:status) { should be(Response::Status::OK) }
10
+ its(:body) { should be(body) }
11
+ its(:headers) { should eql('Content-Type' => 'application/json; charset=UTF-8') }
12
+
13
+ it 'allows to modify response' do
14
+ object.build(body) do |response|
15
+ response.with_status(404)
16
+ end.status.should be(404)
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#last_modified' do
4
+ subject { object.last_modified }
5
+
6
+ let(:object) { Response.build.with_headers(header) }
7
+
8
+ let(:time) { Time.httpdate(Time.now.httpdate) }
9
+
10
+ context 'when Content-Type header is present' do
11
+ let(:header) { { 'Last-Modified' => time.httpdate } }
12
+
13
+ it { should eql(time) }
14
+
15
+ it_should_behave_like 'an idempotent method'
16
+ end
17
+
18
+ context 'when Content-Type header is not present' do
19
+ let(:header) { {} }
20
+
21
+ it { should be(nil) }
22
+
23
+ it_should_behave_like 'an idempotent method'
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#merge_headers' do
4
+ subject { object.merge_headers(update_headers) }
5
+
6
+ let(:object) { described_class.build(status, original_headers, body) }
7
+ let(:status) { Response::Status::OK }
8
+ let(:update_headers) { { 'Baz' => 'Zot' } }
9
+ let(:original_headers) { { 'Foo' => 'Bar' } }
10
+
11
+ let(:status) { mock('Status') }
12
+ let(:body) { mock('Body') }
13
+
14
+ its(:status) { should be(status) }
15
+ its(:body) { should be(body) }
16
+ its(:headers) { should eql('Foo' => 'Bar', 'Baz' => 'Zot') }
17
+
18
+ it_should_behave_like 'a functional command method'
19
+ end
20
+
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#rack_array' do
4
+ subject { object.rack_array }
5
+
6
+ let(:status) { Response::Status::OK }
7
+ let(:headers) { mock('Headers') }
8
+ let(:body) { mock('Body') }
9
+
10
+ let(:object) { described_class.build(status, headers, body) }
11
+
12
+ it { should eql([200, headers, body]) }
13
+
14
+ it_should_behave_like 'an idempotent method'
15
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response::Redirect::Found, '#build' do
4
+
5
+ subject { object.build(location) }
6
+
7
+ let(:object) { described_class }
8
+
9
+ let(:location) { mock('Location', :to_s => 'THE-LOCATION') }
10
+
11
+ its(:status) { should be(Response::Status::FOUND) }
12
+ its(:headers) { should eql('Location' => location, 'Content-Type' => 'text/plain; charset=UTF-8') }
13
+ its(:body) { should eql('You are beeing redirected to: THE-LOCATION') }
14
+ end
15
+
16
+ describe Response::Redirect::Permanent, '#build' do
17
+
18
+ subject { object.build(location) }
19
+
20
+ let(:object) { described_class }
21
+
22
+ let(:location) { mock('Location', :to_s => 'THE-LOCATION') }
23
+
24
+ its(:status) { should be(Response::Status::MOVED_PERMANENTLY) }
25
+ its(:headers) { should eql('Location' => location, 'Content-Type' => 'text/plain; charset=UTF-8') }
26
+ its(:body) { should eql('You are beeing redirected to: THE-LOCATION') }
27
+ end
28
+
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#status' do
4
+
5
+ subject { object.status }
6
+
7
+ context 'when unset' do
8
+ let(:object) { described_class.build }
9
+
10
+ it { should be(Response::Undefined) }
11
+ end
12
+
13
+ context 'when set' do
14
+ let(:status) { mock('Status') }
15
+
16
+ let(:object) { described_class.build.with_status(status) }
17
+
18
+ it { should be(status) }
19
+
20
+ it_should_behave_like 'an idempotent method'
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response::Text, '.build' do
4
+ subject { object.build(body) }
5
+
6
+ let(:body) { mock('Body') }
7
+ let(:object) { described_class }
8
+
9
+ its(:status) { should be(Response::Status::OK) }
10
+ its(:body) { should be(body) }
11
+ its(:headers) { should eql('Content-Type' => 'text/plain; charset=UTF-8') }
12
+
13
+ it 'allows to modify response' do
14
+ object.build(body) do |response|
15
+ response.with_status(Response::Status::NOT_FOUND)
16
+ end.status.should be(Response::Status::NOT_FOUND)
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#to_rack_response' do
4
+
5
+ subject { object.to_rack_response }
6
+
7
+ let(:status) { Response::Status::OK }
8
+ let(:headers) { mock('Headers') }
9
+ let(:body) { mock('Body') }
10
+
11
+ context 'with valid response' do
12
+ let(:object) { Response.build(status, headers, body) }
13
+
14
+ it { should eql([200, headers, body]) }
15
+
16
+ it_should_behave_like 'an idempotent method'
17
+ end
18
+
19
+ context 'with invalid response' do
20
+ let(:object) { Response.build }
21
+
22
+ it 'should raise error' do
23
+ expect { subject }.to raise_error(Response::InvalidError, "Not a valid response: #{object.inspect}")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Response, '#valid?' do
4
+ subject { object.valid? }
5
+
6
+ context 'with status and body' do
7
+ let(:object) { described_class.build(Response::Status::OK, {}, mock('Body')) }
8
+
9
+ it { should be(true) }
10
+
11
+ it_should_behave_like 'an idempotent method'
12
+ end
13
+
14
+ context 'without body' do
15
+ let(:object) { described_class.build.with_status(Response::Status::OK) }
16
+
17
+ it { should be(false) }
18
+
19
+ it_should_behave_like 'an idempotent method'
20
+ end
21
+
22
+ context 'without status' do
23
+ let(:object) { described_class.build.with_body(mock('Body')) }
24
+
25
+ it { should be(false) }
26
+
27
+ it_should_behave_like 'an idempotent method'
28
+ end
29
+
30
+ context 'without status and body' do
31
+ let(:object) { described_class.build }
32
+
33
+ it { should be(false) }
34
+
35
+ it_should_behave_like 'an idempotent method'
36
+ end
37
+ end