database_validations 0.8.10 → 0.9.0

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 +0 -1
  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 +24 -19
  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 +63 -0
  19. data/lib/database_validations/lib/validators/db_uniqueness_validator.rb +48 -0
  20. data/lib/database_validations/rspec/uniqueness_validator_matcher.rb +10 -8
  21. data/lib/database_validations/version.rb +1 -1
  22. metadata +34 -17
  23. data/lib/database_validations/lib/db_belongs_to/belongs_to_handlers.rb +0 -30
  24. data/lib/database_validations/lib/db_belongs_to/belongs_to_options.rb +0 -46
  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 -108
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c934b2c71117e26cc8493d7eafb35a681cb54f77bb6a1ad7d9ba0ec309664817
4
- data.tar.gz: 7b7e34001d44eac5b818d756288ecb5e5d7f33147d5f8bbc07a3e54b0de6287c
3
+ metadata.gz: 70578fbf45115b1b0a349f2676b2c1a689f2aa8d29da2da189da263306c0b333
4
+ data.tar.gz: f6fc66d339e488cccdf750103637543ecd39d34a0a84366efa2382addcde6aee
5
5
  SHA512:
6
- metadata.gz: 544cee2e2edf40aec5a25bfdc789df6324d07120997a1a68b366eac75152eeb095ca9cfb3758c32842e189da2ad5306e4530faaa24fa2ecbf44c93c44995e2c9
7
- data.tar.gz: 3e006dfe1110c3758a3a971a2db45660a73427e6b9007d140f1e338d8dd02bd099c5a9cd29f78b8018c1f541c8c67f4bd064a845bbbf5bb6f1a18ff21d6d679c
6
+ metadata.gz: 6d40cf1b7d776cf984846f9cef85f1969271d619ae4ee9b9127fe67307c8983eb0ce385883541394932d9cc1f88a2e69fccdf5ff6b800ee06c5a1f5710b21495
7
+ data.tar.gz: 1e94c35ae4a40a56e275b6bad79a05cf0c3f29b735cf37495c2fb5e9013d2929ab0a54f025c6221a5fa8179a26d3660c9518ba8d5c1f4310ea3832b4ab4cdb28
@@ -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,7 +1,6 @@
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
@@ -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,36 @@
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?
7
- end
8
-
9
- attr_accessor :_database_validations_fallback
5
+ def handled?(instance, error)
6
+ Storage.prepare(instance.class) unless Storage.prepared?(instance.class)
10
7
 
11
- def valid?(context = nil)
12
- self._database_validations_fallback = true
13
- super(context)
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
14
15
  end
15
16
 
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)
17
+ def process(instance, error, key_types)
18
+ adapter = Adapters.factory(instance.class)
21
19
 
22
- raise ActiveRecord::RecordInvalid, self
23
- 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]
24
26
 
25
- private
27
+ if attribute_validator
28
+ attribute_validator.validator.apply_error(instance, attribute_validator.attribute)
29
+ return true
30
+ end
31
+ end
26
32
 
27
- def perform_validations(options = {})
28
- options[:validate] == false || valid_without_database_validations?(options[:context])
33
+ false
29
34
  end
30
35
  end
31
36
  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,63 @@
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
+ # TODO: add support of optional db_belongs_to
25
+ def validate(record)
26
+ if record._database_validations_fallback
27
+ super
28
+ else
29
+ attributes.each do |attribute|
30
+ reflection = record.class._reflect_on_association(attribute)
31
+
32
+ next if reflection && record.public_send(reflection.foreign_key).present?
33
+
34
+ validate_each(record, attribute, record.public_send(attribute))
35
+ end
36
+ end
37
+ end
38
+
39
+ def apply_error(instance, attribute)
40
+ # Helps to avoid querying the database when attribute is association
41
+ instance.send("#{attribute}=", nil)
42
+ instance.errors.add(attribute, :blank, message: REFLECTION_MESSAGE)
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ def validates_db_presence_of(*attr_names)
48
+ validates_with(DatabaseValidations::DbPresenceValidator, _merge_attributes(attr_names))
49
+ end
50
+
51
+ def db_belongs_to(name, scope = nil, **options)
52
+ if ActiveRecord::VERSION::MAJOR < 5
53
+ options[:required] = false
54
+ else
55
+ options[:optional] = true
56
+ end
57
+
58
+ belongs_to(name, scope, options)
59
+
60
+ validates_with DatabaseValidations::DbPresenceValidator, _merge_attributes([name, message: DatabaseValidations::DbPresenceValidator::REFLECTION_MESSAGE]) # rubocop:disable Metrics/LineLength
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,48 @@
1
+ module DatabaseValidations
2
+ class DbUniquenessValidator < ActiveRecord::Validations::UniquenessValidator
3
+ attr_reader :index_name, :where, :klass
4
+
5
+ # Used to make 3rd party libraries work correctly
6
+ #
7
+ # @return [Symbol]
8
+ def self.kind
9
+ :uniqueness
10
+ end
11
+
12
+ # @param [Hash] options
13
+ def initialize(options)
14
+ options[:allow_nil] = true
15
+ options[:allow_blank] = false
16
+
17
+ if options.key?(:where)
18
+ condition = options[:where]
19
+ options[:conditions] = -> { where(condition) }
20
+ end
21
+
22
+ @index_name = options.delete(:index_name) if options.key?(:index_name)
23
+ @where = options.delete(:where) if options.key?(:where)
24
+
25
+ super
26
+
27
+ Injector.inject(klass)
28
+ Checkers::DbUniquenessValidator.validate!(self)
29
+ end
30
+
31
+ def validate(record)
32
+ super if record._database_validations_fallback
33
+ end
34
+
35
+ def apply_error(instance, attribute)
36
+ error_options = options.except(:case_sensitive, :scope, :conditions)
37
+ error_options[:value] = instance.public_send(attribute)
38
+
39
+ instance.errors.add(attribute, :taken, error_options)
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ def validates_db_uniqueness_of(*attr_names)
45
+ validates_with(DatabaseValidations::DbUniquenessValidator, _merge_attributes(attr_names))
46
+ end
47
+ end
48
+ end
@@ -46,26 +46,28 @@ RSpec::Matchers.define :validate_db_uniqueness_of do |field| # rubocop:disable M
46
46
 
47
47
  model = object.is_a?(Class) ? object : object.class
48
48
 
49
- DatabaseValidations::Helpers.each_options_storage(model) do |storage|
50
- storage.options.grep(DatabaseValidations::UniquenessOptions).each do |validator|
49
+ model.validators.grep(DatabaseValidations::DbUniquenessValidator).each do |validator|
50
+ validator.attributes.each do |attribute|
51
51
  @validators << {
52
- field: validator.field,
53
- scope: validator.scope,
54
- where: validator.where_clause,
55
- message: validator.message,
52
+ field: attribute,
53
+ scope: Array.wrap(validator.options[:scope]),
54
+ where: validator.where,
55
+ message: validator.options[:message],
56
56
  index_name: validator.index_name,
57
- case_sensitive: validator.case_sensitive
57
+ case_sensitive: validator.options[:case_sensitive]
58
58
  }
59
59
  end
60
60
  end
61
61
 
62
+ case_sensitive_default = ActiveRecord::VERSION::MAJOR >= 6 ? nil : true
63
+
62
64
  @validators.include?(
63
65
  field: field,
64
66
  scope: Array.wrap(@scope),
65
67
  where: @where,
66
68
  message: @message,
67
69
  index_name: @index_name,
68
- case_sensitive: @case_sensitive
70
+ case_sensitive: @case_sensitive.nil? ? case_sensitive_default : @case_sensitive
69
71
  )
70
72
  end
71
73
 
@@ -1,3 +1,3 @@
1
1
  module DatabaseValidations
2
- VERSION = '0.8.10'.freeze
2
+ VERSION = '0.9.0'.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.10
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-21 00:00:00.000000000 Z
11
+ date: 2019-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -44,14 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.16'
47
+ version: '2.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- 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'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: mysql2
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +100,14 @@ dependencies:
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '10.0'
103
+ version: '12.3'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: '10.0'
110
+ version: '12.3'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rspec
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +156,14 @@ dependencies:
142
156
  requirements:
143
157
  - - "~>"
144
158
  - !ruby/object:Gem::Version
145
- version: 1.3.0
159
+ version: '1.3'
146
160
  type: :development
147
161
  prerelease: false
148
162
  version_requirements: !ruby/object:Gem::Requirement
149
163
  requirements:
150
164
  - - "~>"
151
165
  - !ruby/object:Gem::Version
152
- version: 1.3.0
166
+ version: '1.3'
153
167
  description: |-
154
168
  ActiveRecord provides validations on app level but it won't guarantee the
155
169
  consistent. In some cases, like `validates_uniqueness_of` it executes
@@ -169,16 +183,19 @@ files:
169
183
  - lib/database_validations/lib/adapters/mysql_adapter.rb
170
184
  - lib/database_validations/lib/adapters/postgresql_adapter.rb
171
185
  - lib/database_validations/lib/adapters/sqlite_adapter.rb
172
- - lib/database_validations/lib/db_belongs_to/belongs_to_handlers.rb
173
- - lib/database_validations/lib/db_belongs_to/belongs_to_options.rb
174
- - 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
175
189
  - lib/database_validations/lib/errors.rb
176
- - lib/database_validations/lib/helpers.rb
177
- - 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
178
193
  - lib/database_validations/lib/rescuer.rb
179
- - lib/database_validations/lib/validates_db_uniqueness_of/db_uniqueness_validator.rb
180
- - lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers.rb
181
- - 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
182
199
  - lib/database_validations/rails/railtie.rb
183
200
  - lib/database_validations/rspec/matchers.rb
184
201
  - lib/database_validations/rspec/uniqueness_validator_matcher.rb
@@ -207,7 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
224
  version: '0'
208
225
  requirements: []
209
226
  rubyforge_project:
210
- rubygems_version: 2.7.8
227
+ rubygems_version: 2.7.9
211
228
  signing_key:
212
229
  specification_version: 4
213
230
  summary: Provide compatibility between database constraints and ActiveRecord validations
@@ -1,30 +0,0 @@
1
- module DatabaseValidations
2
- module ClassMethods
3
- # rubocop: disable Metrics/AbcSize
4
- def db_belongs_to(name, scope = nil, **options)
5
- Helpers.cache_valid_method!(self)
6
-
7
- @database_validations_storage ||= DatabaseValidations::OptionsStorage.new(self)
8
-
9
- belongs_to_options =
10
- if ActiveRecord::VERSION::MAJOR < 5
11
- options.delete(:optional)
12
- options.merge(required: false)
13
- else
14
- options.merge(optional: true)
15
- end
16
-
17
- belongs_to(name, scope, belongs_to_options)
18
-
19
- foreign_key = reflections[name.to_s].foreign_key
20
-
21
- @database_validations_storage.push_belongs_to(foreign_key, name)
22
-
23
- validates_with DatabaseValidations::DBPresenceValidator,
24
- DatabaseValidations::BelongsToOptions.validator_options(name, foreign_key)
25
-
26
- include(DatabaseValidations::Rescuer)
27
- end
28
- end
29
- # rubocop: enable Metrics/AbcSize
30
- end
@@ -1,46 +0,0 @@
1
- module DatabaseValidations
2
- class BelongsToOptions
3
- VALIDATOR_MESSAGE =
4
- if ActiveRecord::VERSION::MAJOR < 5
5
- :blank
6
- else
7
- :required
8
- end
9
-
10
- def self.validator_options(association, foreign_key)
11
- { attributes: association, foreign_key: foreign_key, message: VALIDATOR_MESSAGE }
12
- end
13
-
14
- attr_reader :column, :adapter, :relation
15
-
16
- def initialize(column, relation, adapter)
17
- @column = column
18
- @relation = relation
19
- @adapter = adapter
20
-
21
- raise_if_unsupported_database!
22
- raise_if_foreign_key_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
23
- end
24
-
25
- # @return [String]
26
- def key
27
- @key ||= Helpers.generate_key_for_belongs_to(column)
28
- end
29
-
30
- def handle_foreign_key_error(instance)
31
- # Hack to not query the database because we know the result already
32
- instance.send("#{relation}=", nil)
33
- instance.errors.add(relation, :blank, message: VALIDATOR_MESSAGE)
34
- end
35
-
36
- private
37
-
38
- def raise_if_foreign_key_missed!
39
- raise Errors::ForeignKeyNotFound.new(column, adapter.foreign_keys) unless adapter.find_foreign_key_by_column(column)
40
- end
41
-
42
- def raise_if_unsupported_database!
43
- raise Errors::UnsupportedDatabase.new(:db_belongs_to, adapter.adapter_name) if adapter.adapter_name == :sqlite3
44
- end
45
- end
46
- 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: BelongsToOptions::VALIDATOR_MESSAGE)
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.__send__(: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,108 +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 index_columns_size(columns)
95
- columns.is_a?(Array) ? columns.size : (columns.count(',') + 1)
96
- end
97
-
98
- def check_index_options?(index)
99
- (columns.size == index_columns_size(index.columns)) && (where_clause.nil? == index.where.nil?)
100
- end
101
-
102
- def raise_if_index_missed!(index)
103
- return if index && check_index_options?(index)
104
-
105
- raise Errors::IndexNotFound.new(columns, where_clause, index_name, adapter.indexes, adapter.table_name)
106
- end
107
- end
108
- end