database_validations 0.8.5 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/database_validations.rb +14 -10
  3. data/lib/database_validations/lib/adapters.rb +20 -0
  4. data/lib/database_validations/{validations → lib}/adapters/base_adapter.rb +1 -17
  5. data/lib/database_validations/lib/adapters/mysql_adapter.rb +19 -0
  6. data/lib/database_validations/lib/adapters/postgresql_adapter.rb +21 -0
  7. data/lib/database_validations/lib/adapters/sqlite_adapter.rb +19 -0
  8. data/lib/database_validations/lib/attribute_validator.rb +3 -0
  9. data/lib/database_validations/lib/checkers/db_presence_validator.rb +36 -0
  10. data/lib/database_validations/lib/checkers/db_uniqueness_validator.rb +47 -0
  11. data/lib/database_validations/{validations → lib}/errors.rb +1 -12
  12. data/lib/database_validations/lib/injector.rb +13 -0
  13. data/lib/database_validations/lib/key_generator.rb +32 -0
  14. data/lib/database_validations/lib/presence_key_extractor.rb +18 -0
  15. data/lib/database_validations/lib/rescuer.rb +36 -0
  16. data/lib/database_validations/lib/storage.rb +28 -0
  17. data/lib/database_validations/lib/uniqueness_key_extractor.rb +38 -0
  18. data/lib/database_validations/lib/validations.rb +31 -0
  19. data/lib/database_validations/lib/validators/db_presence_validator.rb +63 -0
  20. data/lib/database_validations/lib/validators/db_uniqueness_validator.rb +48 -0
  21. data/lib/database_validations/rspec/uniqueness_validator_matcher.rb +19 -10
  22. data/lib/database_validations/version.rb +1 -1
  23. metadata +52 -41
  24. data/lib/database_validations/validations/adapters.rb +0 -20
  25. data/lib/database_validations/validations/adapters/mysql_adapter.rb +0 -20
  26. data/lib/database_validations/validations/adapters/postgresql_adapter.rb +0 -20
  27. data/lib/database_validations/validations/adapters/sqlite_adapter.rb +0 -22
  28. data/lib/database_validations/validations/belongs_to_handlers.rb +0 -58
  29. data/lib/database_validations/validations/belongs_to_options.rb +0 -44
  30. data/lib/database_validations/validations/belongs_to_presence_validator.rb +0 -25
  31. data/lib/database_validations/validations/helpers.rb +0 -69
  32. data/lib/database_validations/validations/options_storage.rb +0 -34
  33. data/lib/database_validations/validations/uniqueness_handlers.rb +0 -56
  34. data/lib/database_validations/validations/uniqueness_options.rb +0 -104
  35. data/lib/database_validations/validations/valid_without_database_validations.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51bc2df5e836cbe85e6ec06cc3446652458093b3dba7d7dbd9123e579d67d6ce
4
- data.tar.gz: 82b5fdfec69d72f1fecf811f6add8f4e5d606ed2adbd2a6fab1a90125b0e57e2
3
+ metadata.gz: 7611b039f9baa05f65bee0a18cbfc2cfe2c0ae5e7f69bc01d232b00de5306f92
4
+ data.tar.gz: 9601fd3196555c0d861b5fb1a3809d25814453db9199fbcc0b115b7976a3012d
5
5
  SHA512:
6
- metadata.gz: 7a97f163683bc28f3884dbdd5b8d26a4059443199211bd4e5c980e5abdd1ef2b5d37efcc6c8557e0a853fe0fdb1f247f5677b6c0534b7f632bfb25af20e86606
7
- data.tar.gz: 556d083c72c34d0b90e381e052999289eb797ba74cd1b938fe5a1a53cdd47511d41472671ea5c10902b6bc02049e1772a2d7b83f0ec0fd1ff43c8298235e4a50
6
+ metadata.gz: 8a72b136bdd5528cf93dc019c80ecf88b09a9484a68d523e5574e28ee7ffb122122eda7cf3062655015f59637d4dd06de4c3c028c79067fea101b326f15a80e6
7
+ data.tar.gz: bb9ee57860b06c58853db54583b08acd953f0cdf037d0e11a5faee3b72aa748b022051b3ee21a8fbe3d95f18f7c460c5a1d91e77cbbd2bdf5d4237802b9d8ad4
@@ -4,18 +4,22 @@ require 'database_validations/version'
4
4
 
5
5
  require 'database_validations/rails/railtie' if defined?(Rails)
6
6
 
7
- require 'database_validations/validations/uniqueness_handlers'
8
- require 'database_validations/validations/uniqueness_options'
7
+ require 'database_validations/lib/checkers/db_uniqueness_validator'
8
+ require 'database_validations/lib/checkers/db_presence_validator'
9
9
 
10
- require 'database_validations/validations/belongs_to_presence_validator'
11
- require 'database_validations/validations/belongs_to_handlers'
12
- require 'database_validations/validations/belongs_to_options'
10
+ require 'database_validations/lib/validators/db_uniqueness_validator'
11
+ require 'database_validations/lib/validators/db_presence_validator'
13
12
 
14
- require 'database_validations/validations/valid_without_database_validations'
15
- require 'database_validations/validations/options_storage'
16
- require 'database_validations/validations/errors'
17
- require 'database_validations/validations/helpers'
18
- require 'database_validations/validations/adapters'
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'
19
+ require 'database_validations/lib/errors'
20
+ require 'database_validations/lib/rescuer'
21
+ require 'database_validations/lib/injector'
22
+ require 'database_validations/lib/adapters'
19
23
 
20
24
  module DatabaseValidations
21
25
  extend ActiveSupport::Concern
@@ -0,0 +1,20 @@
1
+ require 'database_validations/lib/adapters/base_adapter'
2
+ require 'database_validations/lib/adapters/sqlite_adapter'
3
+ require 'database_validations/lib/adapters/postgresql_adapter'
4
+ require 'database_validations/lib/adapters/mysql_adapter'
5
+
6
+ module DatabaseValidations
7
+ module Adapters
8
+ module_function
9
+
10
+ def factory(model)
11
+ case (database = model.connection_config[:adapter].downcase.to_sym)
12
+ when SqliteAdapter::ADAPTER then SqliteAdapter
13
+ when PostgresqlAdapter::ADAPTER then PostgresqlAdapter
14
+ when MysqlAdapter::ADAPTER then MysqlAdapter
15
+ else
16
+ raise Errors::UnknownDatabase, database
17
+ end
18
+ end
19
+ end
20
+ end
@@ -28,23 +28,7 @@ module DatabaseValidations
28
28
  end
29
29
 
30
30
  def find_foreign_key_by_column(column)
31
- model.connection.foreign_key_exists?(model.table_name, column: column)
32
- end
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
31
+ foreign_keys.find { |foreign_key| foreign_key.column.to_s == column.to_s }
48
32
  end
49
33
 
50
34
  # @return [String]
@@ -0,0 +1,19 @@
1
+ module DatabaseValidations
2
+ module Adapters
3
+ class MysqlAdapter < BaseAdapter
4
+ ADAPTER = :mysql2
5
+
6
+ class << self
7
+ def unique_index_name(error_message)
8
+ error_message[/key '([^']+)'/, 1]&.split('.')&.last
9
+ end
10
+
11
+ def unique_error_columns(_error_message); end
12
+
13
+ def foreign_key_error_column(error_message)
14
+ error_message[/FOREIGN KEY \(`([^`]+)`\)/, 1]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module DatabaseValidations
2
+ module Adapters
3
+ class PostgresqlAdapter < BaseAdapter
4
+ ADAPTER = :postgresql
5
+
6
+ class << self
7
+ def unique_index_name(error_message)
8
+ error_message[/unique constraint "([^"]+)"/, 1]
9
+ end
10
+
11
+ def unique_error_columns(error_message)
12
+ error_message[/Key \((.+)\)=/, 1].split(', ')
13
+ end
14
+
15
+ def foreign_key_error_column(error_message)
16
+ error_message[/Key \(([^)]+)\)/, 1]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module DatabaseValidations
2
+ module Adapters
3
+ class SqliteAdapter < BaseAdapter
4
+ ADAPTER = :sqlite3
5
+
6
+ class << self
7
+ def unique_index_name(_error_message); end
8
+
9
+ def unique_error_columns(error_message)
10
+ error_message.scan(/\w+\.([^,:]+)/).flatten
11
+ end
12
+
13
+ def foreign_key_error_column(error_message)
14
+ error_message[/\("([^"]+)"\) VALUES/, 1]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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
 
@@ -59,7 +48,7 @@ module DatabaseValidations
59
48
  @column = column
60
49
  @foreign_keys = foreign_keys
61
50
 
62
- super "No foreign key found with column: \"#{column}\". Founded foreign keys are: #{foreign_keys}. " + env_message
51
+ super "No foreign key found with column: \"#{column}\". Found foreign keys are: #{foreign_keys}. " + env_message
63
52
  end
64
53
  end
65
54
 
@@ -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
@@ -0,0 +1,36 @@
1
+ module DatabaseValidations
2
+ module Rescuer
3
+ module_function
4
+
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
15
+ end
16
+
17
+ def process(instance, error, key_types)
18
+ adapter = Adapters.factory(instance.class)
19
+
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
+ if attribute_validator
28
+ attribute_validator.validator.apply_error(instance, attribute_validator.attribute)
29
+ return true
30
+ end
31
+ end
32
+
33
+ false
34
+ end
35
+ end
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