rest-in-peace 1.4.0 → 2.0.0

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.
@@ -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' }