pod4 0.6.2

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.
data/lib/pod4.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'logger'
2
+ require 'devnull'
3
+
4
+ require_relative 'pod4/param'
5
+ require_relative 'pod4/basic_model'
6
+ require_relative 'pod4/model'
7
+ require_relative 'pod4/alert'
8
+
9
+
10
+
11
+ ##
12
+ # Pod4, which:
13
+ #
14
+ # * will gather data from absolutely anything. Nebulous, Sequel, Pg,
15
+ # TinyTds, whatever. Add your own on the fly.
16
+ #
17
+ # * will allow you to define models which are genuinely represent the data
18
+ # your way, not the way the data source sees it.
19
+ #
20
+ # * is hopefully simple and clean; just a very light helper layer with the
21
+ # absolute minimum of magic or surprises for the developer.
22
+ #
23
+ # For more information:
24
+ #
25
+ # * There is a short tutorial in the readme.
26
+ #
27
+ # * you should look at the contract Pod4 makes with its
28
+ # callers -- you should find all that you need in the classes Pod4::Interface
29
+ # and Pod4::Model.
30
+ #
31
+ # * Or, read the tests, of course.
32
+ #
33
+ module Pod4
34
+
35
+
36
+ ##
37
+ # If you have a logger instance, set it here to have Pod4 models and
38
+ # interfaces write to it.
39
+ #
40
+ def self.set_logger(instance)
41
+ Param.set(:logger, instance)
42
+ end
43
+
44
+
45
+ ##
46
+ # Return a logger instance if you set one using set_logger.
47
+ # Otherwise, return a logger instance that points to a DevNull IO object.
48
+ #
49
+ def self.logger
50
+ Param.get(:logger) || Logger.new( DevNull.new )
51
+ end
52
+
53
+
54
+ end
data/md/fixme.md ADDED
@@ -0,0 +1,32 @@
1
+ Things I Wish I Could Do
2
+ ========================
3
+
4
+ * TinyTds gem fails to cast date fields as date. Nothing we can do about that,
5
+ AFAICS.
6
+
7
+ * PG gem raises lots of "please cast this type explicitly" warnings for money,
8
+ numeric types. There is no documentation for how to do this, and apparently
9
+ no-one knows how /O\
10
+
11
+
12
+ Things To Do
13
+ ============
14
+
15
+ * I had a note here on how SequelInterface does not support the table and
16
+ quoted_table variables. Well, these definitely aren't part of the contract:
17
+ how would NebulousInterface support them? But there might be an issue with
18
+ passing selection parameters to SequelInterface.list if the schema is set. We
19
+ need to tie down a test for that and fix it if it exists.
20
+
21
+ * PgInterface works pretty well for the PG gem, but not the pg_jruby gem. We
22
+ need to take a rather more paranoid approach to the thing; how we go about
23
+ adding test coverage for this I have literally no idea...
24
+
25
+ * TinyTDS just updated to 1.0 and ... fell over. We need to work out what's
26
+ going on there.
27
+
28
+ * Ideally interfaces should support parameterised insertion. Ideally in a
29
+ manner consistent for all interfaces...
30
+
31
+ * We should have a test suite for jRuby.
32
+
data/md/roadmap.md ADDED
@@ -0,0 +1,69 @@
1
+ Connection Object
2
+ =================
3
+
4
+ PgInterface and TdsInterface both take a connection Hash, which is all very
5
+ well, but it means that we are running one database connection per model.
6
+ Presumably this is a bad idea. :-)
7
+
8
+ This actually hasn't come up in my own use of Pod4 -- for complex reasons I'm
9
+ either using SequelInterface or running transient jobs which start up a couple
10
+ of models, do some work, and then stop entirely -- but it _is_ rather silly.
11
+
12
+ Connection is baked into those interfaces, and interface dependant. So I'm
13
+ thinking in terms of a memoising object that stores the connection hash and
14
+ then gets passed to the interface. When the interface wants a connection, then
15
+ it asks the connection object. If the connection object doesn't have one, then
16
+ the interface connects, and gives the connection to the connection object.
17
+
18
+
19
+ Transactions
20
+ ============
21
+
22
+ We really need this, because without it we can't even pretend to be doing
23
+ proper pessimistic locking.
24
+
25
+ I've got a pretty solid idea for a nice, simple way to make this happen. It
26
+ will be in place soon.
27
+
28
+
29
+ Migrations
30
+ ==========
31
+
32
+ This will almost certainly be something crude -- since we don't really control
33
+ the database connection in the same way as, say, ActiveRecord -- but I honestly
34
+ think it's a worthwhile feature. Just having something that you can version
35
+ control and run to update a data model is enough, really.
36
+
37
+ I'm not yet sure of the least useless way to implement it. Again, I favour SQL
38
+ as the DSL.
39
+
40
+
41
+ JDBC-SQL interface
42
+ ==================
43
+
44
+ For the jdbc-msssqlserver gem. Doable ... I *think*.
45
+
46
+ driver = Java::com.microsoft.sqlserver.jdbc.SQLServerDriver.new
47
+ props = java.util.Properties.new
48
+ props.setProperty("user", "username")
49
+ props.setProperty("password", "password")
50
+ url = 'jdbc:sqlserver://servername;instanceName=instance;databaseName=DbName;'
51
+
52
+ conn = driver.connect(url, props)
53
+ #or maybe conn = driver.get_connection(url, "username", "password")
54
+
55
+ stmt = conn.create_statement
56
+ sql = %Q|blah;|
57
+
58
+ rsS = stmt.execute_query(sql)
59
+
60
+ while (rsS.next) do
61
+ veg = Hash.new
62
+ veg["vegName"] = rsS.getObject("name")
63
+ # etc
64
+ end
65
+
66
+ stmt.close
67
+ conn.close
68
+
69
+ see https://github.com/jruby/jruby/wiki/JDBC
data/pod4.gemspec ADDED
@@ -0,0 +1,49 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'pod4/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pod4"
7
+ spec.version = Pod4::VERSION
8
+ spec.authors = ["Andy Jones"]
9
+ spec.email = ["andy.jones@twosticksconsulting.co.uk"]
10
+ spec.summary = %q|Totally not an ORM|
11
+ spec.description = <<~DESC
12
+ Provides a simple, common framework to talk to a bunch of data sources,
13
+ using model classes which consist of a bare minimum of DSL plus vanilla Ruby
14
+ inheritance.
15
+ DESC
16
+
17
+ spec.homepage = "https://bitbucket.org/andy-twosticks/pod4"
18
+ spec.license = "MIT"
19
+
20
+ spec.files = `hg status -macn0`.split("\x0")
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.extra_rdoc_files = spec.files.grep(%r{^md/})
26
+
27
+ spec.add_runtime_dependency "devnull", '~>0.1'
28
+ spec.add_runtime_dependency "octothorpe", '~>0.1'
29
+
30
+ # for bundler, management, etc etc
31
+ spec.add_development_dependency "bundler", "~> 1.6"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec"
34
+ spec.add_development_dependency "rdoc"
35
+
36
+ # For testing
37
+ spec.add_development_dependency "sequel"
38
+ spec.add_development_dependency "sqlite3"
39
+ spec.add_development_dependency "tiny_tds"
40
+ spec.add_development_dependency "pg"
41
+ spec.add_development_dependency "nebulous"
42
+
43
+ # Development tools
44
+ spec.add_development_dependency "pry"
45
+ spec.add_development_dependency "pry-doc"
46
+ spec.add_development_dependency "ripper-tags"
47
+ spec.add_development_dependency "geminabox"
48
+
49
+ end
data/spec/README.md ADDED
@@ -0,0 +1,19 @@
1
+ Some Notes On Testing
2
+ =====================
3
+
4
+ Some of the interface tests require an actual database to run.
5
+
6
+ If you want to run the whole test suite you will have to configure
7
+ spec/fixtures/database.yaml so that you can connect to both an SQL Server
8
+ database and a Postgres database.
9
+
10
+ In each case the database name you need to use is hardcoded: pod4_test. (Pod4
11
+ assumes that the schema names can be hardcoded, and since we create and wipe
12
+ tables in these tests, you should really have a seperate database for them
13
+ anyway.)
14
+
15
+ Sequel
16
+ ------
17
+ Note that the Sequel ORM adapter does **not** require a database -- we
18
+ currently use an in-memory sqlite database instead.
19
+
@@ -0,0 +1,173 @@
1
+ require 'pod4/alert'
2
+
3
+
4
+ describe Alert do
5
+
6
+ let(:err) { StandardError.new('whoa') }
7
+
8
+
9
+ describe '#new' do
10
+
11
+ it 'requires a type of error, warning, info or success; and a message' do
12
+ expect{ Alert.new }.to raise_exception ArgumentError
13
+ expect{ Alert.new(nil) }.to raise_exception ArgumentError
14
+ expect{ Alert.new('foo') }.to raise_exception ArgumentError
15
+
16
+ [:baz, :werning, nil, :note, :debug].each do |badType|
17
+ expect{ Alert.new(badType, 'foo') }.
18
+ to raise_exception(ArgumentError), "Alert.new(#{badType.inspect}...)"
19
+
20
+ end
21
+
22
+ [:error, :warning, :info, 'success', 'error'].each do |type|
23
+ expect{ Alert.new(type, 'foo') }.
24
+ not_to raise_exception, "Alert.new(#{type.inspect}...)"
25
+
26
+ end
27
+
28
+ end
29
+
30
+ it 'allows the message to be a string' do
31
+ expect{ Alert.new(:warning, 'foo') }.not_to raise_exception
32
+ end
33
+
34
+ it 'allows the message to be an exception' do
35
+ expect{ Alert.new(:error, err) }.not_to raise_exception
36
+ end
37
+
38
+ it 'allows entry of a field name' do
39
+ expect{ Alert.new(:success, 'foo', 'bar') }.not_to raise_exception
40
+ end
41
+
42
+ end
43
+ ##
44
+
45
+
46
+ describe '#type' do
47
+
48
+ it 'reflects the type passed to #new' do
49
+ expect( Alert.new(:info, 'foo').type ).to eq :info
50
+ expect( Alert.new('info', 'foo').type ).to eq :info
51
+ end
52
+
53
+ end
54
+ ##
55
+
56
+
57
+ describe '#message' do
58
+
59
+ let(:al1) { Alert.new(:info, 'foo') }
60
+ let(:al2) { Alert.new(:info, err) }
61
+
62
+ it 'reflects the message passed to #new' do
63
+ expect( al1.message ).to eq 'foo'
64
+ expect( al2.message ).to eq 'whoa'
65
+ end
66
+
67
+ it 'allows you to change it' do
68
+ al1.message = "one"; al2.message = "two"
69
+
70
+ expect( al1.message ).to eq 'one'
71
+ expect( al2.message ).to eq 'two'
72
+ end
73
+
74
+ end
75
+ ##
76
+
77
+
78
+ describe '#field' do
79
+
80
+ it 'defaults to nil' do
81
+ expect( Alert.new(:error, 'baz').field ).to be_nil
82
+ end
83
+
84
+ it 'reflects the field passed to #new' do
85
+ expect( Alert.new(:info, 'fld1', 'foo').field ).to eq :fld1
86
+ end
87
+
88
+ it 'allows you to change it' do
89
+ al = Alert.new(:success, :boris, 'foo')
90
+ al.field = :yuri
91
+
92
+ expect( al.field ).to eq :yuri
93
+ end
94
+
95
+ end
96
+ ##
97
+
98
+
99
+ describe '#exception' do
100
+
101
+ it 'defaults to nil' do
102
+ expect( Alert.new('warning', 'one').exception ).to be_nil
103
+ end
104
+
105
+ it 'reflects the exception passed to #new' do
106
+ expect( Alert.new(:info, err).exception ).to eq err
107
+ end
108
+
109
+ end
110
+ ##
111
+
112
+
113
+ describe '#log' do
114
+
115
+ after do
116
+ Pod4::Param.reset
117
+ end
118
+
119
+ let(:alert_error) { Alert.new(:error, 'error') }
120
+ let(:alert_warning) { Alert.new(:warning, 'warning') }
121
+ let(:alert_info) { Alert.new(:info, 'info') }
122
+ let(:alert_success) { Alert.new(:success, 'success') }
123
+
124
+ it 'accepts a context field' do
125
+ expect{ alert_warning.log }.not_to raise_exception
126
+ expect{ alert_warning.log('foo') }.not_to raise_exception
127
+ expect{ alert_warning.log('foo', 2) }.to raise_exception ArgumentError
128
+ end
129
+
130
+ it 'outputs itself to the log, at the right level' do
131
+ lugger = double(Logger)
132
+ Pod4.set_logger lugger
133
+
134
+ expect(lugger).to receive(:error)
135
+ alert_error.log
136
+
137
+ expect(lugger).to receive(:warn)
138
+ alert_warning.log
139
+
140
+ expect(lugger).to receive(:info)
141
+ alert_info.log
142
+
143
+ expect(lugger).to receive(:info)
144
+ alert_success.log
145
+ end
146
+
147
+ it 'returns self' do
148
+ expect( alert_error.log ).to eq alert_error
149
+ end
150
+
151
+ end
152
+ ##
153
+
154
+
155
+ context 'when collected in an array' do
156
+
157
+ it 'orders itself by severity of type' do
158
+ arr = []
159
+ arr << Alert.new(:warning, 'one')
160
+ arr << Alert.new(:error, 'two')
161
+ arr << Alert.new(:info, 'three')
162
+ arr << Alert.new(:success, 'four')
163
+
164
+ expect( arr.sort.map{|a| a.type } ).
165
+ to eq([:error,:warning,:info,:success])
166
+
167
+ end
168
+
169
+ end
170
+ ##
171
+
172
+ end
173
+
@@ -0,0 +1,220 @@
1
+ require 'octothorpe'
2
+
3
+ require 'pod4/basic_model'
4
+ require 'pod4/null_interface'
5
+
6
+
7
+ ##
8
+ # We define a model class to test, since in normal operation we would never use
9
+ # Model directly, and since it needs an inner Interface.
10
+ #
11
+ # We can't use a mock for the interface -- class definitions fall outside the
12
+ # RSpec DSL as far as I can tell, so I can neither create a mock here or inject
13
+ # it. Which means we can't mock the interface in the rest of the test either;
14
+ # any mock we created would not get called.
15
+ #
16
+ # But: we want to test that Model calls Interface correctly.
17
+ #
18
+ # We do have what appears to be a perfectly sane way of testing. We can define
19
+ # an inner class based on the genuinely existing, non-mock NullInterface class;
20
+ # and then define expectations on it. When we do this, Rspec fails to pass the
21
+ # call on to the object, unless we specifically say `.and_call_original`
22
+ # instead of `.and_return`.
23
+ #
24
+ # This is actually quite nice, but more than a little confusing when you see it
25
+ # for the first time. Its use isn't spelled out in the RSpec docs AFAICS.
26
+ #
27
+ class WeirdModel < Pod4::BasicModel
28
+ set_interface NullInterface.new(:id, :name, :price, :groups, [])
29
+
30
+ def fake_an_alert(*args)
31
+ add_alert(*args) #protected method
32
+ end
33
+
34
+ def reset_alerts; @alerts = []; end
35
+ end
36
+
37
+
38
+
39
+ describe 'WeirdModel' do
40
+ let(:model) { WeirdModel.new(20) }
41
+
42
+
43
+ describe 'Model.set_interface' do
44
+ it 'requires an Interface object' do
45
+ expect( WeirdModel ).to respond_to(:set_interface).with(1).argument
46
+ end
47
+
48
+ # it 'sets interface' - covered by the interface test
49
+ end
50
+ ##
51
+
52
+
53
+ describe 'Model.interface' do
54
+ it 'is the interface object' do
55
+ expect( WeirdModel.interface ).to be_a_kind_of NullInterface
56
+ expect( WeirdModel.interface.id_fld ).to eq :id
57
+ end
58
+ end
59
+ ##
60
+
61
+
62
+ describe '#new' do
63
+
64
+ it 'takes an optional ID' do
65
+ expect{ WeirdModel.new }.not_to raise_exception
66
+ expect{ WeirdModel.new(1) }.not_to raise_exception
67
+ end
68
+
69
+ it 'sets the ID attribute' do
70
+ expect( WeirdModel.new(23).model_id ).to eq 23
71
+ end
72
+
73
+ it 'sets the status to empty' do
74
+ expect( WeirdModel.new.model_status ).to eq :empty
75
+ end
76
+
77
+ it 'initializes the alerts attribute' do
78
+ expect( WeirdModel.new.alerts ).to eq([])
79
+ end
80
+
81
+ end
82
+ ##
83
+
84
+
85
+ describe '#interface' do
86
+ it 'returns the interface set in the class definition, again' do
87
+ expect( WeirdModel.new.interface ).to be_a_kind_of NullInterface
88
+ expect( WeirdModel.new.interface.id_fld ).to eq :id
89
+ end
90
+ end
91
+ ##
92
+
93
+
94
+ describe '#alerts' do
95
+ it 'returns the list of alerts against the model' do
96
+ cm = WeirdModel.new
97
+ cm.fake_an_alert(:warning, :foo, 'one')
98
+ cm.fake_an_alert(:error, :bar, 'two')
99
+
100
+ expect( cm.alerts.size ).to eq 2
101
+ expect( cm.alerts.map{|a| a.message} ).to match_array(%w|one two|)
102
+ end
103
+ end
104
+ ##
105
+
106
+
107
+ describe '#add_alert' do
108
+ # add_alert is a protected method, which is only supposed to be called
109
+ # within the validate method of a subclass of Method. So we test it by
110
+ # calling our alert faking method
111
+
112
+ it 'requires type, message or type, field, message' do
113
+ expect{ model.fake_an_alert }.to raise_exception ArgumentError
114
+ expect{ model.fake_an_alert(nil) }.to raise_exception ArgumentError
115
+ expect{ model.fake_an_alert('foo') }.to raise_exception ArgumentError
116
+
117
+ expect{ model.fake_an_alert(:error, 'foo') }.not_to raise_exception
118
+ expect{ model.fake_an_alert(:warning, :name, 'bar') }.
119
+ not_to raise_exception
120
+
121
+ end
122
+
123
+ it 'only allows valid types' do
124
+ [:brian, :werning, nil, :alert, :danger].each do |l|
125
+ expect{ model.fake_an_alert(l, 'foo') }.to raise_exception ArgumentError
126
+ end
127
+
128
+ [:warning, :error, :success, :info].each do |l|
129
+ expect{ model.fake_an_alert(l, 'foo') }.not_to raise_exception
130
+ end
131
+
132
+ end
133
+
134
+ it 'creates an Alert and adds it to @alerts' do
135
+ lurch = 'Dnhhhhhh'
136
+ model.fake_an_alert(:error, :price, lurch)
137
+
138
+ expect( model.alerts.size ).to eq 1
139
+ expect( model.alerts.first ).to be_a_kind_of Pod4::Alert
140
+ expect( model.alerts.first.message ).to eq lurch
141
+ end
142
+
143
+ it 'sets @model_status if the type is worse than @model_status' do
144
+ model.fake_an_alert(:warning, :price, 'xoo')
145
+ expect( model.model_status ).to eq :warning
146
+
147
+ model.fake_an_alert(:success, :price, 'flom')
148
+ expect( model.model_status ).to eq :warning
149
+
150
+ model.fake_an_alert(:info, :price, 'flom')
151
+ expect( model.model_status ).to eq :warning
152
+
153
+ model.fake_an_alert(:error, :price, 'qar')
154
+ expect( model.model_status ).to eq :error
155
+
156
+ model.fake_an_alert(:warning, :price, 'drazq')
157
+ expect( model.model_status ).to eq :error
158
+ end
159
+
160
+ it 'ignores a new alert if identical to an existing one' do
161
+ lurch = 'Dnhhhhhh'
162
+ 2.times { model.fake_an_alert(:error, :price, lurch) }
163
+
164
+ expect( model.alerts.size ).to eq 1
165
+ end
166
+
167
+ end
168
+ ##
169
+
170
+
171
+ describe '#clear_alerts' do
172
+ before do
173
+ model.fake_an_alert(:error, "bad stuff")
174
+ model.clear_alerts
175
+ end
176
+
177
+ it 'resets the @alerts array' do
178
+ expect( model.alerts ).to eq([])
179
+ end
180
+
181
+ it 'sets model_status to :okay' do
182
+ expect( model.model_status ).to eq :okay
183
+ end
184
+
185
+
186
+ end
187
+ ##
188
+
189
+
190
+ describe '#raise_exceptions' do
191
+
192
+ it 'is also known as .or_die' do
193
+ cm = WeirdModel.new
194
+ expect( cm.method(:raise_exceptions) ).to eq( cm.method(:or_die) )
195
+ end
196
+
197
+ it 'raises ValidationError if model status is :error' do
198
+ model.fake_an_alert(:error, :price, 'qar')
199
+ expect{ model.raise_exceptions }.to raise_exception Pod4::ValidationError
200
+ end
201
+
202
+ it 'does nothing if model status is not :error' do
203
+ expect{ model.raise_exceptions }.not_to raise_exception
204
+
205
+ model.fake_an_alert(:info, :price, 'qar')
206
+ expect{ model.raise_exceptions }.not_to raise_exception
207
+
208
+ model.fake_an_alert(:success, :price, 'qar')
209
+ expect{ model.raise_exceptions }.not_to raise_exception
210
+
211
+ model.fake_an_alert(:warning, :price, 'qar')
212
+ expect{ model.raise_exceptions }.not_to raise_exception
213
+ end
214
+
215
+ end
216
+ ##
217
+
218
+
219
+ end
220
+
@@ -0,0 +1,5 @@
1
+ class DocNoPending < RSpec::Core::Formatters::DocumentationFormatter
2
+ RSpec::Core::Formatters.register self, :example_pending
3
+
4
+ def example_pending(notifications); end
5
+ end
@@ -0,0 +1,13 @@
1
+ DB = {}
2
+
3
+ DB[:tds] =
4
+ { dataserver: 'SQLDEV',
5
+ username: 'pod4test',
6
+ password: 'pod4test' }
7
+
8
+ DB[:pg] =
9
+ { host: 'centos7andy',
10
+ dbname: 'pod4_test',
11
+ user: 'pod4test',
12
+ password: 'pod4test' }
13
+