database_validations 0.8.5 → 0.9.1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/database_validations.rb +14 -10
  3. data/lib/database_validations/lib/adapters.rb +20 -0
  4. data/lib/database_validations/{validations → lib}/adapters/base_adapter.rb +1 -17
  5. data/lib/database_validations/lib/adapters/mysql_adapter.rb +19 -0
  6. data/lib/database_validations/lib/adapters/postgresql_adapter.rb +21 -0
  7. data/lib/database_validations/lib/adapters/sqlite_adapter.rb +19 -0
  8. data/lib/database_validations/lib/attribute_validator.rb +3 -0
  9. data/lib/database_validations/lib/checkers/db_presence_validator.rb +36 -0
  10. data/lib/database_validations/lib/checkers/db_uniqueness_validator.rb +47 -0
  11. data/lib/database_validations/{validations → lib}/errors.rb +1 -12
  12. data/lib/database_validations/lib/injector.rb +13 -0
  13. data/lib/database_validations/lib/key_generator.rb +32 -0
  14. data/lib/database_validations/lib/presence_key_extractor.rb +18 -0
  15. data/lib/database_validations/lib/rescuer.rb +36 -0
  16. data/lib/database_validations/lib/storage.rb +28 -0
  17. data/lib/database_validations/lib/uniqueness_key_extractor.rb +38 -0
  18. data/lib/database_validations/lib/validations.rb +31 -0
  19. data/lib/database_validations/lib/validators/db_presence_validator.rb +63 -0
  20. data/lib/database_validations/lib/validators/db_uniqueness_validator.rb +48 -0
  21. data/lib/database_validations/rspec/uniqueness_validator_matcher.rb +19 -10
  22. data/lib/database_validations/version.rb +1 -1
  23. metadata +52 -41
  24. data/lib/database_validations/validations/adapters.rb +0 -20
  25. data/lib/database_validations/validations/adapters/mysql_adapter.rb +0 -20
  26. data/lib/database_validations/validations/adapters/postgresql_adapter.rb +0 -20
  27. data/lib/database_validations/validations/adapters/sqlite_adapter.rb +0 -22
  28. data/lib/database_validations/validations/belongs_to_handlers.rb +0 -58
  29. data/lib/database_validations/validations/belongs_to_options.rb +0 -44
  30. data/lib/database_validations/validations/belongs_to_presence_validator.rb +0 -25
  31. data/lib/database_validations/validations/helpers.rb +0 -69
  32. data/lib/database_validations/validations/options_storage.rb +0 -34
  33. data/lib/database_validations/validations/uniqueness_handlers.rb +0 -56
  34. data/lib/database_validations/validations/uniqueness_options.rb +0 -104
  35. data/lib/database_validations/validations/valid_without_database_validations.rb +0 -9
@@ -1,22 +0,0 @@
1
- module DatabaseValidations
2
- module Adapters
3
- class SqliteAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message if unless].freeze
5
- ADAPTER = :sqlite3
6
-
7
- def index_name(_error_message); end
8
-
9
- def find_foreign_key_by_column(column)
10
- foreign_keys.find { |foreign_key| foreign_key.column.to_s == column.to_s }
11
- end
12
-
13
- def unique_error_columns(error_message)
14
- error_message.scan(/#{model.table_name}\.([^,:]+)/).flatten
15
- end
16
-
17
- def foreign_key_error_column(error_message)
18
- error_message[/\("([^"]+)"\) VALUES/, 1]
19
- end
20
- end
21
- end
22
- end
@@ -1,58 +0,0 @@
1
- module DatabaseValidations
2
- module BelongsToHandlers
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- alias_method :validate, :valid?
7
- end
8
-
9
- def valid?(context = nil)
10
- output = super(context)
11
-
12
- Helpers.each_belongs_to_presence_validator(self.class) do |validator|
13
- next if validator.column_and_relation_blank_for?(self)
14
-
15
- validates_with(ActiveRecord::Validations::PresenceValidator, validator.validates_presence_options)
16
- end
17
-
18
- errors.empty? && output
19
- end
20
-
21
- def save(opts = {})
22
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
23
- rescue ActiveRecord::InvalidForeignKey => e
24
- Helpers.handle_foreign_key_error!(self, e)
25
- false
26
- end
27
-
28
- def save!(opts = {})
29
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
30
- rescue ActiveRecord::InvalidForeignKey => e
31
- Helpers.handle_foreign_key_error!(self, e)
32
- raise ActiveRecord::RecordInvalid, self
33
- end
34
-
35
- private
36
-
37
- def perform_validations(options = {})
38
- options[:validate] == false || valid_without_database_validations?(options[:context])
39
- end
40
- end
41
-
42
- module ClassMethods
43
- def db_belongs_to(name, scope = nil, **options)
44
- include(DatabaseValidations::ValidWithoutDatabaseValidations)
45
- @database_validations_opts ||= DatabaseValidations::OptionsStorage.new(self)
46
-
47
- belongs_to(name, scope, options.merge(optional: true))
48
-
49
- foreign_key = reflections[name.to_s].foreign_key
50
-
51
- validates_with DatabaseValidations::Validations::BelongsToPresenceValidator, column: foreign_key, relation: name
52
-
53
- @database_validations_opts.push_belongs_to(foreign_key, name)
54
-
55
- include(DatabaseValidations::BelongsToHandlers)
56
- end
57
- end
58
- end
@@ -1,44 +0,0 @@
1
- module DatabaseValidations
2
- class BelongsToOptions
3
- attr_reader :column, :adapter, :relation
4
-
5
- def initialize(column, relation, adapter)
6
- @column = column
7
- @relation = relation
8
- @adapter = adapter
9
-
10
- raise_if_unsupported_database!
11
- raise_if_foreign_key_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
12
- end
13
-
14
- # @return [String]
15
- def key
16
- Helpers.generate_key_for_belongs_to(column)
17
- end
18
-
19
- # @return [Boolean]
20
- def column_and_relation_blank_for?(instance)
21
- instance.public_send(column).blank? && instance.public_send(relation).blank?
22
- end
23
-
24
- def handle_foreign_key_error(instance)
25
- # Hack to not query the database because we know the result already
26
- instance.send("#{relation}=", nil)
27
- instance.errors.add(relation, :blank, message: :required)
28
- end
29
-
30
- def validates_presence_options
31
- { attributes: relation, message: :required }
32
- end
33
-
34
- private
35
-
36
- def raise_if_foreign_key_missed!
37
- raise Errors::ForeignKeyNotFound.new(column, adapter.foreign_keys) unless adapter.find_foreign_key_by_column(column)
38
- end
39
-
40
- def raise_if_unsupported_database!
41
- raise Errors::UnsupportedDatabase.new(:db_belongs_to, adapter.adapter_name) if adapter.adapter_name == :sqlite3
42
- end
43
- end
44
- end
@@ -1,25 +0,0 @@
1
- module DatabaseValidations
2
- module Validations
3
- class BelongsToPresenceValidator < ActiveModel::Validator
4
- attr_reader :attributes
5
-
6
- # This is a hack to simulate presence validator
7
- # It's used for cases when some 3rd parties are relies on the validators
8
- # For example, required option from simple_form checks the validator
9
- def self.kind
10
- :presence
11
- end
12
-
13
- def initialize(options = {})
14
- @attributes = [options[:relation]]
15
- super
16
- end
17
-
18
- def validate(record)
19
- return unless record.public_send(options[:column]).blank? && record.public_send(options[:relation]).blank?
20
-
21
- record.errors.add(options[:relation], :blank, message: :required)
22
- end
23
- end
24
- end
25
- end
@@ -1,69 +0,0 @@
1
- module DatabaseValidations
2
- module Helpers
3
- module_function
4
-
5
- def handle_unique_error!(instance, error) # rubocop:disable Metrics/AbcSize
6
- adapter = Adapters.factory(instance.class)
7
- index_key = generate_key_for_uniqueness_index(adapter.index_name(error.message))
8
- column_key = generate_key_for_uniqueness(adapter.unique_error_columns(error.message))
9
-
10
- each_options_storage(instance.class) do |storage|
11
- return storage[index_key].handle_unique_error(instance) if storage[index_key]
12
- return storage[column_key].handle_unique_error(instance) if storage[column_key]
13
- end
14
-
15
- raise error
16
- end
17
-
18
- def handle_foreign_key_error!(instance, error)
19
- adapter = Adapters.factory(instance.class)
20
- column_key = generate_key_for_belongs_to(adapter.foreign_key_error_column(error.message))
21
-
22
- each_options_storage(instance.class) do |storage|
23
- return storage[column_key].handle_foreign_key_error(instance) if storage[column_key]
24
- end
25
-
26
- raise error
27
- end
28
-
29
- def each_options_storage(klass)
30
- while klass.respond_to?(:validates_db_uniqueness_of)
31
- storage = klass.instance_variable_get(:'@database_validations_opts')
32
- yield(storage) if storage
33
- klass = klass.superclass
34
- end
35
- end
36
-
37
- def each_uniqueness_validator(klass)
38
- each_options_storage(klass) do |storage|
39
- storage.each_uniqueness_validator { |validator| yield(validator) }
40
- end
41
- end
42
-
43
- def each_belongs_to_presence_validator(klass)
44
- each_options_storage(klass) do |storage|
45
- storage.each_belongs_to_presence_validator { |validator| yield(validator) }
46
- end
47
- end
48
-
49
- def unify_columns(*columns)
50
- columns.flatten.compact.map(&:to_s).sort
51
- end
52
-
53
- def generate_key_for_uniqueness_index(index_name)
54
- generate_key(:uniqueness_index, index_name)
55
- end
56
-
57
- def generate_key_for_uniqueness(*columns)
58
- generate_key(:uniqueness, columns)
59
- end
60
-
61
- def generate_key_for_belongs_to(column)
62
- generate_key(:belongs_to, column)
63
- end
64
-
65
- def generate_key(type, *columns)
66
- [type, *unify_columns(columns)].join('__')
67
- end
68
- end
69
- end
@@ -1,34 +0,0 @@
1
- module DatabaseValidations
2
- class OptionsStorage
3
- def initialize(klass)
4
- @adapter = Adapters.factory(klass)
5
- @storage = {}
6
- end
7
-
8
- def push_uniqueness(field, options)
9
- uniqueness_options = UniquenessOptions.new(field, options, adapter)
10
- storage[uniqueness_options.key] = uniqueness_options
11
- end
12
-
13
- def push_belongs_to(field, relation)
14
- belongs_to_options = BelongsToOptions.new(field, relation, adapter)
15
- storage[belongs_to_options.key] = belongs_to_options
16
- end
17
-
18
- def [](key)
19
- storage[key]
20
- end
21
-
22
- def each_uniqueness_validator
23
- storage.values.grep(UniquenessOptions).each { |validator| yield(validator) }
24
- end
25
-
26
- def each_belongs_to_presence_validator
27
- storage.values.grep(BelongsToOptions).each { |validator| yield(validator) }
28
- end
29
-
30
- private
31
-
32
- attr_reader :storage, :adapter
33
- end
34
- end
@@ -1,56 +0,0 @@
1
- module DatabaseValidations
2
- module UniquenessHandlers
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- alias_method :validate, :valid?
7
- end
8
-
9
- def valid?(context = nil)
10
- output = super(context)
11
-
12
- Helpers.each_uniqueness_validator(self.class) do |validator|
13
- if validator.if_and_unless_pass?(self)
14
- validates_with(ActiveRecord::Validations::UniquenessValidator, validator.validates_uniqueness_options)
15
- end
16
- end
17
-
18
- errors.empty? && output
19
- end
20
-
21
- def save(options = {})
22
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
23
- rescue ActiveRecord::RecordNotUnique => e
24
- Helpers.handle_unique_error!(self, e)
25
- false
26
- end
27
-
28
- def save!(options = {})
29
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
30
- rescue ActiveRecord::RecordNotUnique => e
31
- Helpers.handle_unique_error!(self, e)
32
- raise ActiveRecord::RecordInvalid, self
33
- end
34
-
35
- private
36
-
37
- def perform_validations(options = {})
38
- options[:validate] == false || valid_without_database_validations?(options[:context])
39
- end
40
- end
41
-
42
- module ClassMethods
43
- def validates_db_uniqueness_of(*attributes)
44
- include(DatabaseValidations::ValidWithoutDatabaseValidations)
45
- @database_validations_opts ||= DatabaseValidations::OptionsStorage.new(self)
46
-
47
- options = attributes.extract_options!
48
-
49
- attributes.each do |attribute|
50
- @database_validations_opts.push_uniqueness(attribute, options.merge(attributes: attribute))
51
- end
52
-
53
- include(DatabaseValidations::UniquenessHandlers)
54
- end
55
- end
56
- end
@@ -1,104 +0,0 @@
1
- module DatabaseValidations
2
- class UniquenessOptions
3
- CUSTOM_OPTIONS = %i[where index_name].freeze
4
- DEFAULT_OPTIONS = { allow_nil: true, case_sensitive: true, allow_blank: false }.freeze
5
-
6
- attr_reader :field
7
-
8
- def initialize(field, options, adapter)
9
- @field = field
10
- @options = options
11
- @adapter = adapter
12
-
13
- raise_if_unsupported_options!
14
- raise_if_index_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
15
- end
16
-
17
- def handle_unique_error(instance)
18
- error_options = options.except(:case_sensitive, :scope, :conditions, :attributes, *CUSTOM_OPTIONS)
19
- error_options[:value] = instance.public_send(options[:attributes])
20
-
21
- instance.errors.add(options[:attributes], :taken, error_options)
22
- end
23
-
24
- # @return [Hash<Symbol, Object>]
25
- def validates_uniqueness_options
26
- where_clause_str = where_clause
27
-
28
- DEFAULT_OPTIONS
29
- .merge(options)
30
- .except(*CUSTOM_OPTIONS)
31
- .tap { |opts| opts[:conditions] = -> { where(where_clause_str) } if where_clause }
32
- end
33
-
34
- # @return [Boolean]
35
- def if_and_unless_pass?(instance)
36
- (options[:if].nil? || condition_passes?(options[:if], instance)) &&
37
- (options[:unless].nil? || !condition_passes?(options[:unless], instance))
38
- end
39
-
40
- # @return [String]
41
- def key
42
- @key ||= index_name ? Helpers.generate_key_for_uniqueness_index(index_name) : Helpers.generate_key_for_uniqueness(columns)
43
- end
44
-
45
- # @return [Array<String>]
46
- def columns
47
- @columns ||= Helpers.unify_columns(field, scope)
48
- end
49
-
50
- # @return [String|nil]
51
- def where_clause
52
- @where_clause ||= options[:where]
53
- end
54
-
55
- # @return [String|nil]
56
- def message
57
- @message ||= options[:message]
58
- end
59
-
60
- # @return [Array<String|Symbol>]
61
- def scope
62
- @scope ||= Array.wrap(options[:scope])
63
- end
64
-
65
- # @return [String|Symbol|nil]
66
- def index_name
67
- @index_name ||= options[:index_name]
68
- end
69
-
70
- # @return [Boolean|nil]
71
- def case_sensitive
72
- @case_sensitive ||= options[:case_sensitive]
73
- end
74
-
75
- private
76
-
77
- attr_reader :adapter, :options
78
-
79
- def condition_passes?(condition, instance)
80
- if condition.is_a?(Symbol)
81
- instance.__send__(condition)
82
- elsif condition.is_a?(Proc) && condition.arity.zero?
83
- instance.instance_exec(&condition)
84
- else
85
- instance.instance_eval(&condition)
86
- end
87
- end
88
-
89
- def raise_if_unsupported_options!
90
- options.except(:attributes).each_key do |option|
91
- unless adapter.support_option?(option)
92
- raise Errors::OptionIsNotSupported.new(option, adapter.adapter_name, adapter.supported_options)
93
- end
94
- end
95
- end
96
-
97
- def raise_if_index_missed! # rubocop:disable Metrics/AbcSize
98
- unless (index_name && adapter.find_index_by_name(index_name.to_s)) ||
99
- (!index_name && adapter.find_index(columns, where_clause))
100
- raise Errors::IndexNotFound.new(columns, where_clause, index_name, adapter.indexes, adapter.table_name)
101
- end
102
- end
103
- end
104
- end
@@ -1,9 +0,0 @@
1
- module DatabaseValidations
2
- module ValidWithoutDatabaseValidations
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- alias_method :valid_without_database_validations?, :valid?
7
- end
8
- end
9
- end