repository-base 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +27 -0
  5. data/.travis.yml +6 -0
  6. data/.yardopts +1 -0
  7. data/CHANGELOG.md +128 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE +22 -0
  10. data/README.md +132 -0
  11. data/Rakefile +33 -0
  12. data/bin/bundle +105 -0
  13. data/bin/htmldiff +29 -0
  14. data/bin/kramdown +29 -0
  15. data/bin/ldiff +29 -0
  16. data/bin/rake +29 -0
  17. data/bin/rspec +29 -0
  18. data/bin/rubocop +29 -0
  19. data/bin/ruby-parse +29 -0
  20. data/bin/ruby-rewrite +29 -0
  21. data/bin/setup +43 -0
  22. data/bin/yard +29 -0
  23. data/bin/yardoc +29 -0
  24. data/bin/yri +29 -0
  25. data/doc/Repository.html +128 -0
  26. data/doc/Repository/Base.html +1248 -0
  27. data/doc/Repository/Base/Internals.html +133 -0
  28. data/doc/Repository/Base/Internals/RecordDeleter.html +687 -0
  29. data/doc/Repository/Base/Internals/RecordSaver.html +816 -0
  30. data/doc/Repository/Base/Internals/RecordUpdater.html +1026 -0
  31. data/doc/Repository/Base/Internals/SlugFinder.html +986 -0
  32. data/doc/_index.html +176 -0
  33. data/doc/class_list.html +51 -0
  34. data/doc/css/common.css +1 -0
  35. data/doc/css/full_list.css +58 -0
  36. data/doc/css/style.css +499 -0
  37. data/doc/file.CHANGELOG.html +240 -0
  38. data/doc/file.README.html +218 -0
  39. data/doc/file_list.html +61 -0
  40. data/doc/frames.html +17 -0
  41. data/doc/index.html +218 -0
  42. data/doc/js/app.js +248 -0
  43. data/doc/js/full_list.js +216 -0
  44. data/doc/js/jquery.js +4 -0
  45. data/doc/method_list.html +363 -0
  46. data/doc/top-level-namespace.html +110 -0
  47. data/lib/repository/base.rb +115 -0
  48. data/lib/repository/base/internals/internals.rb +6 -0
  49. data/lib/repository/base/internals/record_deleter.rb +46 -0
  50. data/lib/repository/base/internals/record_saver.rb +58 -0
  51. data/lib/repository/base/internals/record_updater.rb +54 -0
  52. data/lib/repository/base/internals/slug_finder.rb +70 -0
  53. data/lib/repository/base/version.rb +12 -0
  54. data/repository-base.gemspec +37 -0
  55. data/spec/repository/base_spec.rb +398 -0
  56. data/spec/spec_helper.rb +14 -0
  57. metadata +281 -0
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module Repository
5
+ # Base class for Repository in Data Mapper pattern.
6
+ class Base
7
+ # Gem version, following [RubyGems.org](https://rubygems.org) and
8
+ # [SemVer](http://semver.org/) conventions.
9
+ VERSION = '0.4.0'
10
+ end
11
+ end
12
+ # :nocov:
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'repository/base/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'repository-base'
9
+ spec.version = Repository::Base::VERSION
10
+ spec.authors = ['Jeff Dickey']
11
+ spec.email = ['jdickey@seven-sigma.com']
12
+ spec.summary = %q(Base implementation of Repository layer in Data Mapper pattern.)
13
+ spec.description = %q(Base implementation of Repository layer in Data Mapper pattern. See README for details.)
14
+ spec.homepage = 'https://github.com/jdickey/repository-base'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ # This Gem installs no executables of its own, and developer stubs for bin/setup will be overwritten
19
+ # spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ['lib']
22
+ spec.metadata["yard.run"] = "yri" # use "yard" to build full HTML docs.
23
+
24
+ spec.add_dependency 'repository-support', '>= 0.1.1'
25
+
26
+ spec.add_development_dependency 'activemodel', '~> 4.2', '>= 4.2.10'
27
+ spec.add_development_dependency 'bundler', '~> 1.16'
28
+ spec.add_development_dependency 'rake', '~> 12.3', '>= 12.3.0'
29
+ spec.add_development_dependency 'rspec', '~> 3.7'
30
+ spec.add_development_dependency 'rubocop', '~> 0.52', '>= 0.52.1'
31
+ spec.add_development_dependency 'simplecov', '~> 0.15', '>= 0.15.1'
32
+ spec.add_development_dependency 'awesome_print', '>= 1.8.0'
33
+
34
+ spec.add_development_dependency 'fancy-open-struct'
35
+ spec.add_development_dependency 'yard'
36
+ spec.add_development_dependency 'kramdown', '~> 1.16'
37
+ end
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'active_model'
5
+
6
+ require 'repository/support'
7
+
8
+ # FIXME: Periodically try disabling this after updating to new release of
9
+ # ActiveModel and see if they've fixed the conflict w/stdlib Forwardable.
10
+ # This is per https://github.com/cequel/cequel/issues/193 - another epic
11
+ # ActiveFail.
12
+ module Forwardable
13
+ remove_method :delegate
14
+ end
15
+
16
+ # Test entity class; simply a wrapper around attributes and comparison of same.
17
+ class BaseEntityClass
18
+ include Comparable
19
+ extend Repository::Support::TestAttributeContainer
20
+
21
+ init_empty_attribute_container
22
+
23
+ def initialize(attributes_in = {})
24
+ @attributes = attributes_in
25
+ end
26
+
27
+ def <=>(other)
28
+ attributes <=> other.attributes
29
+ end
30
+ end
31
+
32
+ describe Repository::Base do
33
+ let(:all_dao_records) { [] }
34
+ let(:save_successful) { true }
35
+ let(:test_dao) do
36
+ ret = Class.new do
37
+ attr_reader :attributes
38
+ include ActiveModel::Validations
39
+
40
+ def initialize(attribs = {})
41
+ @attributes = attribs
42
+ end
43
+
44
+ def self.all
45
+ @all_records.to_a # silence RuboCop Style/TrivialAccessors cop
46
+ end
47
+
48
+ def self.delete(identifier)
49
+ record = @all_records.detect { |r| r.attributes[:slug] == identifier }
50
+ if record
51
+ @all_records.delete record
52
+ 1
53
+ else
54
+ errors.add :slug, "not found: '#{identifier}'"
55
+ 0
56
+ end
57
+ end
58
+
59
+ def self.where(conditions = {})
60
+ ret = @all_records.to_a.select do |record|
61
+ conditions.delete_if do |field, value|
62
+ record.attributes[field.to_sym] == value
63
+ end.empty?
64
+ end
65
+ ret
66
+ end
67
+
68
+ def save
69
+ self.class.instance_variable_get(:@save_flag).tap do |flag|
70
+ unless flag
71
+ errors.add :foo, 'is foo'
72
+ errors.add :something, 'is broken'
73
+ end
74
+ end
75
+ end
76
+
77
+ def update
78
+ StoreResult::Success.new 'succeeded'
79
+ end
80
+ end
81
+ ret.instance_variable_set :@save_flag, save_successful
82
+ ret.instance_variable_set :@all_records, all_dao_records
83
+ ret
84
+ end
85
+ let(:test_factory) do
86
+ Class.new do
87
+ def self.create(record)
88
+ BaseEntityClass.new record.attributes
89
+ end
90
+ end
91
+ end
92
+ let(:obj) { described_class.new dao: test_dao, factory: test_factory }
93
+
94
+ it 'has a version number' do
95
+ expect(Repository::Base::VERSION).not_to be nil
96
+ end
97
+
98
+ describe 'instantiation' do
99
+ it 'requires two keyword arguments, :factory and :dao' do
100
+ message = 'missing keywords: factory, dao'
101
+ expect { described_class.new }.to raise_error ArgumentError, message
102
+ end
103
+
104
+ it 'requires the :dao argument to be a class' do
105
+ expect { described_class.new dao: nil, factory: nil }.to \
106
+ raise_error ArgumentError, 'the :dao argument must be a Class'
107
+ end
108
+
109
+ describe 'requires the :factory argument to' do
110
+ it 'be a class' do
111
+ expect { described_class.new dao: test_dao, factory: nil }.to \
112
+ raise_error ArgumentError, 'the :factory argument must be a Class'
113
+ end
114
+ end # describe 'requires the :factory argument to'
115
+ end # describe 'instantiation'
116
+
117
+ describe 'has an #add method that' do
118
+ describe 'takes one argument' do
119
+ it 'that is required' do
120
+ method = obj.method :add
121
+ expect(method.arity).to eq 1
122
+ end
123
+
124
+ describe 'with an #attributes method that returns' do
125
+ it 'an Enumerable' do
126
+ arg = 'q'
127
+ message = /undefined method .attributes. .*/
128
+ expect { obj.add arg }.to raise_error NoMethodError, message
129
+ entity = Struct.new(:attributes).new arg
130
+ message = /undefined method .to_hash. .*/
131
+ expect { obj.add entity }.to raise_error NoMethodError, message
132
+ end
133
+
134
+ it 'an object with a #to_hash method' do
135
+ attributes_class = Class.new do
136
+ def to_hash
137
+ { q: 'a' }
138
+ end
139
+ end
140
+ entity = Class.new do
141
+ attr_reader :attributes
142
+
143
+ def initialize(attributes)
144
+ @attributes = attributes
145
+ end
146
+ end.new(attributes_class.new)
147
+ expect { obj.add entity }.not_to raise_error
148
+ end
149
+ end
150
+ end # describe 'takes one argument'
151
+
152
+ describe 'returns a StoreResult' do
153
+ let(:entity) { BaseEntityClass.new foo: true }
154
+ let(:result) { obj.add entity }
155
+
156
+ context 'for a successful save' do
157
+ let(:save_successful) { true }
158
+
159
+ it 'a #success method returning true' do
160
+ expect(result).to be_success
161
+ end
162
+
163
+ describe 'an #entity method which returns an entity that' do
164
+ it 'is an entity, not a record' do
165
+ expect(result.entity).not_to respond_to :save
166
+ end
167
+
168
+ it 'has the correct attributes' do
169
+ expect(result.entity.attributes).to eq entity.attributes
170
+ end
171
+ end # describe 'an #entity method which returns an entity that'
172
+ end # context 'for a successful save'
173
+
174
+ context 'for an unsuccessful save' do
175
+ let(:save_successful) { false }
176
+
177
+ it 'a #success method returning false' do
178
+ expect(result).not_to be_success
179
+ end
180
+
181
+ it 'an #entity method returning nil' do
182
+ expect(result.entity).to be nil
183
+ end
184
+
185
+ it 'an #errors method returning the expected error-pair hashes' do
186
+ expected = [
187
+ { field: 'foo', message: 'is foo' },
188
+ { field: 'something', message: 'is broken' }
189
+ ]
190
+ expect(result.errors.count).to eq expected.count
191
+ expected.each { |item| expect(result.errors).to include item }
192
+ end
193
+ end # context 'for an unsuccessful save'
194
+ end # describe 'returns a StoreResult with'
195
+ end # describe 'has an #add method that'
196
+
197
+ describe 'has an #all method that' do
198
+ context 'for a repository without records' do
199
+ it 'returns an empty array' do
200
+ actual = obj.all
201
+ expect(actual).to respond_to :to_ary
202
+ expect(actual).to be_empty
203
+ end
204
+ end # context 'for a repository without records'
205
+
206
+ context 'for a repository with records' do
207
+ let(:all_dao_records) do
208
+ [
209
+ BaseEntityClass.new(attr1: 'value1'),
210
+ BaseEntityClass.new(attr1: 'value2')
211
+ ]
212
+ end
213
+
214
+ it 'returns an Array with one entry for each record in the Repository' do
215
+ actual = obj.all
216
+ expect(actual).to respond_to :to_ary
217
+ expect(actual).to eq all_dao_records
218
+ end
219
+ end # context 'for a repository with records'
220
+ end # describe 'has an #all method that'
221
+
222
+ describe 'has a #delete method that' do
223
+ let(:entity) { BaseEntityClass.new entity_attributes }
224
+ let(:entity_attributes) { { foo: 'bar', slug: 'the-slug' } }
225
+ let(:result) { obj.delete entity.attributes[:slug] }
226
+
227
+ before :each do
228
+ obj.add entity
229
+ end
230
+
231
+ context 'for an existing record, returns a StoreResult that' do
232
+ let(:all_dao_records) { [entity] }
233
+ let(:save_successful) { true }
234
+
235
+ it 'is successful' do
236
+ expect(result).to be_success
237
+ end
238
+
239
+ it 'has an entity matching the target' do
240
+ expect(result.entity.attributes.to_h).to eq entity.attributes
241
+ end
242
+
243
+ it 'reports no errors' do
244
+ expect(result.errors).to be_empty
245
+ end
246
+ end # context 'for an existing record, returns a StoreResult that'
247
+
248
+ context 'for a nonexistent record, returns a StoreResult that' do
249
+ let(:all_dao_records) { [] }
250
+ let(:save_successful) { false }
251
+
252
+ it 'is not successful' do
253
+ expect(result).not_to be_success
254
+ end
255
+
256
+ it 'has an #entity method returning nil' do
257
+ expect(result.entity).to be nil
258
+ end
259
+
260
+ it 'reports one error: that the target slug was not found' do
261
+ expect(result.errors.count).to eq 1
262
+ error = result.errors.first
263
+ expect(error[:field].to_s).to eq 'slug'
264
+ expect(error[:message]).to eq "not found: '#{entity.attributes[:slug]}'"
265
+ end
266
+ end # context 'for a nonexistent record, returns a StoreResult that'
267
+ end # describe 'has a #delete method that'
268
+
269
+ describe 'has a #find_by_slug method' do
270
+ let(:entity) { BaseEntityClass.new entity_attributes }
271
+ let(:entity_attributes) { { foo: 'bar', slug: 'the-slug' } }
272
+ let(:result) { obj.find_by_slug entity.attributes[:slug] }
273
+
274
+ context 'when called using the slug for an existing record' do
275
+ describe 'returns a result that' do
276
+ let(:all_dao_records) { [entity] }
277
+
278
+ it 'has no errors' do
279
+ expect(result.errors).to be_empty
280
+ end
281
+
282
+ it 'is successful' do
283
+ expect(result).to be_success
284
+ end
285
+
286
+ it 'has an entity field matching the original entity attributes' do
287
+ expect(result.entity.attributes).to eq entity_attributes
288
+ end
289
+ end # describe 'returns a result that'
290
+ end # context 'when called using the slug for an existing record'
291
+
292
+ context 'when called using a slug that matches no existing record' do
293
+ describe 'returns a result that' do
294
+ it 'is not successful' do
295
+ expect(result).not_to be_success
296
+ end
297
+
298
+ it 'has an entity field with a nil value' do
299
+ expect(result.entity).to be nil
300
+ end
301
+
302
+ it 'reports a single error, stating that the slug was not found' do
303
+ expect(result.errors.count).to eq 1
304
+ message = "not found: '#{entity_attributes[:slug]}'"
305
+ expected = { field: 'slug', message: message }
306
+ expect(result.errors.first).to eq expected
307
+ end
308
+ end # describe 'returns a result that'
309
+ end # context 'when called using a slug that matches no existing record'
310
+ end # describe 'has a #find_by_slug method'
311
+
312
+ describe 'has an #update method that' do
313
+ let(:entity) do
314
+ # Class that provides a stubbed `#update` method that quacks enough like
315
+ # ActiveRecord's to be useful for testing, now that `Repository::Support`
316
+ # has this (presently) shiny new `TestAttributeContainer` module that
317
+ # makes the attribute-management finagling we need easier.
318
+ class EntityClass < BaseEntityClass
319
+ attr_accessor :update_successful
320
+ attr_reader :errors
321
+
322
+ def initialize(attributes_in = {})
323
+ super attributes_in
324
+ @update_successful = true
325
+ @errors = {}
326
+ end
327
+
328
+ def update(new_attributes)
329
+ if update_successful
330
+ new_hash = attributes.merge new_attributes.to_h.symbolize_keys
331
+ @attributes = new_hash
332
+ else
333
+ @errors = {
334
+ new_attributes.keys.first.to_sym => 'is invalid'
335
+ }
336
+ end
337
+ update_successful
338
+ end
339
+ end
340
+ EntityClass.new(entity_attributes).tap do |ret|
341
+ ret.update_successful = update_success
342
+ end
343
+ end
344
+ let(:entity_attributes) { { foo: 'bar', slug: 'the-slug' } }
345
+ let(:new_attrs) { { foo: 'quux' } }
346
+ let(:result) do
347
+ obj.update identifier: entity.attributes[:slug], updated_attrs: new_attrs
348
+ end
349
+
350
+ context 'for a valid update' do
351
+ let(:all_dao_records) { [entity] }
352
+ let(:update_success) { true }
353
+
354
+ describe 'it returns a result that' do
355
+ it 'is successful' do
356
+ expect(result).to be_success
357
+ end
358
+
359
+ it 'has no errors' do
360
+ expect(result.errors).to be_empty
361
+ end
362
+
363
+ describe 'an #entity method which returns an entity that' do
364
+ it 'is an entity, not a record' do
365
+ expect(result.entity).not_to respond_to :save
366
+ end
367
+
368
+ it 'has the correct attributes' do
369
+ expected = entity_attributes.merge(new_attrs)
370
+ expect(result.entity.attributes).to eq expected
371
+ end
372
+ end # describe 'an #entity method which returns an entity that'
373
+ end # describe 'it returns a result that'
374
+ end # context 'for a valid update'
375
+
376
+ context 'for a failed update' do
377
+ let(:all_dao_records) { [entity] }
378
+ let(:update_success) { false }
379
+
380
+ describe 'it returns a result that' do
381
+ it 'is not successful' do
382
+ expect(result).not_to be_success
383
+ end
384
+
385
+ it 'has an #entity method that returns nil' do
386
+ expect(result.entity).to be nil
387
+ end
388
+
389
+ it 'an #errors method that returns the expected error-pair hashes' do
390
+ # from entity#update; see above
391
+ expect(result.errors).to respond_to :to_hash
392
+ expect(result.errors.keys.count).to eq 1
393
+ expect(result.errors[entity_attributes.keys.first]).to eq 'is invalid'
394
+ end
395
+ end # describe 'it returns a result that'
396
+ end # context 'for a failed update'
397
+ end # describe 'has an #update method that'
398
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start 'rails' do
5
+ add_filter '/gemset/'
6
+ end
7
+
8
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
9
+
10
+ require 'awesome_print'
11
+ require 'fancy-open-struct'
12
+ # require 'pry'
13
+
14
+ require 'repository/base'