pod4 0.10.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|