rest-in-peace 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,201 @@
1
+ require 'rest_in_peace'
2
+ require 'rest_in_peace/active_model_api'
3
+ require 'ostruct'
4
+
5
+ describe RESTinPeace do
6
+ let(:api_double) { double('IamAnAPI') }
7
+ let(:response) { OpenStruct.new(body: 'string!') }
8
+ let(:extended_class) do
9
+ Class.new do
10
+ include RESTinPeace
11
+
12
+ rest_in_peace do
13
+ attributes do
14
+ read :id, :name
15
+ write :description, :title
16
+ end
17
+
18
+ resource do
19
+ put :save, '/class/:id'
20
+ post :create, '/class'
21
+ end
22
+
23
+ acts_as_active_model
24
+ end
25
+ end
26
+ end
27
+
28
+ let(:id) { 1 }
29
+ let(:name) { 'pony one' }
30
+ let(:description) { nil }
31
+ let(:attributes) do
32
+ {
33
+ id: id,
34
+ name: name,
35
+ description: description,
36
+ }
37
+ end
38
+ let(:instance) { extended_class.new(attributes) }
39
+
40
+ context 'class methods' do
41
+ describe '::acts_as_active_model' do
42
+ context 'missing save method' do
43
+ let(:extended_class) do
44
+ Class.new do
45
+ include RESTinPeace
46
+ rest_in_peace do
47
+ acts_as_active_model
48
+ end
49
+ end
50
+ end
51
+
52
+ it 'raises an error when no save method specified' do
53
+ expect { extended_class.new }.to raise_error(RESTinPeace::ActiveModelAPI::MissingMethod)
54
+ end
55
+ end
56
+
57
+ context 'missing create method' do
58
+ let(:extended_class) do
59
+ Class.new do
60
+ include RESTinPeace
61
+ rest_in_peace do
62
+ resource do
63
+ put :save, '/yolo'
64
+ end
65
+ acts_as_active_model
66
+ end
67
+ end
68
+ end
69
+
70
+ it 'raises an error when no create method specified' do
71
+ expect { extended_class.new }.to raise_error(RESTinPeace::ActiveModelAPI::MissingMethod)
72
+ end
73
+ end
74
+ end
75
+
76
+ describe '::model_name' do
77
+ before do
78
+ def extended_class.model_name
79
+ ActiveModel::Name.new(self, nil, 'TemporaryClassForTests')
80
+ end
81
+ end
82
+ specify { expect(extended_class.model_name).to eq('TemporaryClassForTests') }
83
+ specify { expect(extended_class.model_name).to respond_to(:route_key) }
84
+ end
85
+
86
+ describe 'validation handling' do
87
+ specify { expect(extended_class).to respond_to(:human_attribute_name).with(2).arguments }
88
+ specify { expect(extended_class.human_attribute_name(:description)).to eq('description') }
89
+
90
+ specify { expect(extended_class).to respond_to(:lookup_ancestors).with(0).arguments }
91
+ specify { expect(extended_class.lookup_ancestors).to eq([extended_class]) }
92
+ end
93
+ end
94
+
95
+ context 'instance methods' do
96
+ before do
97
+ extended_class.api = api_double
98
+ allow(api_double).to receive(:put).and_return(response)
99
+ allow(api_double).to receive(:post).and_return(response)
100
+ end
101
+
102
+ describe '#changed?' do
103
+ context 'a new instance' do
104
+ specify { expect(instance.changed?).to eq(false) }
105
+ end
106
+
107
+ context 'a modified instance' do
108
+ before do
109
+ instance.description = 'new value'
110
+ end
111
+ specify { expect(instance.changed?).to eq(true) }
112
+ end
113
+
114
+ context 'a saved instance' do
115
+ before do
116
+ instance.description = 'new value'
117
+ instance.save
118
+ end
119
+ specify { expect(instance.changed?).to eq(false) }
120
+ end
121
+ end
122
+
123
+ describe 'attribute methods' do
124
+ specify { expect(instance).to respond_to(:description_changed?) }
125
+ specify { expect(instance).to respond_to(:title_changed?) }
126
+ end
127
+
128
+ describe '#to_key' do
129
+ specify { expect(instance.to_key).to eq([1]) }
130
+ end
131
+
132
+ describe '#persisted?' do
133
+ context 'persisted model' do
134
+ specify { expect(instance.persisted?).to eq(true) }
135
+ end
136
+
137
+ context 'not yet persisted model' do
138
+ let(:id) { nil }
139
+ specify { expect(instance.persisted?).to eq(false) }
140
+ end
141
+ end
142
+
143
+ describe '#save' do
144
+ context 'without errors' do
145
+ let(:response) { OpenStruct.new(body: { name: 'new name from api' }) }
146
+
147
+ specify { expect { instance.save }.to_not raise_error }
148
+ specify { expect(instance.save.object_id).to eq(instance.object_id) }
149
+ specify { expect { instance.save }.to change(instance, :name) }
150
+ end
151
+
152
+ context 'with errors' do
153
+ before do
154
+ instance.description = 'value!'
155
+ end
156
+ let(:response) { OpenStruct.new(body: { errors: { 'description' => ['is not empty'] } }) }
157
+
158
+ specify { expect { instance.save }.to change { instance.errors.any? } }
159
+ specify { expect { instance.save }.to_not change { instance.description } }
160
+ specify { expect { instance.save }.to_not change { instance.name } }
161
+ end
162
+ end
163
+
164
+ describe '#create' do
165
+ context 'without errors' do
166
+ let(:response) { OpenStruct.new(body: { name: 'new name from api' }) }
167
+
168
+ specify { expect { instance.create }.to_not raise_error }
169
+ specify { expect(instance.create.object_id).to eq(instance.object_id) }
170
+ specify { expect { instance.create }.to change(instance, :name) }
171
+ end
172
+
173
+ context 'with errors' do
174
+ before do
175
+ instance.description = 'value!'
176
+ end
177
+ let(:response) { OpenStruct.new(body: { errors: { 'description' => ['is not empty'] } }) }
178
+
179
+ specify { expect { instance.create }.to change { instance.errors.any? } }
180
+ specify { expect { instance.create }.to_not change { instance.description } }
181
+ specify { expect { instance.create }.to_not change { instance.name } }
182
+ end
183
+ end
184
+
185
+ describe 'validation handling' do
186
+ let(:description) { 'desc' }
187
+
188
+ specify { expect(instance).to respond_to(:read_attribute_for_validation).with(1).argument }
189
+ specify { expect(instance.read_attribute_for_validation(:description)).to eq('desc') }
190
+
191
+ describe '#errors' do
192
+ specify { expect(instance.errors).to be_instance_of(ActiveModel::Errors) }
193
+ end
194
+
195
+ describe '#errors=' do
196
+ let(:errors) { { description: ['must not be empty'] } }
197
+ specify { expect { instance.errors = errors }.to change { instance.errors.count } }
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,36 @@
1
+ require 'rest_in_peace'
2
+ require 'rest_in_peace/definition_proxy/attributes_definitions'
3
+
4
+ describe RESTinPeace::DefinitionProxy::AttributesDefinitions do
5
+ let(:target) do
6
+ Class.new do
7
+ include RESTinPeace
8
+ end
9
+ end
10
+ let(:instance_of_target) { target.new }
11
+ let(:definitions) { described_class.new(target) }
12
+
13
+ subject { definitions }
14
+
15
+ describe '#read' do
16
+ it 'defines a getter for the given attributes' do
17
+ expect { subject.read(:id) }.to change { instance_of_target.respond_to?(:id) }.from(false).to(true)
18
+ end
19
+ it 'does not define a setter for the given attributes' do
20
+ expect { subject.read(:id) }.to_not change { instance_of_target.respond_to?(:id=) }.from(false)
21
+ end
22
+ specify { expect { subject.read(:id) }.to change { target.rip_attributes[:read] }.from([]).to([:id]) }
23
+ specify { expect { subject.read(:id) }.to_not change { target.rip_attributes[:write] }.from([]) }
24
+ end
25
+
26
+ describe '#write' do
27
+ it 'defines a getter for the given attributes' do
28
+ expect { subject.write(:id) }.to change { instance_of_target.respond_to?(:id) }.from(false).to(true)
29
+ end
30
+ it 'defines a setter for the given attributes' do
31
+ expect { subject.write(:id) }.to change { instance_of_target.respond_to?(:id=) }.from(false).to(true)
32
+ end
33
+ specify { expect { subject.write(:id) }.to change { target.rip_attributes[:read] }.from([]).to([:id]) }
34
+ specify { expect { subject.write(:id) }.to change { target.rip_attributes[:write] }.from([]).to([:id]) }
35
+ end
36
+ end
@@ -5,9 +5,8 @@ describe RESTinPeace::DefinitionProxy::CollectionMethodDefinitions do
5
5
  let(:method_name) { :find }
6
6
  let(:url_template) { '/a/:id' }
7
7
  let(:default_params) { {} }
8
- let(:struct) { Struct.new(:id, :name) }
9
8
  let(:target) do
10
- Class.new(struct) do
9
+ Class.new do
11
10
  include RESTinPeace
12
11
  end
13
12
  end
@@ -71,6 +70,12 @@ describe RESTinPeace::DefinitionProxy::CollectionMethodDefinitions do
71
70
  target.find(id: 1)
72
71
  expect(default_params).to eq({ per_page: 250 })
73
72
  end
73
+
74
+ it 'raises an error when param is not a hash' do
75
+ subject.get(:find, '/a/:id', default_params)
76
+
77
+ expect { target.find(1) }.to raise_error(RESTinPeace::DefinitionProxy::InvalidArgument)
78
+ end
74
79
  end
75
80
 
76
81
  context 'without any attributes' do
@@ -2,10 +2,16 @@ require 'rest_in_peace'
2
2
  require 'rest_in_peace/definition_proxy/resource_method_definitions'
3
3
 
4
4
  describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
5
- let(:struct) { Struct.new(:id, :name) }
6
5
  let(:target) do
7
- Class.new(struct) do
6
+ Class.new do
8
7
  include RESTinPeace
8
+ rest_in_peace do
9
+ attributes do
10
+ read :id
11
+ write :name
12
+ end
13
+ end
14
+
9
15
  end
10
16
  end
11
17
  let(:instance) { target.new }
@@ -16,6 +22,7 @@ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
16
22
 
17
23
  before do
18
24
  allow(RESTinPeace::ApiCall).to receive(:new).and_return(api_call_double)
25
+ allow(api_call_double).to receive(http_verb)
19
26
  end
20
27
 
21
28
  shared_examples_for 'an instance method' do
@@ -28,21 +35,30 @@ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
28
35
  subject.send(http_verb, method_name, url_template)
29
36
  expect(target.rip_registry[:resource]).to eq([method: http_verb, name: method_name, url: url_template])
30
37
  end
38
+ end
31
39
 
32
- describe 'the created method' do
33
- before do
34
- allow(api_call_double).to receive(http_verb)
40
+ shared_examples_for 'an instance method with parameters' do
41
+ describe 'parameter and arguments handling' do
42
+ it 'uses the attributes of the class' do
43
+ expect(RESTinPeace::ApiCall).to receive(:new).
44
+ with(target.api, url_template, instance, instance.hash_for_updates).
45
+ and_return(api_call_double)
46
+
47
+ subject.send(http_verb, method_name, url_template)
48
+ instance.send(method_name)
35
49
  end
50
+ end
51
+ end
36
52
 
37
- describe 'parameter and arguments handling' do
38
- it 'uses the attributes of the class' do
39
- expect(RESTinPeace::ApiCall).to receive(:new).
40
- with(target.api, url_template, instance, instance.to_h).
41
- and_return(api_call_double)
53
+ shared_examples_for 'an instance method without parameters' do
54
+ describe 'parameter and arguments handling' do
55
+ it 'provides the id only' do
56
+ expect(RESTinPeace::ApiCall).to receive(:new).
57
+ with(target.api, url_template, instance, id: instance.id).
58
+ and_return(api_call_double)
42
59
 
43
- subject.send(http_verb, method_name, url_template)
44
- instance.send(method_name)
45
- end
60
+ subject.send(http_verb, method_name, url_template)
61
+ instance.send(method_name)
46
62
  end
47
63
  end
48
64
  end
@@ -53,6 +69,14 @@ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
53
69
  let(:method_name) { :reload }
54
70
  let(:url_template) { '/a/:id' }
55
71
  end
72
+
73
+ describe 'the created method' do
74
+ it_behaves_like 'an instance method with parameters' do
75
+ let(:http_verb) { :get }
76
+ let(:method_name) { :reload }
77
+ let(:url_template) { '/a/:id' }
78
+ end
79
+ end
56
80
  end
57
81
 
58
82
  context '#patch' do
@@ -61,6 +85,14 @@ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
61
85
  let(:method_name) { :save }
62
86
  let(:url_template) { '/a/:id' }
63
87
  end
88
+
89
+ describe 'the created method' do
90
+ it_behaves_like 'an instance method with parameters' do
91
+ let(:http_verb) { :patch }
92
+ let(:method_name) { :save }
93
+ let(:url_template) { '/a/:id' }
94
+ end
95
+ end
64
96
  end
65
97
 
66
98
  context '#post' do
@@ -69,6 +101,14 @@ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
69
101
  let(:method_name) { :create }
70
102
  let(:url_template) { '/a/:id' }
71
103
  end
104
+
105
+ describe 'the created method' do
106
+ it_behaves_like 'an instance method with parameters' do
107
+ let(:http_verb) { :post }
108
+ let(:method_name) { :create }
109
+ let(:url_template) { '/a/:id' }
110
+ end
111
+ end
72
112
  end
73
113
 
74
114
  context '#put' do
@@ -77,6 +117,14 @@ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
77
117
  let(:method_name) { :update }
78
118
  let(:url_template) { '/a/:id' }
79
119
  end
120
+
121
+ describe 'the created method' do
122
+ it_behaves_like 'an instance method with parameters' do
123
+ let(:http_verb) { :put }
124
+ let(:method_name) { :update }
125
+ let(:url_template) { '/a/:id' }
126
+ end
127
+ end
80
128
  end
81
129
 
82
130
  context '#delete' do
@@ -85,6 +133,14 @@ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
85
133
  let(:method_name) { :destroy }
86
134
  let(:url_template) { '/a/:id' }
87
135
  end
136
+
137
+ describe 'the created method' do
138
+ it_behaves_like 'an instance method without parameters' do
139
+ let(:http_verb) { :delete }
140
+ let(:method_name) { :destroy }
141
+ let(:url_template) { '/a/:id' }
142
+ end
143
+ end
88
144
  end
89
145
 
90
146
  end
@@ -1,9 +1,12 @@
1
+ require 'rest_in_peace'
1
2
  require 'rest_in_peace/definition_proxy'
3
+ require 'ostruct'
2
4
 
3
5
  describe RESTinPeace::DefinitionProxy do
4
6
  let(:resource_definitions) { object_double(RESTinPeace::DefinitionProxy::ResourceMethodDefinitions) }
5
7
  let(:collection_definitions) { object_double(RESTinPeace::DefinitionProxy::CollectionMethodDefinitions) }
6
- let(:target) { }
8
+ let(:attributes_definitions) { object_double(RESTinPeace::DefinitionProxy::AttributesDefinitions) }
9
+ let(:target) { double('Target') }
7
10
  let(:proxy) { RESTinPeace::DefinitionProxy.new(target) }
8
11
  let(:test_proc) { ->() {} }
9
12
 
@@ -14,6 +17,8 @@ describe RESTinPeace::DefinitionProxy do
14
17
  to receive(:new).with(target).and_return(resource_definitions)
15
18
  allow(RESTinPeace::DefinitionProxy::CollectionMethodDefinitions).
16
19
  to receive(:new).with(target).and_return(collection_definitions)
20
+ allow(RESTinPeace::DefinitionProxy::AttributesDefinitions).
21
+ to receive(:new).with(target).and_return(attributes_definitions)
17
22
  end
18
23
 
19
24
  describe '#resource' do
@@ -33,4 +38,32 @@ describe RESTinPeace::DefinitionProxy do
33
38
  subject.collection(&test_proc)
34
39
  end
35
40
  end
41
+
42
+ describe '#attributes' do
43
+ it 'forwards the given block to a attributes method definition' do
44
+ expect(attributes_definitions).to receive(:instance_eval) do |&block|
45
+ expect(block).to be_instance_of(Proc)
46
+ end
47
+ subject.attributes(&test_proc)
48
+ end
49
+ end
50
+
51
+ describe '#namespace_attributes_with' do
52
+ let(:target) do
53
+ Class.new do
54
+ include RESTinPeace
55
+ end
56
+ end
57
+ it 'configures the namespace' do
58
+ expect { subject.namespace_attributes_with(:blubb) }.
59
+ to change { target.rip_namespace }.from(nil).to(:blubb)
60
+ end
61
+ end
62
+
63
+ describe '#acts_as_active_model' do
64
+ it 'includes RESTinPeace::ActiveModelAPI' do
65
+ expect(target).to receive(:include).with(RESTinPeace::ActiveModelAPI)
66
+ subject.acts_as_active_model
67
+ end
68
+ end
36
69
  end
@@ -1,3 +1,4 @@
1
+ require 'rest_in_peace'
1
2
  require 'rest_in_peace/response_converter'
2
3
  require 'ostruct'
3
4
 
@@ -6,6 +7,16 @@ describe RESTinPeace::ResponseConverter do
6
7
  let(:element2) { { name: 'test2' } }
7
8
  let(:response) { OpenStruct.new(body: response_body) }
8
9
  let(:converter) { RESTinPeace::ResponseConverter.new(response, klass) }
10
+ let(:extended_class) do
11
+ Class.new do
12
+ include RESTinPeace
13
+ rest_in_peace do
14
+ attributes do
15
+ read :name
16
+ end
17
+ end
18
+ end
19
+ end
9
20
 
10
21
  describe '#result' do
11
22
  subject { converter.result }
@@ -13,13 +24,14 @@ describe RESTinPeace::ResponseConverter do
13
24
  shared_examples_for 'an array input' do
14
25
  let(:response_body) { [element1, element2] }
15
26
  specify { expect(subject).to be_instance_of(Array) }
16
- specify { expect(subject).to eq([OpenStruct.new(name: 'test1'), OpenStruct.new(name: 'test2')]) }
27
+ specify { expect(subject.first).to be_instance_of(extended_class) }
28
+ specify { expect(subject.map(&:name)).to eq(%w(test1 test2)) }
17
29
  end
18
30
 
19
31
  shared_examples_for 'a hash input' do
20
32
  let(:response_body) { element1 }
21
- specify { expect(subject).to be_instance_of(OpenStruct) }
22
- specify { expect(subject).to eq(OpenStruct.new(name: 'test1')) }
33
+ specify { expect(subject).to be_instance_of(extended_class) }
34
+ specify { expect(subject.name).to eq('test1') }
23
35
  end
24
36
 
25
37
  shared_examples_for 'a string input' do
@@ -28,8 +40,13 @@ describe RESTinPeace::ResponseConverter do
28
40
  specify { expect(subject).to eq('yolo binary stuff') }
29
41
  end
30
42
 
43
+ shared_examples_for 'an unknown input do' do
44
+ let(:response_body) { Object }
45
+ specify { expect { subject }.to raise_error(RESTinPeace::ResponseConverter::UnknownConvertStrategy) }
46
+ end
47
+
31
48
  context 'given type is a class' do
32
- let(:klass) { OpenStruct }
49
+ let(:klass) { extended_class }
33
50
  context 'input is an array' do
34
51
  it_behaves_like 'an array input'
35
52
  end
@@ -44,7 +61,7 @@ describe RESTinPeace::ResponseConverter do
44
61
  end
45
62
 
46
63
  context 'given type is an instance' do
47
- let(:klass) { OpenStruct.new }
64
+ let(:klass) { extended_class.new }
48
65
  context 'input is an array' do
49
66
  it_behaves_like 'an array input'
50
67
  end
@@ -19,6 +19,18 @@ describe RESTinPeace::TemplateSanitizer do
19
19
  specify { expect(subject).to eq('/a/1/b/2') }
20
20
  end
21
21
 
22
+ context 'tokens with substrings' do
23
+ let(:params) { { element: 'asd', element_id: 1 } }
24
+ let(:url_template) { '/a/:element/b/:element_id' }
25
+ specify { expect(subject).to eq('/a/asd/b/1') }
26
+ end
27
+
28
+ context 'tokens with substrings, reverse order' do
29
+ let(:params) { { element: 'asd', element_id: 1 } }
30
+ let(:url_template) { '/a/:element_id/b/:element' }
31
+ specify { expect(subject).to eq('/a/1/b/asd') }
32
+ end
33
+
22
34
  context 'incomplete params' do
23
35
  let(:params) { {} }
24
36
  let(:url_template) { '/a/:id' }