Skip to content

Commit a1837fa

Browse files
authored
support passing validation context on save (#173)
1 parent 2910101 commit a1837fa

File tree

4 files changed

+128
-17
lines changed

4 files changed

+128
-17
lines changed

.github/workflows/ruby.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
steps:
2727
- uses: actions/checkout@v4
2828
- name: Set up CouchDB
29-
uses: cobot/couchdb-action@v5
29+
uses: cobot/couchdb-action@v5.0.1
3030
with:
3131
couchdb-version: "2.3.1"
3232
- name: Set up Ruby

lib/couch_potato/database.rb

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,20 @@ def first!(spec)
105105
end
106106

107107
# saves a document. returns true on success, false on failure.
108+
# By default validations are run before saving. You can disable
109+
# validations by passing validate: false as an option.
110+
# You can also pass a custom validation context by passing context: :custom_context
108111
# if passed a block will:
109112
# * yield the object to be saved to the block and run if once before saving
110113
# * on conflict: reload the document, run the block again and retry saving
111-
def save_document(document, validate = true, retries = 0, &block)
114+
def save_document(document, options = {}, retries = 0, &block)
112115
cache&.clear
113116
begin
114117
block&.call document
115-
save_document_without_conflict_handling(document, validate)
118+
save_document_without_conflict_handling(document, options)
116119
rescue CouchRest::Conflict
117120
if block
118-
handle_write_conflict document, validate, retries, &block
121+
handle_write_conflict document, options, retries, &block
119122
else
120123
raise CouchPotato::Conflict
121124
end
@@ -124,8 +127,8 @@ def save_document(document, validate = true, retries = 0, &block)
124127
alias save save_document
125128

126129
# saves a document, raises a CouchPotato::Database::ValidationsFailedError on failure
127-
def save_document!(document)
128-
save_document(document) || raise(ValidationsFailedError, document.errors.full_messages)
130+
def save_document!(document, options = {})
131+
save_document(document, options) || raise(ValidationsFailedError, document.errors.full_messages)
129132
end
130133
alias save! save_document!
131134

@@ -293,15 +296,15 @@ def view_cache_id(spec)
293296
spec.send(:klass).to_s + spec.view_name.to_s + spec.view_parameters.to_s
294297
end
295298

296-
def handle_write_conflict(document, validate, retries, &block)
299+
def handle_write_conflict(document, options, retries, &block)
297300
cache&.clear
298301
if retries == 5
299302
raise CouchPotato::Conflict
300303
else
301304
reloaded = document.reload
302305
document.attributes = reloaded.attributes
303306
document._rev = reloaded._rev
304-
save_document document, validate, retries + 1, &block
307+
save_document document, options, retries + 1, &block
305308
end
306309
end
307310

@@ -314,11 +317,11 @@ def destroy_document_without_conflict_handling(document)
314317
document._rev = nil
315318
end
316319

317-
def save_document_without_conflict_handling(document, validate = true)
320+
def save_document_without_conflict_handling(document, options = {})
318321
if document.new?
319-
create_document(document, validate)
322+
create_document(document, options)
320323
else
321-
update_document(document, validate)
324+
update_document(document, options)
322325
end
323326
end
324327

@@ -337,14 +340,15 @@ def bulk_load(ids)
337340
end
338341
end
339342

340-
def create_document(document, validate)
343+
def create_document(document, options)
341344
document.database = self
345+
validate, validation_context = parse_save_options(options)
342346

343347
if validate
344348
document.errors.clear
345349
return false if document.run_callbacks(:validation_on_save) do
346350
return false if document.run_callbacks(:validation_on_create) do
347-
return false unless valid_document?(document)
351+
return false unless valid_document?(document, validation_context)
348352
end == false
349353
end == false
350354
end
@@ -360,12 +364,25 @@ def create_document(document, validate)
360364
true
361365
end
362366

363-
def update_document(document, validate)
367+
def parse_save_options(options)
368+
if options.is_a?(Hash)
369+
validate = options.fetch(:validate, true)
370+
validation_context = options[:context]
371+
else
372+
validate = !!options
373+
validation_context = nil
374+
end
375+
[validate, validation_context]
376+
end
377+
378+
def update_document(document, options)
379+
validate, validation_context = parse_save_options(options)
380+
364381
if validate
365382
document.errors.clear
366383
return false if document.run_callbacks(:validation_on_save) do
367384
return false if document.run_callbacks(:validation_on_update) do
368-
return false unless valid_document?(document)
385+
return false unless valid_document?(document, validation_context)
369386
end == false
370387
end == false
371388
end
@@ -380,9 +397,9 @@ def update_document(document, validate)
380397
true
381398
end
382399

383-
def valid_document?(document)
400+
def valid_document?(document, validation_context = nil)
384401
original_errors_hash = document.errors.to_hash
385-
document.valid?
402+
document.valid?(validation_context)
386403
original_errors_hash.each do |k, v|
387404
if v.respond_to?(:each)
388405
v.each { |message| document.errors.add(k, message) }

spec/spec_helper.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ class BigDecimalContainer
3939
property :number, type: BigDecimal
4040
end
4141

42+
class WithValidationContext
43+
include CouchPotato::Persistence
44+
45+
property :name
46+
47+
validates_presence_of :name, on: :create
48+
validates_length_of :name, minimum: 5, on: :update
49+
validates_length_of :name, minimum: 10, on: :custom
50+
end
51+
4252
def recreate_db
4353
CouchPotato.couchrest_database.recreate!
4454
end

spec/validation_context_spec.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require 'spec_helper'
2+
3+
describe "validation context" do
4+
let(:db) { CouchPotato.database }
5+
6+
context 'when calling save' do
7+
8+
it 'uses the :create context on creation' do
9+
model = WithValidationContext.new
10+
11+
db.save(model)
12+
13+
expect(model.errors[:name]).to eq(["can't be blank"])
14+
end
15+
16+
it 'uses the :update context on update' do
17+
model = WithValidationContext.new(name: 'initial name')
18+
db.save!(model)
19+
20+
model.name = 'new'
21+
db.save(model)
22+
23+
expect(model.errors[:name]).to eq(["is too short (minimum is 5 characters)"])
24+
end
25+
26+
it 'uses a custom context on create when specified' do
27+
model = WithValidationContext.new(name: 'short')
28+
29+
db.save(model, context: :custom)
30+
31+
expect(model.errors[:name]).to eq(["is too short (minimum is 10 characters)"])
32+
end
33+
34+
it 'uses a custom context on update when specified' do
35+
model = WithValidationContext.new(name: 'initial name')
36+
db.save!(model)
37+
38+
model.name = 'new'
39+
db.save(model, context: :custom)
40+
41+
expect(model.errors[:name]).to eq(["is too short (minimum is 10 characters)"])
42+
end
43+
44+
end
45+
46+
context 'when calling save!' do
47+
48+
it 'uses the :create context on creation' do
49+
model = WithValidationContext.new
50+
51+
expect do
52+
db.save!(model)
53+
end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name can't be blank/)
54+
end
55+
56+
it 'uses the :update context on update' do
57+
model = WithValidationContext.new(name: 'initial name')
58+
db.save!(model)
59+
60+
model.name = 'new'
61+
expect do
62+
db.save!(model)
63+
end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 5 characters\)/)
64+
end
65+
66+
it 'uses a custom context on create when specified' do
67+
model = WithValidationContext.new(name: 'short')
68+
69+
expect do
70+
db.save!(model, context: :custom)
71+
end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 10 characters\)/)
72+
end
73+
74+
it 'uses a custom context on update when specified' do
75+
model = WithValidationContext.new(name: 'initial name')
76+
db.save!(model)
77+
78+
model.name = 'new'
79+
expect do
80+
db.save!(model, context: :custom)
81+
end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 10 characters\)/)
82+
end
83+
end
84+
end

0 commit comments

Comments
 (0)