repository-base 0.4.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.
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'