database_validations 0.8.9 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
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