pod4 0.10.6 → 1.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 +5 -5
- data/.bugs/bugs +2 -1
- data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
- data/.hgtags +1 -0
- data/Gemfile +5 -5
- data/README.md +157 -46
- data/lib/pod4/basic_model.rb +9 -22
- data/lib/pod4/connection.rb +67 -0
- data/lib/pod4/connection_pool.rb +154 -0
- data/lib/pod4/errors.rb +20 -0
- data/lib/pod4/interface.rb +34 -12
- data/lib/pod4/model.rb +32 -27
- data/lib/pod4/nebulous_interface.rb +25 -30
- data/lib/pod4/null_interface.rb +22 -16
- data/lib/pod4/pg_interface.rb +84 -104
- data/lib/pod4/sequel_interface.rb +138 -82
- data/lib/pod4/tds_interface.rb +83 -70
- data/lib/pod4/tweaking.rb +105 -0
- data/lib/pod4/version.rb +1 -1
- data/md/breaking_changes.md +80 -0
- data/spec/common/basic_model_spec.rb +67 -70
- data/spec/common/connection_pool_parallelism_spec.rb +154 -0
- data/spec/common/connection_pool_spec.rb +246 -0
- data/spec/common/connection_spec.rb +129 -0
- data/spec/common/model_ai_missing_id_spec.rb +256 -0
- data/spec/common/model_plus_encrypting_spec.rb +16 -4
- data/spec/common/model_plus_tweaking_spec.rb +128 -0
- data/spec/common/model_plus_typecasting_spec.rb +10 -4
- data/spec/common/model_spec.rb +283 -363
- data/spec/common/nebulous_interface_spec.rb +159 -108
- data/spec/common/null_interface_spec.rb +88 -65
- data/spec/common/sequel_interface_pg_spec.rb +217 -161
- data/spec/common/shared_examples_for_interface.rb +50 -50
- data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
- data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
- data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
- data/spec/mri/pg_encrypting_spec.rb +1 -1
- data/spec/mri/pg_interface_spec.rb +311 -223
- data/spec/mri/sequel_encrypting_spec.rb +1 -1
- data/spec/mri/sequel_interface_spec.rb +177 -180
- data/spec/mri/tds_encrypting_spec.rb +1 -1
- data/spec/mri/tds_interface_spec.rb +296 -212
- data/tags +340 -174
- metadata +19 -11
- data/md/fixme.md +0 -3
- data/md/roadmap.md +0 -125
- data/md/typecasting.md +0 -80
- data/spec/common/model_new_validate_spec.rb +0 -204
@@ -0,0 +1,246 @@
|
|
1
|
+
require "pod4/connection_pool"
|
2
|
+
|
3
|
+
|
4
|
+
describe Pod4::ConnectionPool do
|
5
|
+
|
6
|
+
let(:ifce_class) do
|
7
|
+
Class.new Pod4::Interface do
|
8
|
+
def initialize; end
|
9
|
+
def close_connection; end
|
10
|
+
def new_connection(opts); @conn; end
|
11
|
+
|
12
|
+
def set_conn(c); @conn = c; end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
describe "#new" do
|
18
|
+
|
19
|
+
it "accepts some options and stores them as attributes" do
|
20
|
+
c = ConnectionPool.new(interface: ifce_class, max_clients: 4, max_wait: 4_000)
|
21
|
+
|
22
|
+
expect( c.max_clients ).to eq 4
|
23
|
+
expect( c.max_wait ).to eq 4_000
|
24
|
+
end
|
25
|
+
|
26
|
+
it "falls back to reasonable defaults" do
|
27
|
+
c = ConnectionPool.new(interface: ifce_class)
|
28
|
+
|
29
|
+
expect( c.max_clients ).to eq 10
|
30
|
+
expect( c.max_wait ).to eq nil
|
31
|
+
end
|
32
|
+
|
33
|
+
end # of #new
|
34
|
+
|
35
|
+
|
36
|
+
describe "#client" do
|
37
|
+
|
38
|
+
context "when there is a client in the pool for this thread" do
|
39
|
+
before(:each) do
|
40
|
+
@connection = ConnectionPool.new(interface: ifce_class)
|
41
|
+
@connection.data_layer_options = "meh"
|
42
|
+
@interface = ifce_class.new
|
43
|
+
@interface.set_conn "bar"
|
44
|
+
|
45
|
+
# First call to client for a thread should assign a client to it
|
46
|
+
@connection.client(@interface)
|
47
|
+
expect( @connection._pool.size ).to eq 1
|
48
|
+
expect( @connection._pool.first.thread_id ).to eq Thread.current.object_id
|
49
|
+
end
|
50
|
+
|
51
|
+
it "returns that client" do
|
52
|
+
expect( @interface ).not_to receive(:new_connection)
|
53
|
+
expect( @connection.client(@interface) ).to eq "bar"
|
54
|
+
end
|
55
|
+
end # of when there is a client in the pool for this thread
|
56
|
+
|
57
|
+
context "when there is no client for this thread and a free one in the pool" do
|
58
|
+
before(:each) do
|
59
|
+
@connection = ConnectionPool.new(interface: ifce_class)
|
60
|
+
@connection.data_layer_options = "meh"
|
61
|
+
@interface = ifce_class.new
|
62
|
+
@interface.set_conn "foo"
|
63
|
+
|
64
|
+
# Call client to assign a client to the thread; then call close to release it. We end up
|
65
|
+
# with a client in the pool which is assigned to no thread.
|
66
|
+
@connection.client(@interface)
|
67
|
+
@connection.close(@interface)
|
68
|
+
expect( @connection._pool.size ).to eq 1
|
69
|
+
expect( @connection._pool.first.thread_id ).to be_nil
|
70
|
+
end
|
71
|
+
|
72
|
+
it "returns the free one" do
|
73
|
+
expect( @interface ).not_to receive(:new_connection).and_call_original
|
74
|
+
expect( @connection.client(@interface) ).to eq "foo"
|
75
|
+
end
|
76
|
+
|
77
|
+
it "assigns the client to this thread id" do
|
78
|
+
@connection.client(@interface)
|
79
|
+
expect( @connection._pool.size ).to eq 1
|
80
|
+
expect( @connection._pool.first.thread_id ).to eq Thread.current.object_id
|
81
|
+
end
|
82
|
+
end # of when there is no client for this thread and a free one
|
83
|
+
|
84
|
+
context "when max_clients == nil, there is no client for this thread and none free" do
|
85
|
+
before(:each) do
|
86
|
+
@connection = ConnectionPool.new(interface: ifce_class)
|
87
|
+
@connection.data_layer_options = "meh"
|
88
|
+
@interface = ifce_class.new
|
89
|
+
@interface.set_conn "foo"
|
90
|
+
|
91
|
+
# The simplest case for this scenario is an empty pool
|
92
|
+
end
|
93
|
+
|
94
|
+
it "asks the interface to give it a new client and returns that" do
|
95
|
+
expect( @interface ).to receive(:new_connection).with("meh").and_call_original
|
96
|
+
expect( @connection.client(@interface) ).to eq "foo"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "stores the new client from the interface in the pool against the thread" do
|
100
|
+
@connection.client(@interface)
|
101
|
+
expect( @connection._pool.size ).to eq 1
|
102
|
+
expect( @connection._pool.first.thread_id ).to eq Thread.current.object_id
|
103
|
+
end
|
104
|
+
end # of when max_clients == nil, there is no client for this thread and none free
|
105
|
+
|
106
|
+
context "when max_clients != nil, there is no client for this thread and none free" do
|
107
|
+
|
108
|
+
context "when we're not at the maximum" do
|
109
|
+
before(:each) do
|
110
|
+
@connection = ConnectionPool.new(interface: ifce_class, max_clients: 1)
|
111
|
+
@connection.data_layer_options = "meh"
|
112
|
+
@interface = ifce_class.new
|
113
|
+
@interface.set_conn "baz"
|
114
|
+
|
115
|
+
#this is an empty pool again
|
116
|
+
end
|
117
|
+
|
118
|
+
it "asks the interface to give it a new client and returns that" do
|
119
|
+
expect( @interface ).to receive(:new_connection).with("meh").and_call_original
|
120
|
+
expect( @connection.client(@interface) ).to eq "baz"
|
121
|
+
end
|
122
|
+
|
123
|
+
it "stores the new client from the interface in the pool against the thread" do
|
124
|
+
@connection.client(@interface)
|
125
|
+
expect( @connection._pool.size ).to eq 1
|
126
|
+
expect( @connection._pool.first.thread_id ).to eq Thread.current.object_id
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "when we reach the maximum" do
|
131
|
+
before(:each) do
|
132
|
+
@connection = ConnectionPool.new(interface: ifce_class, max_clients: 1)
|
133
|
+
@connection.data_layer_options = "meh"
|
134
|
+
@interface = ifce_class.new
|
135
|
+
@interface.set_conn "boz"
|
136
|
+
|
137
|
+
# assign our 1 client in the pool to a different thread
|
138
|
+
@thread = Thread.new do
|
139
|
+
@connection.client(@interface)
|
140
|
+
Thread.stop # pause the thread here until something external restarts it
|
141
|
+
sleep 1
|
142
|
+
@connection.close(@interface)
|
143
|
+
end
|
144
|
+
sleep 0.1 until @thread.stop?
|
145
|
+
|
146
|
+
expect( @connection._pool.size ).to eq 1
|
147
|
+
expect( @connection._pool.first.thread_id ).not_to be_nil
|
148
|
+
expect( @connection._pool.first.thread_id ).not_to eq Thread.current.object_id
|
149
|
+
end
|
150
|
+
|
151
|
+
after(:each) { @thread&.kill }
|
152
|
+
|
153
|
+
it "blocks until a thread is free" do
|
154
|
+
# This is hard to test! We can do it but we have to make the horrible assumption that we
|
155
|
+
# are using the Ruby `loop` keyword, then stub a loop method to override it.
|
156
|
+
expect( @connection ).to receive(:loop).and_yield.and_yield.and_yield
|
157
|
+
|
158
|
+
@connection.client(@interface)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "once a client is released it uses that" do
|
162
|
+
@thread.run # free pool in 1 sec (we want to be already running #client when it frees)
|
163
|
+
expect( @connection.client(@interface) ).to eq "boz" # ...eventually
|
164
|
+
expect( @connection._pool.size ).to eq 1
|
165
|
+
expect( @connection._pool.first.thread_id ).to eq Thread.current.object_id
|
166
|
+
end
|
167
|
+
end # of when we reach the maximum
|
168
|
+
|
169
|
+
context "when we reach the maximum, max_wait is set and the time is up" do
|
170
|
+
before(:each) do
|
171
|
+
@connection = ConnectionPool.new(interface: ifce_class, max_clients: 2, max_wait: 1)
|
172
|
+
@connection.data_layer_options = "meh"
|
173
|
+
@interface = ifce_class.new
|
174
|
+
@interface.set_conn "foo"
|
175
|
+
|
176
|
+
# assign our 2 clients in the pool to a different thread
|
177
|
+
@threads = []
|
178
|
+
2.times do
|
179
|
+
@threads << Thread.new { @connection.client(@interface); Thread.stop }
|
180
|
+
end
|
181
|
+
sleep 0.1 until @threads.all?{|t| t.stop? }
|
182
|
+
|
183
|
+
expect( @connection._pool.size ).to eq 2
|
184
|
+
@threads.each do |t|
|
185
|
+
expect( t ).not_to be_nil
|
186
|
+
expect( t ).not_to eq Thread.current.object_id
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
after(:each) { @threads.each{|t| t.kill} }
|
191
|
+
|
192
|
+
it "raises a PoolTimeout" do
|
193
|
+
expect{ @connection.client(@interface) }.to raise_error Pod4::PoolTimeout
|
194
|
+
end
|
195
|
+
end # of when we reach the maximum, max_wait is set and the time is up
|
196
|
+
|
197
|
+
end # of when max_clients != nil, there is no client for this thread and none free
|
198
|
+
|
199
|
+
end # of #client
|
200
|
+
|
201
|
+
|
202
|
+
describe "#close" do
|
203
|
+
before(:each) do
|
204
|
+
@connection = ConnectionPool.new(interface: ifce_class)
|
205
|
+
@connection.data_layer_options = "meh"
|
206
|
+
@interface = ifce_class.new
|
207
|
+
@interface.set_conn "brep"
|
208
|
+
|
209
|
+
@connection.client(@interface)
|
210
|
+
expect( @connection._pool.size ).to eq 1
|
211
|
+
expect( @connection._pool.first.thread_id ).to eq Thread.current.object_id
|
212
|
+
end
|
213
|
+
|
214
|
+
it "de-assigns the client for this thread from the thread" do
|
215
|
+
@connection.close(@interface)
|
216
|
+
expect( @connection._pool.size ).to eq 1
|
217
|
+
expect( @connection._pool.first.thread_id ).to be_nil
|
218
|
+
end
|
219
|
+
|
220
|
+
end # of #close
|
221
|
+
|
222
|
+
|
223
|
+
describe "#drop" do
|
224
|
+
before(:each) do
|
225
|
+
@connection = ConnectionPool.new(interface: ifce_class)
|
226
|
+
@connection.data_layer_options = "meh"
|
227
|
+
@interface = ifce_class.new
|
228
|
+
@interface.set_conn "flong"
|
229
|
+
|
230
|
+
@connection.client(@interface)
|
231
|
+
expect( @connection._pool.size ).to eq 1
|
232
|
+
expect( @connection._pool.first.thread_id ).to eq Thread.current.object_id
|
233
|
+
end
|
234
|
+
|
235
|
+
it "removes the client object from the pool entirely" do
|
236
|
+
@connection.drop(@interface)
|
237
|
+
expect( @connection._pool.size ).to eq 0
|
238
|
+
end
|
239
|
+
|
240
|
+
end # of #drop
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
|
245
|
+
end
|
246
|
+
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'pod4/connection'
|
2
|
+
require 'pod4/null_interface'
|
3
|
+
|
4
|
+
|
5
|
+
##
|
6
|
+
# I can't make these anonymous classes in an Rspec `let`, because the name of the interface class is
|
7
|
+
# passed to the Connection object when it is initialised.
|
8
|
+
##
|
9
|
+
|
10
|
+
class ConnectionTestingI < Interface
|
11
|
+
def initialize; end
|
12
|
+
def close_connection; end
|
13
|
+
def new_connection(args); {conn: args}; end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ConnectionTestingIBad < Interface
|
17
|
+
def initialize; end
|
18
|
+
def close_connection; end
|
19
|
+
def new_connection(args); end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
describe Pod4::Connection do
|
24
|
+
|
25
|
+
let(:interface) { ConnectionTestingI.new }
|
26
|
+
let(:conn) { Pod4::Connection.new(interface: ConnectionTestingI) }
|
27
|
+
|
28
|
+
|
29
|
+
describe "#new" do
|
30
|
+
|
31
|
+
it "takes a hash" do
|
32
|
+
expect{ Pod4::Connection.new }.to raise_error ArgumentError
|
33
|
+
expect{ Pod4::Connection.new(:foo) }.to raise_error ArgumentError
|
34
|
+
end
|
35
|
+
|
36
|
+
it "needs :interface, a Pod4::Interface class" do
|
37
|
+
expect{ Pod4::Connection.new(interface: "foo") }.to raise_error ArgumentError
|
38
|
+
expect{ Pod4::Connection.new(interface: Array) }.to raise_error ArgumentError
|
39
|
+
expect{ Pod4::Connection.new(interface: ConnectionTestingI) }.not_to raise_error
|
40
|
+
|
41
|
+
expect( conn.interface_class ).to eq ConnectionTestingI
|
42
|
+
end
|
43
|
+
|
44
|
+
end # of #new
|
45
|
+
|
46
|
+
|
47
|
+
describe "#data_layer_options" do
|
48
|
+
|
49
|
+
it "stores an arbitrary object" do
|
50
|
+
expect( conn.data_layer_options ).to be_nil
|
51
|
+
|
52
|
+
conn.data_layer_options = {one: 2, three: 4}
|
53
|
+
|
54
|
+
expect( conn.data_layer_options ).to eq(one: 2, three: 4)
|
55
|
+
end
|
56
|
+
|
57
|
+
end # of #data_layer_options
|
58
|
+
|
59
|
+
|
60
|
+
describe "#close" do
|
61
|
+
|
62
|
+
it "raises ArgumentError if given an interface that wasn't the one you passed in #new" do
|
63
|
+
i = ConnectionTestingIBad.new
|
64
|
+
expect{ conn.close(i) }.to raise_error ArgumentError
|
65
|
+
end
|
66
|
+
|
67
|
+
it "calls close on the interface" do
|
68
|
+
expect(interface).to receive(:close_connection)
|
69
|
+
|
70
|
+
conn.close(interface)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "resets the stored client" do
|
74
|
+
conn.close(interface)
|
75
|
+
|
76
|
+
# Now the stored client should be unset, so a further call to #client should ask the
|
77
|
+
# interface for one
|
78
|
+
expect(interface).to receive(:new_connection)
|
79
|
+
|
80
|
+
conn.client(interface)
|
81
|
+
end
|
82
|
+
|
83
|
+
end # of #close
|
84
|
+
|
85
|
+
|
86
|
+
describe "#client" do
|
87
|
+
|
88
|
+
it "takes an interface object" do
|
89
|
+
expect{ conn.client }.to raise_exception ArgumentError
|
90
|
+
expect{ conn.client(14) }.to raise_exception ArgumentError
|
91
|
+
|
92
|
+
expect{ conn.client(interface) }.not_to raise_exception
|
93
|
+
end
|
94
|
+
|
95
|
+
it "raises ArgumentError if given an interface that wasn't the one you passed in #new" do
|
96
|
+
i = ConnectionTestingIBad.new
|
97
|
+
expect{ conn.client(i) }.to raise_error ArgumentError
|
98
|
+
end
|
99
|
+
|
100
|
+
context "when it has no connection" do
|
101
|
+
|
102
|
+
it "calls new_connection on the interface" do
|
103
|
+
expect(interface).to receive(:new_connection).with("bar").and_call_original
|
104
|
+
|
105
|
+
conn.data_layer_options = "bar"
|
106
|
+
|
107
|
+
expect( conn.client(interface) ).to eq(conn: "bar")
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when it has a connection" do
|
113
|
+
|
114
|
+
it "returns what it has" do
|
115
|
+
# set things up like before so we have an existing connection
|
116
|
+
conn.data_layer_options = "foo"
|
117
|
+
conn.client(interface)
|
118
|
+
|
119
|
+
expect(interface).not_to receive(:new_connection)
|
120
|
+
|
121
|
+
expect( conn.client(interface) ).to eq(conn: "foo")
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end # of #client
|
127
|
+
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
require "octothorpe"
|
2
|
+
|
3
|
+
require "pod4/model"
|
4
|
+
require "pod4/null_interface"
|
5
|
+
|
6
|
+
|
7
|
+
##
|
8
|
+
# This test covers a model with an autoincrementing ID but where the ID field is not named as an
|
9
|
+
# attribute. Pre-1.0 you _had_ to do it this way. No reason why it would not be an option going
|
10
|
+
# forward.
|
11
|
+
#
|
12
|
+
describe "(Autoincrementing Model with No ID Attribute)" do
|
13
|
+
|
14
|
+
let(:customer_model_class) do
|
15
|
+
Class.new Pod4::Model do
|
16
|
+
attr_columns :name, :groups, :price
|
17
|
+
set_interface NullInterface.new(:id, :name, :price, :groups,
|
18
|
+
[ {id: 1, name: "Gomez", price: 1.23, groups: "trains" },
|
19
|
+
{id: 2, name: "Morticia", price: 2.34, groups: "spanish" },
|
20
|
+
{id: 3, name: "Wednesday", price: 3.45, groups: "school" },
|
21
|
+
{id: 4, name: "Pugsley", price: 4.56, groups: "trains,school"} ] )
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:records) do
|
27
|
+
[ {id: 1, name: "Gomez", price: 1.23, groups: "trains" },
|
28
|
+
{id: 2, name: "Morticia", price: 2.34, groups: "spanish" },
|
29
|
+
{id: 3, name: "Wednesday", price: 3.45, groups: "school" },
|
30
|
+
{id: 4, name: "Pugsley", price: 4.56, groups: "trains,school"} ]
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:records_as_ot) { records.map{|r| Octothorpe.new(r) } }
|
35
|
+
|
36
|
+
# model is just a plain newly created object that you can call read on.
|
37
|
+
# model2 and model3 are in an identical state - they have been filled with a
|
38
|
+
# read(). We have two so that we can RSpec "allow" on one and not the other.
|
39
|
+
|
40
|
+
let(:model) { customer_model_class.new(2) }
|
41
|
+
|
42
|
+
let(:model2) do
|
43
|
+
m = customer_model_class.new(3)
|
44
|
+
|
45
|
+
allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[2]) )
|
46
|
+
m.read.or_die
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:model3) do
|
50
|
+
m = customer_model_class.new(4)
|
51
|
+
|
52
|
+
allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[3]) )
|
53
|
+
m.read.or_die
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
describe "Model.list" do
|
58
|
+
let(:list1) { customer_model_class.list }
|
59
|
+
|
60
|
+
it "returns an array of customer_model_class records" do
|
61
|
+
expect( list1 ).to be_a_kind_of Array
|
62
|
+
expect( list1 ).to all(be_a_kind_of customer_model_class)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns the data from the interface" do
|
66
|
+
expect( list1.size ).to eq records.size
|
67
|
+
expect( list1.map(&:to_ot).map(&:to_h) ).to match_array(records)
|
68
|
+
end
|
69
|
+
|
70
|
+
end # of Model.list
|
71
|
+
|
72
|
+
|
73
|
+
describe "#columns" do
|
74
|
+
|
75
|
+
it "returns the attr_columns list from the class definition" do
|
76
|
+
expect( customer_model_class.new.columns ).
|
77
|
+
to match_array( [:name,:price,:groups] )
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end # of #columns
|
82
|
+
|
83
|
+
|
84
|
+
describe "#set" do
|
85
|
+
let (:ot) { records_as_ot[3] }
|
86
|
+
|
87
|
+
it "takes an Octothorpe or a Hash" do
|
88
|
+
expect{ model.set }.to raise_exception ArgumentError
|
89
|
+
expect{ model.set(nil) }.to raise_exception ArgumentError
|
90
|
+
expect{ model.set(:foo) }.to raise_exception ArgumentError
|
91
|
+
|
92
|
+
expect{ model.set(ot) }.not_to raise_exception
|
93
|
+
end
|
94
|
+
|
95
|
+
it "returns self" do
|
96
|
+
expect( model.set(ot) ).to eq model
|
97
|
+
end
|
98
|
+
|
99
|
+
it "sets the attribute columns from the hash" do
|
100
|
+
model.set(ot)
|
101
|
+
|
102
|
+
expect( model.name ).to eq ot.>>.name
|
103
|
+
expect( model.price ).to eq ot.>>.price
|
104
|
+
end
|
105
|
+
|
106
|
+
end # of #set
|
107
|
+
|
108
|
+
|
109
|
+
describe "#to_ot" do
|
110
|
+
|
111
|
+
it "returns an Octothorpe made of the attribute columns, including the missing ID field" do
|
112
|
+
m1 = customer_model_class.new
|
113
|
+
expect( m1.to_ot ).to be_a_kind_of Octothorpe
|
114
|
+
expect( m1.to_ot.to_h ).to eq( {id: nil, name: nil, price:nil, groups:nil} )
|
115
|
+
|
116
|
+
m2 = customer_model_class.new(1)
|
117
|
+
m2.read
|
118
|
+
expect( m2.to_ot ).to be_a_kind_of Octothorpe
|
119
|
+
expect( m2.to_ot ).to eq records_as_ot[0]
|
120
|
+
|
121
|
+
m2 = customer_model_class.new(2)
|
122
|
+
m2.read
|
123
|
+
expect( m2.to_ot ).to be_a_kind_of Octothorpe
|
124
|
+
expect( m2.to_ot ).to eq records_as_ot[1]
|
125
|
+
end
|
126
|
+
|
127
|
+
end # of #to_ot
|
128
|
+
|
129
|
+
|
130
|
+
describe "#map_to_model" do
|
131
|
+
|
132
|
+
it "sets the columns" do
|
133
|
+
cm = customer_model_class.new
|
134
|
+
cm.map_to_model(records.last)
|
135
|
+
|
136
|
+
expect( cm.groups ).to eq "trains,school"
|
137
|
+
end
|
138
|
+
|
139
|
+
end # of #map_to_model
|
140
|
+
|
141
|
+
|
142
|
+
describe "#map_to_interface" do
|
143
|
+
|
144
|
+
it "returns the columns" do
|
145
|
+
cm = customer_model_class.new
|
146
|
+
cm.map_to_model(records.last)
|
147
|
+
|
148
|
+
expect( cm.map_to_interface ).to be_an Octothorpe
|
149
|
+
expect( cm.map_to_interface.>>.groups ).to eq( "trains,school" )
|
150
|
+
end
|
151
|
+
|
152
|
+
it "includes the ID field" do
|
153
|
+
cm = customer_model_class.new(2).read
|
154
|
+
|
155
|
+
expect( cm.map_to_interface ).to be_an Octothorpe
|
156
|
+
expect( cm.map_to_interface.keys ).to include(:id)
|
157
|
+
end
|
158
|
+
|
159
|
+
end # of #map_to_interface
|
160
|
+
|
161
|
+
|
162
|
+
describe "#create" do
|
163
|
+
|
164
|
+
it "calls map_to_interface to get record data" do
|
165
|
+
m = customer_model_class.new(5)
|
166
|
+
|
167
|
+
expect( m ).to receive(:map_to_interface).and_call_original
|
168
|
+
|
169
|
+
m.name = "Lurch"
|
170
|
+
m.create
|
171
|
+
end
|
172
|
+
|
173
|
+
end # of #create
|
174
|
+
|
175
|
+
|
176
|
+
describe "#read" do
|
177
|
+
|
178
|
+
it "calls read on the interface" do
|
179
|
+
expect( model.interface ).to receive(:read).with(2).and_call_original
|
180
|
+
model.read
|
181
|
+
end
|
182
|
+
|
183
|
+
it "sets the attribute columns using map_to_model" do
|
184
|
+
ot = records_as_ot.last
|
185
|
+
allow( model.interface ).to receive(:read).and_return( ot )
|
186
|
+
|
187
|
+
cm = customer_model_class.new(1).read
|
188
|
+
expect( cm.name ).to eq ot.>>.name
|
189
|
+
expect( cm.price ).to eq ot.>>.price
|
190
|
+
expect( cm.groups ).to eq ot.>>.groups
|
191
|
+
end
|
192
|
+
|
193
|
+
context "if the interface.read returns an empty Octothorpe" do
|
194
|
+
let(:missing) { customer_model_class.new(99) }
|
195
|
+
|
196
|
+
it "doesn't throw an exception" do
|
197
|
+
expect{ missing.read }.not_to raise_exception
|
198
|
+
end
|
199
|
+
|
200
|
+
it "raises an error alert" do
|
201
|
+
expect( missing.read.model_status ).to eq :error
|
202
|
+
expect( missing.read.alerts.first.type ).to eq :error
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end # of #read
|
207
|
+
|
208
|
+
|
209
|
+
describe "#update" do
|
210
|
+
|
211
|
+
it "raises a Pod4Error if model status is :unknown" do
|
212
|
+
expect( model.model_status ).to eq :unknown
|
213
|
+
expect{ model.update }.to raise_exception Pod4::Pod4Error
|
214
|
+
end
|
215
|
+
|
216
|
+
it "raises a Pod4Error if model status is :deleted" do
|
217
|
+
model2.delete
|
218
|
+
expect{ model2.update }.to raise_exception Pod4::Pod4Error
|
219
|
+
end
|
220
|
+
|
221
|
+
it "calls map_to_interface to get record data" do
|
222
|
+
expect( model3 ).to receive(:map_to_interface)
|
223
|
+
model3.update
|
224
|
+
end
|
225
|
+
|
226
|
+
end # of #update
|
227
|
+
|
228
|
+
|
229
|
+
describe "#delete" do
|
230
|
+
|
231
|
+
it "raises a Pod4Error if model status is :unknown" do
|
232
|
+
expect( model.model_status ).to eq :unknown
|
233
|
+
expect{ model.delete }.to raise_exception Pod4::Pod4Error
|
234
|
+
end
|
235
|
+
|
236
|
+
it "raises a Pod4Error if model status is :deleted"do
|
237
|
+
model2.delete
|
238
|
+
expect{ model2.delete }.to raise_exception Pod4::Pod4Error
|
239
|
+
end
|
240
|
+
|
241
|
+
it "still gives you full access to the data after a delete" do
|
242
|
+
model2.delete
|
243
|
+
|
244
|
+
expect( model2.name ).to eq records_as_ot[2].>>.name
|
245
|
+
expect( model2.price ).to eq records_as_ot[2].>>.price
|
246
|
+
end
|
247
|
+
|
248
|
+
it "sets status to :deleted" do
|
249
|
+
model2.delete
|
250
|
+
expect( model2.model_status ).to eq :deleted
|
251
|
+
end
|
252
|
+
|
253
|
+
end # of #delete
|
254
|
+
|
255
|
+
end
|
256
|
+
|