iknow_view_models 3.6.5 → 3.6.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea15135f8f921a083f88184fb1602c2cb1ab1654845bf7d29b5d6a35ce4a1e93
4
- data.tar.gz: cbb335d8125222a4e5efdb6fc5ae6317a6c1e74d86270885b68d2e482742f392
3
+ metadata.gz: eb322edf3edec85adb744dba24cab64e15914716f16cfa5ec6bb75de5fcb8e33
4
+ data.tar.gz: 4b41f0f4ba1fa9a3d7568ee8b0005c4139e8186c91244c7aa04ff8b3bae19fce
5
5
  SHA512:
6
- metadata.gz: cde2777790785631ba0407ddd576931b05257bb1f51544927c4f9750ec0472cd45f87a9cfc3536271ead56f7b7decbc548f3f401ca23ae48a28bb11c2a6b67dd
7
- data.tar.gz: 0bc8000d9cdf342248e2c9cd24e40f8fd4386f34b01891b022785747f9eda8474a19201cc1c72af8d526632ca9493d97eef00e59da3c683ba7bd8d1083790cc2
6
+ metadata.gz: a51e6a510110c90e3b4a77d0f3da2df2191e68d12137cd9ab040a97125f799e8292ae86c9b53f39b85ebb589d785fa791e8bf56883b122d6b08bfc15b2a0f702
7
+ data.tar.gz: 03c790566b0304ab4333e632fd81ba8886425ee8a45f1edb2fc5c777af7e2bcbfccecaa947c8a3001d47711ed8dee4132dcb4b52284cd799b915e147de9daf35
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.6.5'
4
+ VERSION = '3.6.6'
5
5
  end
@@ -174,6 +174,12 @@ class ViewModel::ActiveRecord < ViewModel::Record
174
174
  .run!(deserialize_context: deserialize_context)
175
175
  end
176
176
  end
177
+ rescue ViewModel::DeserializationError => e
178
+ if (new_error = customize_deserialization_error(e))
179
+ raise new_error
180
+ else
181
+ raise
182
+ end
177
183
  end
178
184
 
179
185
  # Constructs a preload specification of the required models for
@@ -347,7 +347,7 @@ class ViewModel
347
347
  # back when we can't.
348
348
  def self.from_exception(exception, nodes = [])
349
349
  case exception.cause
350
- when PG::UniqueViolation
350
+ when PG::UniqueViolation, PG::ExclusionViolation
351
351
  UniqueViolation.from_postgres_error(exception.cause, nodes)
352
352
  else
353
353
  self.new(exception.message, nodes)
@@ -357,36 +357,60 @@ class ViewModel
357
357
 
358
358
  class UniqueViolation < DeserializationError
359
359
  status 400
360
- attr_reader :detail, :constraint, :columns, :values
360
+ attr_reader :detail, :constraint, :columns, :values, :conflicts
361
361
 
362
- PG_ERROR_FIELD_CONSTRAINT_NAME = 'n'.ord # Not exposed in pg gem
363
362
  def self.from_postgres_error(err, nodes)
364
363
  result = err.result
365
- constraint = result.error_field(PG_ERROR_FIELD_CONSTRAINT_NAME)
366
- message_detail = result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
364
+ constraint = result.error_field(PG::PG_DIAG_CONSTRAINT_NAME)
365
+ message_detail = result.error_field(PG::PG_DIAG_MESSAGE_DETAIL)
367
366
 
368
- columns, values = parse_message_detail(message_detail)
367
+ columns, values, conflicts = parse_message_detail(message_detail)
369
368
 
370
369
  unless columns
371
370
  # Couldn't parse the detail message, fall back on an unparsed error
372
371
  return DatabaseConstraint.new(err.message, nodes)
373
372
  end
374
373
 
375
- self.new(err.message, constraint, columns, values, nodes)
374
+ self.new(err.message, constraint, columns, values, conflicts, nodes)
376
375
  end
377
376
 
378
377
  class << self
379
378
  DETAIL_PREFIX = 'Key ('
380
- DETAIL_SUFFIX = ') already exists.'
381
- DETAIL_INFIX = ')=('
379
+ UNIQUE_SUFFIX_TEMPLATE = /\A\)=\((?<values>.*)\) already exists\.\z/
380
+ EXCLUSION_SUFFIX_TEMPLATE = /\A\)=\((?<values>.*)\) conflicts with existing key \(.*\)=\((?<conflicts>.*)\)\.\z/
381
+
382
382
  def parse_message_detail(detail)
383
383
  stream = detail.dup
384
-
385
384
  return nil unless stream.delete_prefix!(DETAIL_PREFIX)
386
- return nil unless stream.delete_suffix!(DETAIL_SUFFIX)
387
385
 
388
386
  # The message should start with an identifier list: pop off identifier
389
387
  # tokens while we can.
388
+ identifiers = parse_identifiers(stream)
389
+ return nil if identifiers.nil?
390
+
391
+ # The message should now contain ")=(" followed by the value list and
392
+ # the suffix, potentially including a conflict list. We consider the
393
+ # value and conflict lists to be essentially unparseable because they
394
+ # are free to contain commas and no escaping is used. We make a best
395
+ # effort to extract them anyway.
396
+ values, conflicts =
397
+ if (m = UNIQUE_SUFFIX_TEMPLATE.match(stream))
398
+ m.values_at(:values)
399
+ elsif (m = EXCLUSION_SUFFIX_TEMPLATE.match(stream))
400
+ m.values_at(:values, :conflicts)
401
+ else
402
+ return nil
403
+ end
404
+
405
+ return identifiers, values, conflicts
406
+ end
407
+
408
+ private
409
+
410
+ QUOTED_IDENTIFIER = /\A"(?:[^"]|"")+"/.freeze
411
+ UNQUOTED_IDENTIFIER = /\A(?:\p{Alpha}|_)(?:\p{Alnum}|_)*/.freeze
412
+
413
+ def parse_identifiers(stream)
390
414
  identifiers = []
391
415
 
392
416
  identifier = parse_identifier(stream)
@@ -401,18 +425,9 @@ class ViewModel
401
425
  identifiers << identifier
402
426
  end
403
427
 
404
- # The message should now contain ")=(" followed by the (unparseable)
405
- # value list.
406
- return nil unless stream.delete_prefix!(DETAIL_INFIX)
407
-
408
- [identifiers, stream]
428
+ identifiers
409
429
  end
410
430
 
411
- private
412
-
413
- QUOTED_IDENTIFIER = /\A"(?:[^"]|"")+"/.freeze
414
- UNQUOTED_IDENTIFIER = /\A(?:\p{Alpha}|_)(?:\p{Alnum}|_)*/.freeze
415
-
416
431
  def parse_identifier(stream)
417
432
  if (identifier = stream.slice!(UNQUOTED_IDENTIFIER))
418
433
  identifier
@@ -424,16 +439,17 @@ class ViewModel
424
439
  end
425
440
  end
426
441
 
427
- def initialize(detail, constraint, columns, values, nodes = [])
442
+ def initialize(detail, constraint, columns, values, conflicts, nodes = [])
428
443
  @detail = detail
429
444
  @constraint = constraint
430
445
  @columns = columns
431
446
  @values = values
447
+ @conflicts = conflicts
432
448
  super(nodes)
433
449
  end
434
450
 
435
451
  def meta
436
- super.merge(constraint: @constraint, columns: @columns, values: @values)
452
+ super.merge(constraint: @constraint, columns: @columns, values: @values, conflicts: @conflicts)
437
453
  end
438
454
  end
439
455
 
@@ -102,6 +102,12 @@ class ViewModel::Record < ViewModel
102
102
 
103
103
  viewmodel
104
104
  end
105
+ rescue ViewModel::DeserializationError => e
106
+ if (new_error = customize_deserialization_error(e))
107
+ raise new_error
108
+ else
109
+ raise
110
+ end
105
111
  end
106
112
 
107
113
  def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)
@@ -164,6 +170,14 @@ class ViewModel::Record < ViewModel
164
170
 
165
171
  @model_class = type
166
172
  end
173
+
174
+ # Overriding this method allows matching and customization of deserialization
175
+ # errors in individual viewmodel types. This method is called on error from
176
+ # #deserialize_from_view. If a value is returned, that exception is raised
177
+ # instead.
178
+ def customize_deserialization_error(e)
179
+ nil
180
+ end
167
181
  end
168
182
 
169
183
  delegate :model_class, to: 'self.class'
@@ -445,6 +445,44 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
445
445
  end
446
446
  end
447
447
 
448
+ class CustomizedErrorTest < ActiveSupport::TestCase
449
+ include ARVMTestUtilities
450
+ class TestError < ViewModel::Error; end
451
+
452
+ def before_all
453
+ super
454
+
455
+ build_viewmodel(:Model) do
456
+ define_schema do |t|
457
+ t.integer :value, null: false
458
+ end
459
+
460
+ define_model do
461
+ end
462
+
463
+ define_viewmodel do
464
+ root!
465
+
466
+ attribute :value
467
+ end
468
+ end
469
+
470
+ ModelView.define_singleton_method(:customize_deserialization_error) do |e|
471
+ TestError.new if e.is_a?(ViewModel::DeserializationError::DatabaseConstraint)
472
+ end
473
+ end
474
+
475
+ def test_customized_error
476
+ m = Model.create!(value: 1)
477
+
478
+ assert_raises(TestError) do
479
+ alter_by_view!(ModelView, m) do |view, refs|
480
+ view['value'] = nil
481
+ end
482
+ end
483
+ end
484
+ end
485
+
448
486
  # Parent view should be correctly passed down the tree when deserializing
449
487
  class DeferredConstraintTest < ActiveSupport::TestCase
450
488
  include ARVMTestUtilities
@@ -455,6 +493,7 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
455
493
  build_viewmodel(:List) do
456
494
  define_schema do |t|
457
495
  t.integer :child_id
496
+ t.integer :value
458
497
  end
459
498
 
460
499
  define_model do
@@ -464,9 +503,11 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
464
503
  define_viewmodel do
465
504
  root!
466
505
  association :child
506
+ attribute :value
467
507
  end
468
508
  end
469
509
  List.connection.execute('ALTER TABLE lists ADD CONSTRAINT unique_child UNIQUE (child_id) DEFERRABLE INITIALLY DEFERRED')
510
+ List.connection.execute('ALTER TABLE lists ADD CONSTRAINT excluded_value EXCLUDE USING btree (value WITH =) DEFERRABLE INITIALLY DEFERRED')
470
511
  end
471
512
 
472
513
  def test_deferred_constraint_violation
@@ -483,13 +524,37 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
483
524
  constraint = 'unique_child'
484
525
  columns = ['child_id']
485
526
  values = l1.child.id.to_s
527
+ conflicts = nil
528
+
529
+ assert_match(/#{constraint}/, ex.message)
530
+ assert_equal(constraint, ex.constraint)
531
+ assert_equal(columns, ex.columns)
532
+ assert_equal(values, ex.values)
533
+
534
+ assert_equal({ constraint: constraint, columns: columns, values: values, conflicts: conflicts, nodes: [] }, ex.meta)
535
+ end
536
+
537
+ def test_deferred_exclusion_violation
538
+ l1 = List.create!(value: 1)
539
+ l2 = List.create!
540
+
541
+ ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
542
+ alter_by_view!(ListView, l2) do |view, refs|
543
+ view['value'] = 1
544
+ end
545
+ end
546
+
547
+ constraint = 'excluded_value'
548
+ columns = ['value']
549
+ values = '1'
550
+ conflicts = '1'
486
551
 
487
552
  assert_match(/#{constraint}/, ex.message)
488
553
  assert_equal(constraint, ex.constraint)
489
554
  assert_equal(columns, ex.columns)
490
555
  assert_equal(values, ex.values)
491
556
 
492
- assert_equal({ constraint: constraint, columns: columns, values: values, nodes: [] }, ex.meta)
557
+ assert_equal({ constraint: constraint, columns: columns, values: values, conflicts: conflicts, nodes: [] }, ex.meta)
493
558
  end
494
559
  end
495
560
  end
@@ -34,7 +34,15 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
34
34
  let(:detail_message) { 'Key (x)=(a) already exists.' }
35
35
 
36
36
  it 'parses the key and value' do
37
- expect(parse).to eq([['x'], 'a'])
37
+ expect(parse).to eq([['x'], 'a', nil])
38
+ end
39
+ end
40
+
41
+ describe 'with a exclusion conflict' do
42
+ let(:detail_message) { 'Key (x)=(a) conflicts with existing key (x)=(z).' }
43
+
44
+ it 'parses the key and value' do
45
+ expect(parse).to eq([['x'], 'a', 'z'])
38
46
  end
39
47
  end
40
48
 
@@ -42,7 +50,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
42
50
  let(:detail_message) { 'Key (x, y)=(a, b) already exists.' }
43
51
 
44
52
  it 'parses the keys and value' do
45
- expect(parse).to eq([['x', 'y'], 'a, b'])
53
+ expect(parse).to eq([['x', 'y'], 'a, b', nil])
46
54
  end
47
55
  end
48
56
 
@@ -50,7 +58,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
50
58
  let(:detail_message) { 'Key ("x, y", z)=(a, b, c) already exists.' }
51
59
 
52
60
  it 'parses the keys and value' do
53
- expect(parse).to eq([['x, y', 'z'], 'a, b, c'])
61
+ expect(parse).to eq([['x, y', 'z'], 'a, b, c', nil])
54
62
  end
55
63
  end
56
64
 
@@ -58,7 +66,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
58
66
  let(:detail_message) { 'Key ("""x"", ""y""", z)=(a, b, c) already exists.' }
59
67
 
60
68
  it 'parses the keys and value' do
61
- expect(parse).to eq([['"x", "y"', 'z'], 'a, b, c'])
69
+ expect(parse).to eq([['"x", "y"', 'z'], 'a, b, c', nil])
62
70
  end
63
71
  end
64
72
 
@@ -66,7 +74,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
66
74
  let(:detail_message) { 'Key (a, b)=(a, b)=(c, d) already exists.' }
67
75
 
68
76
  it 'parses the keys and value' do
69
- expect(parse).to eq([['a', 'b'], 'a, b)=(c, d'])
77
+ expect(parse).to eq([['a', 'b'], 'a, b)=(c, d', nil])
70
78
  end
71
79
  end
72
80
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.5
4
+ version: 3.6.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-13 00:00:00.000000000 Z
11
+ date: 2022-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack