database_validations 0.8.9 → 0.9.3

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/database_validations.rb +12 -9
  3. data/lib/database_validations/lib/adapters/base_adapter.rb +0 -16
  4. data/lib/database_validations/lib/adapters/mysql_adapter.rb +1 -2
  5. data/lib/database_validations/lib/adapters/postgresql_adapter.rb +0 -1
  6. data/lib/database_validations/lib/adapters/sqlite_adapter.rb +0 -1
  7. data/lib/database_validations/lib/attribute_validator.rb +3 -0
  8. data/lib/database_validations/lib/checkers/db_presence_validator.rb +36 -0
  9. data/lib/database_validations/lib/checkers/db_uniqueness_validator.rb +47 -0
  10. data/lib/database_validations/lib/errors.rb +0 -11
  11. data/lib/database_validations/lib/injector.rb +13 -0
  12. data/lib/database_validations/lib/key_generator.rb +32 -0
  13. data/lib/database_validations/lib/presence_key_extractor.rb +18 -0
  14. data/lib/database_validations/lib/rescuer.rb +28 -17
  15. data/lib/database_validations/lib/storage.rb +28 -0
  16. data/lib/database_validations/lib/uniqueness_key_extractor.rb +38 -0
  17. data/lib/database_validations/lib/validations.rb +31 -0
  18. data/lib/database_validations/lib/validators/db_presence_validator.rb +67 -0
  19. data/lib/database_validations/lib/validators/db_uniqueness_validator.rb +65 -0
  20. data/lib/database_validations/rspec/uniqueness_validator_matcher.rb +15 -8
  21. data/lib/database_validations/version.rb +1 -1
  22. metadata +45 -35
  23. data/lib/database_validations/lib/db_belongs_to/belongs_to_handlers.rb +0 -20
  24. data/lib/database_validations/lib/db_belongs_to/belongs_to_options.rb +0 -39
  25. data/lib/database_validations/lib/db_belongs_to/db_presence_validator.rb +0 -30
  26. data/lib/database_validations/lib/helpers.rb +0 -79
  27. data/lib/database_validations/lib/options_storage.rb +0 -31
  28. data/lib/database_validations/lib/validates_db_uniqueness_of/db_uniqueness_validator.rb +0 -14
  29. data/lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers.rb +0 -20
  30. data/lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_options.rb +0 -98
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c5d3a56f3d69b34fdc1da4e3b74b9ff3797d0ca93772df93746e709f9b9fd64
4
- data.tar.gz: db772a3a726ccce4ce21bfceb0ddac72da07d9da3650f89d1845e92e580519b2
3
+ metadata.gz: 37040902b8f9dd94cbfcc1785129eff2ac2f75eb085f567299852a03f4a83cfa
4
+ data.tar.gz: c6a2a3331225b8344c23244692296a982ed25405be15650c32bc0815e227c20a
5
5
  SHA512:
6
- metadata.gz: da0b57c3e7203be84466c31f72a517852d1b4484a306607bf48e6d6f6d683e43dc1ebdcd678bc258075c5ce1f9c52af756805b749ab5562cc84ac83a6ac48ee4
7
- data.tar.gz: 1580a4718c53aa42267a2d7efd04148df0de38ddba3ce7c4c5f6c4c226736e4f568919cbe600b79d269566f081c7c9537b09fca0f5b0d65cdd73378e81b9ae71
6
+ metadata.gz: 27f9f74a6f081f343bbe94da77956cd0c06419dcf321d2576a6238f0b2bddb5dd967861deaffe88ab47929e4b3837825fc9c17bf95b4fba83f5fbfd4b07d04e0
7
+ data.tar.gz: 86f379332348da451c1f2d02e3e4096e528f9e00c25f10fdb8881512d97c10ffdf9ab5cffdc3f4e9ce6ea475e0d3d0a0d75b728fed700f7606ccf07cacfe9bdf
@@ -4,18 +4,21 @@ require 'database_validations/version'
4
4
 
5
5
  require 'database_validations/rails/railtie' if defined?(Rails)
6
6
 
7
- require 'database_validations/lib/validates_db_uniqueness_of/db_uniqueness_validator'
8
- require 'database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers'
9
- require 'database_validations/lib/validates_db_uniqueness_of/uniqueness_options'
7
+ require 'database_validations/lib/checkers/db_uniqueness_validator'
8
+ require 'database_validations/lib/checkers/db_presence_validator'
10
9
 
11
- require 'database_validations/lib/db_belongs_to/db_presence_validator'
12
- require 'database_validations/lib/db_belongs_to/belongs_to_handlers'
13
- require 'database_validations/lib/db_belongs_to/belongs_to_options'
10
+ require 'database_validations/lib/validators/db_uniqueness_validator'
11
+ require 'database_validations/lib/validators/db_presence_validator'
14
12
 
15
- require 'database_validations/lib/rescuer'
16
- require 'database_validations/lib/options_storage'
13
+ require 'database_validations/lib/storage'
14
+ require 'database_validations/lib/attribute_validator'
15
+ require 'database_validations/lib/key_generator'
16
+ require 'database_validations/lib/uniqueness_key_extractor'
17
+ require 'database_validations/lib/presence_key_extractor'
18
+ require 'database_validations/lib/validations'
17
19
  require 'database_validations/lib/errors'
18
- require 'database_validations/lib/helpers'
20
+ require 'database_validations/lib/rescuer'
21
+ require 'database_validations/lib/injector'
19
22
  require 'database_validations/lib/adapters'
20
23
 
21
24
  module DatabaseValidations
@@ -31,22 +31,6 @@ module DatabaseValidations
31
31
  foreign_keys.find { |foreign_key| foreign_key.column.to_s == column.to_s }
32
32
  end
33
33
 
34
- # @return [Symbol]
35
- def adapter_name
36
- self.class::ADAPTER
37
- end
38
-
39
- # @param [Symbol] option_name
40
- # @return [Boolean]
41
- def support_option?(option_name)
42
- supported_options.include?(option_name.to_sym)
43
- end
44
-
45
- # @return [Array<Symbol>]
46
- def supported_options
47
- self.class::SUPPORTED_OPTIONS
48
- end
49
-
50
34
  # @return [String]
51
35
  def table_name
52
36
  model.table_name
@@ -1,12 +1,11 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class MysqlAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message if unless index_name].freeze
5
4
  ADAPTER = :mysql2
6
5
 
7
6
  class << self
8
7
  def unique_index_name(error_message)
9
- error_message[/key '([^']+)'/, 1]
8
+ error_message[/key '([^']+)'/, 1]&.split('.')&.last
10
9
  end
11
10
 
12
11
  def unique_error_columns(_error_message); end
@@ -1,7 +1,6 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class PostgresqlAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message where if unless index_name case_sensitive].freeze
5
4
  ADAPTER = :postgresql
6
5
 
7
6
  class << self
@@ -1,7 +1,6 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class SqliteAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message if unless].freeze
5
4
  ADAPTER = :sqlite3
6
5
 
7
6
  class << self
@@ -0,0 +1,3 @@
1
+ module DatabaseValidations
2
+ AttributeValidator = Struct.new(:attribute, :validator)
3
+ end
@@ -0,0 +1,36 @@
1
+ module DatabaseValidations
2
+ module Checkers
3
+ class DbPresenceValidator
4
+ attr_reader :validator
5
+
6
+ # @param [DatabaseValidations::DbPresenceValidator]
7
+ def self.validate!(validator)
8
+ new(validator).validate!
9
+ end
10
+
11
+ # @param [DatabaseValidations::DbPresenceValidator]
12
+ def initialize(validator)
13
+ @validator = validator
14
+ end
15
+
16
+ def validate!
17
+ validate_foreign_keys! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
18
+ end
19
+
20
+ private
21
+
22
+ def validate_foreign_keys! # rubocop:disable Metrics/AbcSize
23
+ adapter = Adapters::BaseAdapter.new(validator.klass)
24
+
25
+ validator.attributes.each do |attribute|
26
+ reflection = validator.klass._reflect_on_association(attribute)
27
+
28
+ next unless reflection
29
+ next if adapter.find_foreign_key_by_column(reflection.foreign_key)
30
+
31
+ raise Errors::ForeignKeyNotFound.new(reflection.foreign_key, adapter.foreign_keys)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ module DatabaseValidations
2
+ module Checkers
3
+ class DbUniquenessValidator
4
+ attr_reader :validator
5
+
6
+ # @param [DatabaseValidations::DbUniquenessValidator]
7
+ def self.validate!(validator)
8
+ new(validator).validate!
9
+ end
10
+
11
+ # @param [DatabaseValidations::DbUniquenessValidator]
12
+ def initialize(validator)
13
+ @validator = validator
14
+ end
15
+
16
+ def validate!
17
+ validate_index_usage!
18
+
19
+ validate_indexes! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
20
+ end
21
+
22
+ private
23
+
24
+ def valid_index?(columns, index)
25
+ index_columns_size = index.columns.is_a?(Array) ? index.columns.size : (index.columns.count(',') + 1)
26
+
27
+ (columns.size == index_columns_size) && (validator.where.nil? == index.where.nil?)
28
+ end
29
+
30
+ def validate_index_usage!
31
+ return unless validator.index_name.present? && validator.attributes.size > 1
32
+
33
+ raise ArgumentError, "When index_name is provided validator can have only one attribute. See #{validator.inspect}"
34
+ end
35
+
36
+ def validate_indexes! # rubocop:disable Metrics/AbcSize
37
+ adapter = Adapters::BaseAdapter.new(validator.klass)
38
+
39
+ validator.attributes.map do |attribute|
40
+ columns = KeyGenerator.unify_columns(attribute, validator.options[:scope])
41
+ index = validator.index_name ? adapter.find_index_by_name(validator.index_name.to_s) : adapter.find_index(columns, validator.where) # rubocop:disable Metrics/LineLength
42
+ raise Errors::IndexNotFound.new(columns, validator.where, validator.index_name, adapter.indexes, adapter.table_name) unless index && valid_index?(columns, index) # rubocop:disable Metrics/LineLength
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -41,17 +41,6 @@ module DatabaseValidations
41
41
  end
42
42
  end
43
43
 
44
- class OptionIsNotSupported < Base
45
- attr_reader :option, :database, :supported_options
46
-
47
- def initialize(option, database, supported_options)
48
- @option = option
49
- @database = database
50
- @supported_options = supported_options
51
- super "Option #{self.option} is not supported for #{self.database}. Supported options are: #{self.supported_options}"
52
- end
53
- end
54
-
55
44
  class ForeignKeyNotFound < Base
56
45
  attr_reader :column, :foreign_keys
57
46
 
@@ -0,0 +1,13 @@
1
+ module DatabaseValidations
2
+ module Injector
3
+ module_function
4
+
5
+ # @param [ActiveRecord::Base] model
6
+ def inject(model)
7
+ return if model.method_defined?(:valid_without_database_validations?)
8
+
9
+ model.__send__(:alias_method, :valid_without_database_validations?, :valid?)
10
+ model.include(DatabaseValidations::Validations)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module DatabaseValidations
2
+ module KeyGenerator
3
+ module_function
4
+
5
+ # @param [String] index_name
6
+ #
7
+ # @return [String]
8
+ def for_unique_index(index_name)
9
+ generate_key(:unique_index, index_name)
10
+ end
11
+
12
+ # @return [String]
13
+ def for_db_uniqueness(*columns)
14
+ generate_key(:db_uniqueness, columns)
15
+ end
16
+
17
+ # @return [String]
18
+ def for_db_presence(column)
19
+ generate_key(:db_presence, column)
20
+ end
21
+
22
+ # @return [String]
23
+ def generate_key(type, *args)
24
+ [type, *unify_columns(args)].join('__')
25
+ end
26
+
27
+ # @return [String]
28
+ def unify_columns(*args)
29
+ args.flatten.compact.map(&:to_s).sort
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ module DatabaseValidations
2
+ module PresenceKeyExtractor
3
+ module_function
4
+
5
+ # @param [DatabaseValidations::DbPresenceValidator]
6
+ #
7
+ # @return [Hash]
8
+ def attribute_by_key(validator)
9
+ validator.attributes.map do |attribute|
10
+ reflection = validator.klass._reflect_on_association(attribute)
11
+
12
+ key = reflection ? reflection.foreign_key : attribute
13
+
14
+ [KeyGenerator.for_db_presence(key), attribute]
15
+ end.to_h
16
+ end
17
+ end
18
+ end
@@ -1,31 +1,42 @@
1
1
  module DatabaseValidations
2
2
  module Rescuer
3
- extend ActiveSupport::Concern
3
+ module_function
4
4
 
5
- included do
6
- alias_method :validate, :valid?
5
+ def handled?(instance, error)
6
+ Storage.prepare(instance.class) unless Storage.prepared?(instance.class)
7
+
8
+ case error
9
+ when ActiveRecord::RecordNotUnique
10
+ process(instance, error, for_unique_index: :unique_index_name, for_db_uniqueness: :unique_error_columns)
11
+ when ActiveRecord::InvalidForeignKey
12
+ process(instance, error, for_db_presence: :foreign_key_error_column)
13
+ else false
14
+ end
7
15
  end
8
16
 
9
- attr_accessor :_database_validations_fallback
17
+ def process(instance, error, key_types)
18
+ adapter = Adapters.factory(instance.class)
10
19
 
11
- def valid?(context = nil)
12
- self._database_validations_fallback = true
13
- super(context)
14
- end
20
+ keys = key_types.map do |key_generator, error_processor|
21
+ KeyGenerator.public_send(key_generator, adapter.public_send(error_processor, error.message))
22
+ end
23
+
24
+ keys.each do |key|
25
+ attribute_validator = instance._db_validators[key]
26
+
27
+ next unless attribute_validator
15
28
 
16
- def create_or_update(*args, &block)
17
- self._database_validations_fallback = false
18
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
19
- rescue ActiveRecord::InvalidForeignKey, ActiveRecord::RecordNotUnique => error
20
- raise error unless Helpers.handle_error!(self, error)
29
+ return process_validator(instance, attribute_validator)
30
+ end
21
31
 
22
- raise ActiveRecord::RecordInvalid, self
32
+ false
23
33
  end
24
34
 
25
- private
35
+ def process_validator(instance, attribute_validator)
36
+ return false unless attribute_validator.validator.perform_db_validation?
26
37
 
27
- def perform_validations(options = {})
28
- options[:validate] == false || valid_without_database_validations?(options[:context])
38
+ attribute_validator.validator.apply_error(instance, attribute_validator.attribute)
39
+ true
29
40
  end
30
41
  end
31
42
  end
@@ -0,0 +1,28 @@
1
+ module DatabaseValidations
2
+ module Storage
3
+ module_function
4
+
5
+ def prepare(model)
6
+ model.class_attribute :_db_validators, instance_writer: false
7
+ model._db_validators = {}
8
+
9
+ model.validators.each do |validator|
10
+ case validator
11
+ when DbUniquenessValidator then process(validator, UniquenessKeyExtractor, model)
12
+ when DbPresenceValidator then process(validator, PresenceKeyExtractor, model)
13
+ else next
14
+ end
15
+ end
16
+ end
17
+
18
+ def process(validator, extractor, model)
19
+ extractor.attribute_by_key(validator).each do |key, attribute|
20
+ model._db_validators[key] = AttributeValidator.new(attribute, validator)
21
+ end
22
+ end
23
+
24
+ def prepared?(model)
25
+ model.respond_to?(:_db_validators)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ module DatabaseValidations
2
+ module UniquenessKeyExtractor
3
+ module_function
4
+
5
+ # @param [DatabaseValidations::DbUniquenessValidator]
6
+ #
7
+ # @return [Hash]
8
+ def attribute_by_columns_keys(validator)
9
+ validator.attributes.map do |attribute|
10
+ [KeyGenerator.for_db_uniqueness(attribute, Array.wrap(validator.options[:scope])), attribute]
11
+ end.to_h
12
+ end
13
+
14
+ # @param [DatabaseValidations::DbUniquenessValidator]
15
+ #
16
+ # @return [Hash]
17
+ def attribute_by_indexes_keys(validator) # rubocop:disable Metrics/AbcSize
18
+ adapter = Adapters::BaseAdapter.new(validator.klass)
19
+
20
+ if validator.index_name
21
+ [[KeyGenerator.for_unique_index(validator.index_name), validator.attributes[0]]].to_h
22
+ else
23
+ validator.attributes.map do |attribute|
24
+ columns = KeyGenerator.unify_columns(attribute, validator.options[:scope])
25
+ index = adapter.find_index(columns, validator.where)
26
+ [KeyGenerator.for_unique_index(index.name), attribute]
27
+ end.to_h
28
+ end
29
+ end
30
+
31
+ # @param [DatabaseValidations::DbUniquenessValidator]
32
+ #
33
+ # @return [Hash]
34
+ def attribute_by_key(validator)
35
+ attribute_by_columns_keys(validator).merge(attribute_by_indexes_keys(validator))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ module DatabaseValidations
2
+ module Validations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method :validate, :valid?
7
+ end
8
+
9
+ attr_accessor :_database_validations_fallback
10
+
11
+ def valid?(context = nil)
12
+ self._database_validations_fallback = true
13
+ super(context)
14
+ end
15
+
16
+ def create_or_update(*args, &block)
17
+ self._database_validations_fallback = false
18
+ ActiveRecord::Base.connection.transaction(requires_new: true) { super }
19
+ rescue ActiveRecord::InvalidForeignKey, ActiveRecord::RecordNotUnique => e
20
+ raise e unless Rescuer.handled?(self, e)
21
+
22
+ raise ActiveRecord::RecordInvalid, self
23
+ end
24
+
25
+ private
26
+
27
+ def perform_validations(options = {})
28
+ options[:validate] == false || valid_without_database_validations?(options[:context])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ module DatabaseValidations
2
+ class DbPresenceValidator < ActiveRecord::Validations::PresenceValidator
3
+ REFLECTION_MESSAGE = ActiveRecord::VERSION::MAJOR < 5 ? :blank : :required
4
+
5
+ attr_reader :klass
6
+
7
+ # Used to make 3rd party libraries work correctly
8
+ #
9
+ # @return [Symbol]
10
+ def self.kind
11
+ :presence
12
+ end
13
+
14
+ # @param [Hash] options
15
+ def initialize(options)
16
+ @klass = options[:class]
17
+
18
+ super
19
+
20
+ Injector.inject(klass)
21
+ Checkers::DbPresenceValidator.validate!(self)
22
+ end
23
+
24
+ def perform_db_validation?
25
+ true
26
+ end
27
+
28
+ # TODO: add support of optional db_belongs_to
29
+ def validate(record)
30
+ if record._database_validations_fallback
31
+ super
32
+ else
33
+ attributes.each do |attribute|
34
+ reflection = record.class._reflect_on_association(attribute)
35
+
36
+ next if reflection && record.public_send(reflection.foreign_key).present?
37
+
38
+ validate_each(record, attribute, record.public_send(attribute))
39
+ end
40
+ end
41
+ end
42
+
43
+ def apply_error(instance, attribute)
44
+ # Helps to avoid querying the database when attribute is association
45
+ instance.send("#{attribute}=", nil)
46
+ instance.errors.add(attribute, :blank, message: REFLECTION_MESSAGE)
47
+ end
48
+ end
49
+
50
+ module ClassMethods
51
+ def validates_db_presence_of(*attr_names)
52
+ validates_with(DatabaseValidations::DbPresenceValidator, _merge_attributes(attr_names))
53
+ end
54
+
55
+ def db_belongs_to(name, scope = nil, **options)
56
+ if ActiveRecord::VERSION::MAJOR < 5
57
+ options[:required] = false
58
+ else
59
+ options[:optional] = true
60
+ end
61
+
62
+ belongs_to(name, scope, **options)
63
+
64
+ validates_with DatabaseValidations::DbPresenceValidator, _merge_attributes([name, message: DatabaseValidations::DbPresenceValidator::REFLECTION_MESSAGE]) # rubocop:disable Metrics/LineLength
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,65 @@
1
+ module DatabaseValidations
2
+ class DbUniquenessValidator < ActiveRecord::Validations::UniquenessValidator
3
+ DEFAULT_MODE = :optimized
4
+
5
+ attr_reader :index_name, :where, :klass
6
+
7
+ # Used to make 3rd party libraries work correctly
8
+ #
9
+ # @return [Symbol]
10
+ def self.kind
11
+ :uniqueness
12
+ end
13
+
14
+ # @param [Hash] options
15
+ def initialize(options)
16
+ options[:allow_nil] = true
17
+ options[:allow_blank] = false
18
+
19
+ if options.key?(:where)
20
+ condition = options[:where]
21
+ options[:conditions] = -> { where(condition) }
22
+ end
23
+
24
+ handle_custom_options(options)
25
+
26
+ super
27
+
28
+ Injector.inject(klass)
29
+ Checkers::DbUniquenessValidator.validate!(self)
30
+ end
31
+
32
+ def perform_db_validation?
33
+ @mode != :standard
34
+ end
35
+
36
+ def validate(record)
37
+ super if perform_query? || record._database_validations_fallback
38
+ end
39
+
40
+ def apply_error(instance, attribute)
41
+ error_options = options.except(:case_sensitive, :scope, :conditions)
42
+ error_options[:value] = instance.public_send(attribute)
43
+
44
+ instance.errors.add(attribute, :taken, error_options)
45
+ end
46
+
47
+ private
48
+
49
+ def handle_custom_options(options)
50
+ @index_name = options.delete(:index_name) if options.key?(:index_name)
51
+ @where = options.delete(:where) if options.key?(:where)
52
+ @mode = (options.delete(:mode).presence || DEFAULT_MODE).to_sym
53
+ end
54
+
55
+ def perform_query?
56
+ @mode != :optimized
57
+ end
58
+ end
59
+
60
+ module ClassMethods
61
+ def validates_db_uniqueness_of(*attr_names)
62
+ validates_with(DatabaseValidations::DbUniquenessValidator, _merge_attributes(attr_names))
63
+ end
64
+ end
65
+ end
@@ -41,31 +41,37 @@ RSpec::Matchers.define :validate_db_uniqueness_of do |field| # rubocop:disable M
41
41
  @case_sensitive = false
42
42
  end
43
43
 
44
+ chain(:case_sensitive) do
45
+ @case_sensitive = true
46
+ end
47
+
44
48
  match do |object|
45
49
  @validators = []
46
50
 
47
51
  model = object.is_a?(Class) ? object : object.class
48
52
 
49
- DatabaseValidations::Helpers.each_options_storage(model) do |storage|
50
- storage.options.grep(DatabaseValidations::UniquenessOptions).each do |validator|
53
+ model.validators.grep(DatabaseValidations::DbUniquenessValidator).each do |validator|
54
+ validator.attributes.each do |attribute|
51
55
  @validators << {
52
- field: validator.field,
53
- scope: validator.scope,
54
- where: validator.where_clause,
55
- message: validator.message,
56
+ field: attribute,
57
+ scope: Array.wrap(validator.options[:scope]),
58
+ where: validator.where,
59
+ message: validator.options[:message],
56
60
  index_name: validator.index_name,
57
- case_sensitive: validator.case_sensitive
61
+ case_sensitive: validator.options[:case_sensitive]
58
62
  }
59
63
  end
60
64
  end
61
65
 
66
+ case_sensitive_default = ActiveRecord::VERSION::MAJOR >= 6 ? nil : true
67
+
62
68
  @validators.include?(
63
69
  field: field,
64
70
  scope: Array.wrap(@scope),
65
71
  where: @where,
66
72
  message: @message,
67
73
  index_name: @index_name,
68
- case_sensitive: @case_sensitive
74
+ case_sensitive: @case_sensitive.nil? ? case_sensitive_default : @case_sensitive
69
75
  )
70
76
  end
71
77
 
@@ -77,6 +83,7 @@ RSpec::Matchers.define :validate_db_uniqueness_of do |field| # rubocop:disable M
77
83
  desc += "where: '#{@where}'; " if @where
78
84
  desc += "index_name: '#{@index_name}'; " if @index_name
79
85
  desc += 'be case insensitive.' unless @case_sensitive
86
+ desc += 'be case sensitive.' if @case_sensitive
80
87
  desc
81
88
  end
82
89
 
@@ -1,3 +1,3 @@
1
1
  module DatabaseValidations
2
- VERSION = '0.8.9'.freeze
2
+ VERSION = '0.9.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: database_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.9
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-13 00:00:00.000000000 Z
11
+ date: 2020-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,20 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.2'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '6'
19
+ version: 4.2.0
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
- version: '3.2'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '6'
26
+ version: 4.2.0
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: benchmark-ips
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -50,56 +44,70 @@ dependencies:
50
44
  requirements:
51
45
  - - "~>"
52
46
  - !ruby/object:Gem::Version
53
- version: '1.16'
47
+ version: '2.0'
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
51
  requirements:
58
52
  - - "~>"
59
53
  - !ruby/object:Gem::Version
60
- version: '1.16'
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: db-query-matchers
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0.9'
61
69
  - !ruby/object:Gem::Dependency
62
70
  name: mysql2
63
71
  requirement: !ruby/object:Gem::Requirement
64
72
  requirements:
65
- - - "~>"
73
+ - - ">="
66
74
  - !ruby/object:Gem::Version
67
- version: '0.5'
75
+ version: '0'
68
76
  type: :development
69
77
  prerelease: false
70
78
  version_requirements: !ruby/object:Gem::Requirement
71
79
  requirements:
72
- - - "~>"
80
+ - - ">="
73
81
  - !ruby/object:Gem::Version
74
- version: '0.5'
82
+ version: '0'
75
83
  - !ruby/object:Gem::Dependency
76
84
  name: pg
77
85
  requirement: !ruby/object:Gem::Requirement
78
86
  requirements:
79
- - - "~>"
87
+ - - ">="
80
88
  - !ruby/object:Gem::Version
81
- version: '1.1'
89
+ version: '0'
82
90
  type: :development
83
91
  prerelease: false
84
92
  version_requirements: !ruby/object:Gem::Requirement
85
93
  requirements:
86
- - - "~>"
94
+ - - ">="
87
95
  - !ruby/object:Gem::Version
88
- version: '1.1'
96
+ version: '0'
89
97
  - !ruby/object:Gem::Dependency
90
98
  name: rake
91
99
  requirement: !ruby/object:Gem::Requirement
92
100
  requirements:
93
101
  - - "~>"
94
102
  - !ruby/object:Gem::Version
95
- version: '10.0'
103
+ version: '13.0'
96
104
  type: :development
97
105
  prerelease: false
98
106
  version_requirements: !ruby/object:Gem::Requirement
99
107
  requirements:
100
108
  - - "~>"
101
109
  - !ruby/object:Gem::Version
102
- version: '10.0'
110
+ version: '13.0'
103
111
  - !ruby/object:Gem::Dependency
104
112
  name: rspec
105
113
  requirement: !ruby/object:Gem::Requirement
@@ -175,16 +183,19 @@ files:
175
183
  - lib/database_validations/lib/adapters/mysql_adapter.rb
176
184
  - lib/database_validations/lib/adapters/postgresql_adapter.rb
177
185
  - lib/database_validations/lib/adapters/sqlite_adapter.rb
178
- - lib/database_validations/lib/db_belongs_to/belongs_to_handlers.rb
179
- - lib/database_validations/lib/db_belongs_to/belongs_to_options.rb
180
- - lib/database_validations/lib/db_belongs_to/db_presence_validator.rb
186
+ - lib/database_validations/lib/attribute_validator.rb
187
+ - lib/database_validations/lib/checkers/db_presence_validator.rb
188
+ - lib/database_validations/lib/checkers/db_uniqueness_validator.rb
181
189
  - lib/database_validations/lib/errors.rb
182
- - lib/database_validations/lib/helpers.rb
183
- - lib/database_validations/lib/options_storage.rb
190
+ - lib/database_validations/lib/injector.rb
191
+ - lib/database_validations/lib/key_generator.rb
192
+ - lib/database_validations/lib/presence_key_extractor.rb
184
193
  - lib/database_validations/lib/rescuer.rb
185
- - lib/database_validations/lib/validates_db_uniqueness_of/db_uniqueness_validator.rb
186
- - lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers.rb
187
- - lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_options.rb
194
+ - lib/database_validations/lib/storage.rb
195
+ - lib/database_validations/lib/uniqueness_key_extractor.rb
196
+ - lib/database_validations/lib/validations.rb
197
+ - lib/database_validations/lib/validators/db_presence_validator.rb
198
+ - lib/database_validations/lib/validators/db_uniqueness_validator.rb
188
199
  - lib/database_validations/rails/railtie.rb
189
200
  - lib/database_validations/rspec/matchers.rb
190
201
  - lib/database_validations/rspec/uniqueness_validator_matcher.rb
@@ -197,7 +208,7 @@ homepage: https://github.com/toptal/database_validations
197
208
  licenses:
198
209
  - MIT
199
210
  metadata: {}
200
- post_install_message:
211
+ post_install_message:
201
212
  rdoc_options: []
202
213
  require_paths:
203
214
  - lib
@@ -212,9 +223,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
223
  - !ruby/object:Gem::Version
213
224
  version: '0'
214
225
  requirements: []
215
- rubyforge_project:
216
- rubygems_version: 2.7.8
217
- signing_key:
226
+ rubygems_version: 3.0.8
227
+ signing_key:
218
228
  specification_version: 4
219
229
  summary: Provide compatibility between database constraints and ActiveRecord validations
220
230
  with better performance and consistency.
@@ -1,20 +0,0 @@
1
- module DatabaseValidations
2
- module ClassMethods
3
- def db_belongs_to(name, scope = nil, **options)
4
- Helpers.cache_valid_method!(self)
5
-
6
- @database_validations_storage ||= DatabaseValidations::OptionsStorage.new(self)
7
-
8
- belongs_to(name, scope, options.merge(optional: true))
9
-
10
- foreign_key = reflections[name.to_s].foreign_key
11
-
12
- @database_validations_storage.push_belongs_to(foreign_key, name)
13
-
14
- validates_with DatabaseValidations::DBPresenceValidator,
15
- DatabaseValidations::BelongsToOptions.validator_options(name, foreign_key)
16
-
17
- include(DatabaseValidations::Rescuer)
18
- end
19
- end
20
- end
@@ -1,39 +0,0 @@
1
- module DatabaseValidations
2
- class BelongsToOptions
3
- def self.validator_options(association, foreign_key)
4
- { attributes: association, foreign_key: foreign_key, message: :required }
5
- end
6
-
7
- attr_reader :column, :adapter, :relation
8
-
9
- def initialize(column, relation, adapter)
10
- @column = column
11
- @relation = relation
12
- @adapter = adapter
13
-
14
- raise_if_unsupported_database!
15
- raise_if_foreign_key_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
16
- end
17
-
18
- # @return [String]
19
- def key
20
- @key ||= Helpers.generate_key_for_belongs_to(column)
21
- end
22
-
23
- def handle_foreign_key_error(instance)
24
- # Hack to not query the database because we know the result already
25
- instance.send("#{relation}=", nil)
26
- instance.errors.add(relation, :blank, message: :required)
27
- end
28
-
29
- private
30
-
31
- def raise_if_foreign_key_missed!
32
- raise Errors::ForeignKeyNotFound.new(column, adapter.foreign_keys) unless adapter.find_foreign_key_by_column(column)
33
- end
34
-
35
- def raise_if_unsupported_database!
36
- raise Errors::UnsupportedDatabase.new(:db_belongs_to, adapter.adapter_name) if adapter.adapter_name == :sqlite3
37
- end
38
- end
39
- end
@@ -1,30 +0,0 @@
1
- module DatabaseValidations
2
- class DBPresenceValidator < ActiveRecord::Validations::PresenceValidator
3
- # This is a hack to simulate presence validator
4
- # It's used for cases when some 3rd parties are relies on the validators
5
- # For example, +required+ option from simple_form checks the validator
6
- def self.kind
7
- :presence
8
- end
9
-
10
- attr_reader :foreign_key, :association
11
-
12
- def initialize(options)
13
- super(options)
14
-
15
- @association = attributes.first
16
- @foreign_key = options[:foreign_key]
17
- end
18
-
19
- # The else statement required only for optional: false
20
- def validate(record)
21
- if record._database_validations_fallback
22
- super
23
- else
24
- return unless record.public_send(foreign_key).blank? && record.public_send(association).blank?
25
-
26
- record.errors.add(association, :blank, message: :required)
27
- end
28
- end
29
- end
30
- end
@@ -1,79 +0,0 @@
1
- module DatabaseValidations
2
- module Helpers
3
- module_function
4
-
5
- def cache_valid_method!(klass)
6
- return if klass.method_defined?(:valid_without_database_validations?)
7
-
8
- klass.alias_method(:valid_without_database_validations?, :valid?)
9
- end
10
-
11
- def handle_error!(instance, error)
12
- case error
13
- when ActiveRecord::RecordNotUnique
14
- handle_unique_error!(instance, error)
15
- when ActiveRecord::InvalidForeignKey
16
- handle_foreign_key_error!(instance, error)
17
- else false
18
- end
19
- end
20
-
21
- def handle_unique_error!(instance, error)
22
- adapter = Adapters.factory(instance.class)
23
-
24
- keys = [
25
- generate_key_for_uniqueness_index(adapter.unique_index_name(error.message)),
26
- generate_key_for_uniqueness(adapter.unique_error_columns(error.message))
27
- ]
28
-
29
- each_options_storage(instance.class) do |storage|
30
- keys.each { |key| return storage[key].handle_unique_error(instance) if storage[key] }
31
- end
32
-
33
- false
34
- end
35
-
36
- def handle_foreign_key_error!(instance, error)
37
- adapter = Adapters.factory(instance.class)
38
- column_key = generate_key_for_belongs_to(adapter.foreign_key_error_column(error.message))
39
-
40
- each_options_storage(instance.class) do |storage|
41
- return storage[column_key].handle_foreign_key_error(instance) if storage[column_key]
42
- end
43
-
44
- false
45
- end
46
-
47
- def each_options_storage(klass)
48
- while klass.respond_to?(:validates_db_uniqueness_of)
49
- storage = storage_of(klass)
50
- yield(storage) if storage
51
- klass = klass.superclass
52
- end
53
- end
54
-
55
- def storage_of(klass)
56
- klass.instance_variable_get(:'@database_validations_storage')
57
- end
58
-
59
- def unify_columns(*args)
60
- args.flatten.compact.map(&:to_s).sort
61
- end
62
-
63
- def generate_key_for_uniqueness_index(index_name)
64
- generate_key(:uniqueness_index, index_name)
65
- end
66
-
67
- def generate_key_for_uniqueness(*columns)
68
- generate_key(:uniqueness, columns)
69
- end
70
-
71
- def generate_key_for_belongs_to(column)
72
- generate_key(:belongs_to, column)
73
- end
74
-
75
- def generate_key(type, *args)
76
- [type, *unify_columns(args)].join('__')
77
- end
78
- end
79
- end
@@ -1,31 +0,0 @@
1
- module DatabaseValidations
2
- class OptionsStorage
3
- def initialize(klass)
4
- @adapter = Adapters.factory(klass).new(klass)
5
- @storage = {}
6
- end
7
-
8
- def push_uniqueness(field, options)
9
- uniqueness_options = UniquenessOptions.new(field, options, adapter)
10
- storage[uniqueness_options.index_key] = uniqueness_options
11
- storage[uniqueness_options.column_key] = uniqueness_options
12
- end
13
-
14
- def push_belongs_to(field, relation)
15
- belongs_to_options = BelongsToOptions.new(field, relation, adapter)
16
- storage[belongs_to_options.key] = belongs_to_options
17
- end
18
-
19
- def [](key)
20
- storage[key]
21
- end
22
-
23
- def options
24
- storage.values
25
- end
26
-
27
- private
28
-
29
- attr_reader :storage, :adapter
30
- end
31
- end
@@ -1,14 +0,0 @@
1
- module DatabaseValidations
2
- class DBUniquenessValidator < ActiveRecord::Validations::UniquenessValidator
3
- # This is a hack to simulate presence validator
4
- # It's used for cases when some 3rd parties are relies on the validators
5
- # For example, +required+ option from simple_form checks the validator
6
- def self.kind
7
- :uniqueness
8
- end
9
-
10
- def validate(record)
11
- super if record._database_validations_fallback
12
- end
13
- end
14
- end
@@ -1,20 +0,0 @@
1
- module DatabaseValidations
2
- module ClassMethods
3
- def validates_db_uniqueness_of(*attributes)
4
- Helpers.cache_valid_method!(self)
5
-
6
- @database_validations_storage ||= DatabaseValidations::OptionsStorage.new(self)
7
-
8
- options = attributes.extract_options!
9
-
10
- attributes.each do |attribute|
11
- @database_validations_storage.push_uniqueness(attribute, options.merge(attributes: attribute))
12
- end
13
-
14
- validates_with DatabaseValidations::DBUniquenessValidator,
15
- DatabaseValidations::UniquenessOptions.validator_options(attributes, options)
16
-
17
- include(DatabaseValidations::Rescuer)
18
- end
19
- end
20
- end
@@ -1,98 +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
- def self.validator_options(attributes, options)
7
- DEFAULT_OPTIONS
8
- .merge(attributes: attributes)
9
- .merge(options)
10
- .except(*CUSTOM_OPTIONS)
11
- .tap { |opts| opts[:conditions] = -> { where(options[:where]) } if options[:where] }
12
- end
13
-
14
- attr_reader :field, :calculated_index_name
15
-
16
- def initialize(field, options, adapter)
17
- @field = field
18
- @options = options
19
- @adapter = adapter
20
-
21
- raise_if_unsupported_options!
22
-
23
- return if ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
24
-
25
- index = responsible_index
26
- raise_if_index_missed!(index)
27
-
28
- @calculated_index_name = index.name
29
- end
30
-
31
- def handle_unique_error(instance)
32
- error_options = options.except(:case_sensitive, :scope, :conditions, :attributes, *CUSTOM_OPTIONS)
33
- error_options[:value] = instance.public_send(options[:attributes])
34
-
35
- instance.errors.add(options[:attributes], :taken, error_options)
36
- end
37
-
38
- # @return [String]
39
- def index_key
40
- @index_key ||= Helpers.generate_key_for_uniqueness_index(index_name || calculated_index_name)
41
- end
42
-
43
- # @return [String]
44
- def column_key
45
- @column_key ||= Helpers.generate_key_for_uniqueness(columns)
46
- end
47
-
48
- # @return [Array<String>]
49
- def columns
50
- @columns ||= Helpers.unify_columns(field, scope)
51
- end
52
-
53
- # @return [String|nil]
54
- def where_clause
55
- @where_clause ||= options[:where]
56
- end
57
-
58
- # @return [String|nil]
59
- def message
60
- @message ||= options[:message]
61
- end
62
-
63
- # @return [Array<String|Symbol>]
64
- def scope
65
- @scope ||= Array.wrap(options[:scope])
66
- end
67
-
68
- # @return [String|Symbol|nil]
69
- def index_name
70
- @index_name ||= options[:index_name]
71
- end
72
-
73
- # @return [Boolean|nil]
74
- def case_sensitive
75
- @case_sensitive ||= options[:case_sensitive]
76
- end
77
-
78
- private
79
-
80
- attr_reader :adapter, :options
81
-
82
- def raise_if_unsupported_options!
83
- options.except(:attributes).each_key do |option|
84
- unless adapter.support_option?(option)
85
- raise Errors::OptionIsNotSupported.new(option, adapter.adapter_name, adapter.supported_options)
86
- end
87
- end
88
- end
89
-
90
- def responsible_index
91
- index_name ? adapter.find_index_by_name(index_name.to_s) : adapter.find_index(columns, where_clause)
92
- end
93
-
94
- def raise_if_index_missed!(index)
95
- raise Errors::IndexNotFound.new(columns, where_clause, index_name, adapter.indexes, adapter.table_name) unless index
96
- end
97
- end
98
- end