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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +27 -0
- data/.travis.yml +6 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +128 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +132 -0
- data/Rakefile +33 -0
- data/bin/bundle +105 -0
- data/bin/htmldiff +29 -0
- data/bin/kramdown +29 -0
- data/bin/ldiff +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/setup +43 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/doc/Repository.html +128 -0
- data/doc/Repository/Base.html +1248 -0
- data/doc/Repository/Base/Internals.html +133 -0
- data/doc/Repository/Base/Internals/RecordDeleter.html +687 -0
- data/doc/Repository/Base/Internals/RecordSaver.html +816 -0
- data/doc/Repository/Base/Internals/RecordUpdater.html +1026 -0
- data/doc/Repository/Base/Internals/SlugFinder.html +986 -0
- data/doc/_index.html +176 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +499 -0
- data/doc/file.CHANGELOG.html +240 -0
- data/doc/file.README.html +218 -0
- data/doc/file_list.html +61 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +218 -0
- data/doc/js/app.js +248 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +363 -0
- data/doc/top-level-namespace.html +110 -0
- data/lib/repository/base.rb +115 -0
- data/lib/repository/base/internals/internals.rb +6 -0
- data/lib/repository/base/internals/record_deleter.rb +46 -0
- data/lib/repository/base/internals/record_saver.rb +58 -0
- data/lib/repository/base/internals/record_updater.rb +54 -0
- data/lib/repository/base/internals/slug_finder.rb +70 -0
- data/lib/repository/base/version.rb +12 -0
- data/repository-base.gemspec +37 -0
- data/spec/repository/base_spec.rb +398 -0
- data/spec/spec_helper.rb +14 -0
- 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
|
data/spec/spec_helper.rb
ADDED
@@ -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'
|