frenetic 2.0.0 → 3.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.
- checksums.yaml +4 -4
- data/frenetic.gemspec +1 -1
- data/lib/frenetic.rb +8 -1
- data/lib/frenetic/concerns/hal_linked.rb +2 -2
- data/lib/frenetic/concerns/related.rb +54 -0
- data/lib/frenetic/concerns/structure_method_definer.rb +7 -0
- data/lib/frenetic/resource.rb +67 -35
- data/lib/frenetic/resource_collection.rb +4 -4
- data/lib/frenetic/resource_mockery.rb +7 -8
- data/lib/frenetic/structure_registry.rb +20 -0
- data/lib/frenetic/structure_registry/rebuilder.rb +41 -0
- data/lib/frenetic/structure_registry/retriever.rb +35 -0
- data/lib/frenetic/version.rb +1 -1
- data/spec/concerns/related_spec.rb +118 -0
- data/spec/concerns/structure_method_definer_spec.rb +24 -0
- data/spec/resource_mockery_spec.rb +14 -14
- data/spec/resource_spec.rb +63 -3
- data/spec/structure_registry/rebuilder_spec.rb +83 -0
- data/spec/structure_registry/retriever_spec.rb +119 -0
- data/spec/structure_registry_spec.rb +54 -0
- metadata +19 -7
- data/lib/frenetic/concerns/structured.rb +0 -48
- data/spec/concerns/structured_spec.rb +0 -210
data/lib/frenetic/version.rb
CHANGED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Frenetic::Related do
|
4
|
+
let(:success) { true }
|
5
|
+
|
6
|
+
let(:response) do
|
7
|
+
res = Struct.new(:body, :success?)
|
8
|
+
res.new({
|
9
|
+
'id' => 99,
|
10
|
+
'foo' => 'bar'
|
11
|
+
}, success)
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:api_stub) { double('ApiDouble', get: response).as_null_object }
|
15
|
+
|
16
|
+
let(:related_resource) do
|
17
|
+
Class.new do
|
18
|
+
attr_reader :id
|
19
|
+
|
20
|
+
def initialize(attrs = {})
|
21
|
+
@id = attrs.fetch('id')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:my_temp_resource) do
|
27
|
+
Class.new do
|
28
|
+
def initialize(attrs = {})
|
29
|
+
@attrs = attrs
|
30
|
+
end
|
31
|
+
|
32
|
+
def api
|
33
|
+
self.class.class_variable_get('@@_api_stub')
|
34
|
+
end
|
35
|
+
|
36
|
+
def links
|
37
|
+
@attrs['_links']
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.find_resource_class(*args)
|
41
|
+
RelatedResource
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
let(:params) do
|
47
|
+
{
|
48
|
+
'_links' => {
|
49
|
+
'self' => {},
|
50
|
+
'related_resource' => {
|
51
|
+
'href' => '/related_resource'
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
before do
|
58
|
+
stub_const('MyTempResource', my_temp_resource)
|
59
|
+
stub_const('RelatedResource', related_resource)
|
60
|
+
MyTempResource.class_variable_set('@@_api_stub', api_stub)
|
61
|
+
MyTempResource.send(:include, described_class)
|
62
|
+
end
|
63
|
+
|
64
|
+
subject(:instance) { MyTempResource.new(params) }
|
65
|
+
|
66
|
+
describe '#extract_related_resources' do
|
67
|
+
subject { super().extract_related_resources }
|
68
|
+
|
69
|
+
it 'builds a relation map' do
|
70
|
+
subject
|
71
|
+
expect(instance.send(:relations)).to_not be_empty
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'does not extract self-referential resources' do
|
75
|
+
subject
|
76
|
+
expect(instance.send(:relations)).to_not include 'self'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#fetch_related_resource' do
|
81
|
+
let(:relation) { 'related_resource' }
|
82
|
+
|
83
|
+
let(:props) do
|
84
|
+
{
|
85
|
+
'id' => 99
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
subject { super().fetch_related_resource(relation, props) }
|
90
|
+
|
91
|
+
it 'returns the instantiated related resource instance' do
|
92
|
+
expect(subject).to be_a RelatedResource
|
93
|
+
expect(subject.id).to eql 99
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'for an unsuccessul resource request' do
|
97
|
+
let(:success) { false }
|
98
|
+
|
99
|
+
it 'returns nil' do
|
100
|
+
expect(subject).to be_nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'with an unknown resource instance' do
|
105
|
+
let(:exception) do
|
106
|
+
Frenetic::ClientError.new(status: 404)
|
107
|
+
end
|
108
|
+
|
109
|
+
before do
|
110
|
+
allow(api_stub).to receive(:get).and_raise(exception)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'raises an error' do
|
114
|
+
expect{subject}.to raise_error(Frenetic::ResourceNotFound)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Frenetic::StructureMethodDefiner do
|
4
|
+
let(:my_temp_resource) { Class.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
stub_const('MyTempResource', my_temp_resource)
|
8
|
+
MyTempResource.send(:extend, described_class)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '.structure' do
|
12
|
+
before do
|
13
|
+
MyTempResource.structure do
|
14
|
+
123
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
subject { MyTempResource.instance_variable_get('@_structure_block') }
|
19
|
+
|
20
|
+
it 'stores a reference to the block' do
|
21
|
+
expect(subject).to_not be_nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -11,12 +11,12 @@ describe Frenetic::ResourceMockery do
|
|
11
11
|
Class.new(my_temp_resource) do
|
12
12
|
def self.default_attributes
|
13
13
|
{
|
14
|
-
id
|
15
|
-
price
|
16
|
-
qux
|
17
|
-
_embedded
|
18
|
-
embedded_resource
|
19
|
-
plugh
|
14
|
+
'id' => 1,
|
15
|
+
'price' => 0.0,
|
16
|
+
'qux' => 'qux',
|
17
|
+
'_embedded' => {
|
18
|
+
'embedded_resource' => {
|
19
|
+
'plugh' => 'xyzzy'
|
20
20
|
}
|
21
21
|
}
|
22
22
|
}
|
@@ -26,16 +26,16 @@ describe Frenetic::ResourceMockery do
|
|
26
26
|
|
27
27
|
let(:params) do
|
28
28
|
{
|
29
|
-
id
|
30
|
-
foo
|
31
|
-
price
|
32
|
-
bar
|
29
|
+
'id' => '1',
|
30
|
+
'foo' => 'bar',
|
31
|
+
'price' => '0.0',
|
32
|
+
'bar' => 'baz',
|
33
33
|
_embedded: {
|
34
|
-
embedded_resource
|
35
|
-
grault
|
34
|
+
'embedded_resource' => {
|
35
|
+
'grault' => 'garply'
|
36
36
|
},
|
37
|
-
another_resource
|
38
|
-
waldo
|
37
|
+
'another_resource' => {
|
38
|
+
'waldo' => 'fred'
|
39
39
|
}
|
40
40
|
}
|
41
41
|
}
|
data/spec/resource_spec.rb
CHANGED
@@ -88,6 +88,66 @@ describe Frenetic::Resource do
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
+
describe '.extract_embedded_resources' do
|
92
|
+
let(:resource) do
|
93
|
+
{
|
94
|
+
'_embedded' => {
|
95
|
+
'my_temp_resource' => {
|
96
|
+
'id' => 99
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
before { @stubs.api_description }
|
103
|
+
|
104
|
+
subject do
|
105
|
+
MyNamespace::MyTempResource.extract_embedded_resources(resource)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'extracts all embedded resources' do
|
109
|
+
expect(subject).to include 'my_temp_resource' => kind_of(MyNamespace::MyTempResource)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '.find_resource_class' do
|
114
|
+
let(:params) do
|
115
|
+
{
|
116
|
+
id: 54,
|
117
|
+
_links: {
|
118
|
+
known_resource: {
|
119
|
+
href: '/known_resource'
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
before do
|
126
|
+
@stubs.api_description
|
127
|
+
stub_const('MyNamespace::KnownResource', abstract_resource)
|
128
|
+
end
|
129
|
+
|
130
|
+
subject do
|
131
|
+
MyNamespace::MyTempResource.find_resource_class(resource_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'with a known resource' do
|
135
|
+
let(:resource_name) { 'known_resource' }
|
136
|
+
|
137
|
+
it 'returns the resource class' do
|
138
|
+
expect(subject).to eql MyNamespace::KnownResource
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'with an unknown resource' do
|
143
|
+
let(:resource_name) { 'unknown_resource' }
|
144
|
+
|
145
|
+
it 'returns an anonymous class' do
|
146
|
+
expect(subject).to eql OpenStruct
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
91
151
|
describe '.namespace' do
|
92
152
|
before do
|
93
153
|
stub_const 'MyNamespace::MyTempResource', abstract_resource
|
@@ -278,9 +338,9 @@ describe Frenetic::Resource do
|
|
278
338
|
expect{subject.gender}.to raise_error(NoMethodError)
|
279
339
|
end
|
280
340
|
|
281
|
-
it 'is accessible in @
|
282
|
-
|
283
|
-
expect(
|
341
|
+
it 'is accessible in @raw_attributes' do
|
342
|
+
raw_attrs = subject.instance_variable_get('@raw_attributes')
|
343
|
+
expect(raw_attrs).to include 'gender' => 'male'
|
284
344
|
end
|
285
345
|
end
|
286
346
|
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Frenetic::StructureRegistry::Rebuilder do
|
4
|
+
let(:signatures) { {} }
|
5
|
+
let(:resource) { Class.new }
|
6
|
+
let(:attributes) do
|
7
|
+
{
|
8
|
+
'id' => 123
|
9
|
+
}
|
10
|
+
end
|
11
|
+
let(:key) { 'MockKey' }
|
12
|
+
let(:signature) { 'abc123def456' }
|
13
|
+
|
14
|
+
subject(:instance) { described_class.new(signatures, resource, attributes, key, signature) }
|
15
|
+
|
16
|
+
after do
|
17
|
+
Struct.send(:remove_const, key) if Struct.constants.include?(key.to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#call' do
|
21
|
+
subject { super().call }
|
22
|
+
|
23
|
+
it 'removes any existing registration' do
|
24
|
+
allow(instance).to receive(:destroy!)
|
25
|
+
subject
|
26
|
+
expect(instance).to have_received(:destroy!)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'registers the Struct signature' do
|
30
|
+
subject
|
31
|
+
expect(instance.signatures[key]).to eql signature
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns a Struct for the given resource' do
|
35
|
+
expect(subject).to eql Struct::MockKey
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#destroy!' do
|
40
|
+
let!(:struct) { Struct.new(key) }
|
41
|
+
|
42
|
+
subject { super().destroy! }
|
43
|
+
|
44
|
+
it 'removes the registered signature' do
|
45
|
+
subject
|
46
|
+
expect(instance.signatures).to_not include(key)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'removes the registered Struct constant' do
|
50
|
+
subject
|
51
|
+
expect{Struct.const_get(key.to_sym)}.to raise_error(NameError, %r[uninitialized constant Struct::#{key}])
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'for a non-existant struct' do
|
55
|
+
before do
|
56
|
+
allow(instance).to receive(:exists?).and_return(false)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'does nothing' do
|
60
|
+
subject
|
61
|
+
expect(Struct.const_get(key)).to_not be_nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#exists?' do
|
67
|
+
subject { super().exists? }
|
68
|
+
|
69
|
+
context 'for a registered resource' do
|
70
|
+
let!(:struct) { Struct.new(key) }
|
71
|
+
|
72
|
+
it 'returns TRUE' do
|
73
|
+
expect(subject).to eql true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'for an un-registered resource' do
|
78
|
+
it 'returns FALSE' do
|
79
|
+
expect(subject).to eql false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Frenetic::StructureRegistry::Retriever do
|
4
|
+
let(:rebuilder_class) { Frenetic::StructureRegistry::Rebuilder }
|
5
|
+
let(:signatures) { {} }
|
6
|
+
let(:resource) { Class.new }
|
7
|
+
let(:attributes) do
|
8
|
+
{
|
9
|
+
'id' => 123
|
10
|
+
}
|
11
|
+
end
|
12
|
+
let(:key) { 'MockKey' }
|
13
|
+
|
14
|
+
subject(:instance) do
|
15
|
+
described_class.new(signatures, resource, attributes, key, rebuilder_class: rebuilder_class)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#initialize' do
|
19
|
+
context 'with a blank :key argument' do
|
20
|
+
let(:key) { ' ' }
|
21
|
+
|
22
|
+
it 'raises an error' do
|
23
|
+
expect{subject}.to raise_error(ArgumentError, /non-blank/)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#call' do
|
29
|
+
let(:rebuilder) { double('Rebuilder') }
|
30
|
+
let(:rebuilder_class) { double('RebuilderClass', new: rebuilder) }
|
31
|
+
|
32
|
+
subject { super().call }
|
33
|
+
|
34
|
+
context 'for an expired resource registration' do
|
35
|
+
before do
|
36
|
+
allow(instance).to receive(:expired?).and_return(true)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'invokes the Rebuilder' do
|
40
|
+
allow(rebuilder).to receive(:call)
|
41
|
+
subject
|
42
|
+
expect(rebuilder).to have_received(:call)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'for an un-expired resource registration' do
|
47
|
+
before do
|
48
|
+
allow(instance).to receive(:expired?).and_return(false)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'fetches the resource' do
|
52
|
+
allow(instance).to receive(:fetch_structure).and_return(true)
|
53
|
+
subject
|
54
|
+
expect(instance).to have_received(:fetch_structure)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#expired?' do
|
60
|
+
let(:resource_signature) { 'mock_signature' }
|
61
|
+
let(:signatures) do
|
62
|
+
{
|
63
|
+
key => resource_signature
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
before do
|
68
|
+
allow(instance).to receive(:struct_signature).and_return resource_signature
|
69
|
+
end
|
70
|
+
|
71
|
+
subject { super().expired? }
|
72
|
+
|
73
|
+
context 'for an un-registered resource' do
|
74
|
+
let(:signatures) { {} }
|
75
|
+
|
76
|
+
it 'returns TRUE' do
|
77
|
+
expect(subject).to eql true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'for an expired resource' do
|
82
|
+
let(:signatures) do
|
83
|
+
{
|
84
|
+
key => resource_signature + 'OLD'
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'returns TRUE' do
|
89
|
+
expect(subject).to eql true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'for an un-expired resource' do
|
94
|
+
it 'returns FALSE' do
|
95
|
+
expect(subject).to eql false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#fetch_structure' do
|
101
|
+
before { Struct.new(key) }
|
102
|
+
|
103
|
+
after { Struct.send(:remove_const, key) }
|
104
|
+
|
105
|
+
subject { super().fetch_structure }
|
106
|
+
|
107
|
+
it 'returns the registered Struct class' do
|
108
|
+
expect(subject).to eql Struct::MockKey
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '#struct_signature' do
|
113
|
+
subject { super().struct_signature }
|
114
|
+
|
115
|
+
it 'returns a unique signature for a given resource' do
|
116
|
+
expect(subject).to match(/\A[a-z0-9]{40}\z/)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|