database_validations 0.8.10 → 0.9.0

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 +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