response 0.0.2

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