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
@@ -1,22 +0,0 @@
1
- module DatabaseValidations
2
- module Adapters
3
- class SqliteAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message if unless].freeze
5
- ADAPTER = :sqlite3
6
-
7
- def index_name(_error_message); end
8
-
9
- def find_foreign_key_by_column(column)
10
- foreign_keys.find { |foreign_key| foreign_key.column.to_s == column.to_s }
11
- end
12
-
13
- def unique_error_columns(error_message)
14
- error_message.scan(/#{model.table_name}\.([^,:]+)/).flatten
15
- end
16
-
17
- def foreign_key_error_column(error_message)
18
- error_message[/\("([^"]+)"\) VALUES/, 1]
19
- end
20
- end
21
- end
22
- end
@@ -1,58 +0,0 @@
1
- module DatabaseValidations
2
- module BelongsToHandlers
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- alias_method :validate, :valid?
7
- end
8
-
9
- def valid?(context = nil)
10
- output = super(context)
11
-
12
- Helpers.each_belongs_to_presence_validator(self.class) do |validator|
13
- next if validator.column_and_relation_blank_for?(self)
14
-
15
- validates_with(ActiveRecord::Validations::PresenceValidator, validator.validates_presence_options)
16
- end
17
-
18
- errors.empty? && output
19
- end
20
-
21
- def save(opts = {})
22
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
23
- rescue ActiveRecord::InvalidForeignKey => e
24
- Helpers.handle_foreign_key_error!(self, e)
25
- false
26
- end
27
-
28
- def save!(opts = {})
29
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
30
- rescue ActiveRecord::InvalidForeignKey => e
31
- Helpers.handle_foreign_key_error!(self, e)
32
- raise ActiveRecord::RecordInvalid, self
33
- end
34
-
35
- private
36
-
37
- def perform_validations(options = {})
38
- options[:validate] == false || valid_without_database_validations?(options[:context])
39
- end
40
- end
41
-
42
- module ClassMethods
43
- def db_belongs_to(name, scope = nil, **options)
44
- include(DatabaseValidations::ValidWithoutDatabaseValidations)
45
- @database_validations_opts ||= DatabaseValidations::OptionsStorage.new(self)
46
-
47
- belongs_to(name, scope, options.merge(optional: true))
48
-
49
- foreign_key = reflections[name.to_s].foreign_key
50
-
51
- validates_with DatabaseValidations::Validations::BelongsToPresenceValidator, column: foreign_key, relation: name
52
-
53
- @database_validations_opts.push_belongs_to(foreign_key, name)
54
-
55
- include(DatabaseValidations::BelongsToHandlers)
56
- end
57
- end
58
- end
@@ -1,44 +0,0 @@
1
- module DatabaseValidations
2
- class BelongsToOptions
3
- attr_reader :column, :adapter, :relation
4
-
5
- def initialize(column, relation, adapter)
6
- @column = column
7
- @relation = relation
8
- @adapter = adapter
9
-
10
- raise_if_unsupported_database!
11
- raise_if_foreign_key_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
12
- end
13
-
14
- # @return [String]
15
- def key
16
- Helpers.generate_key_for_belongs_to(column)
17
- end
18
-
19
- # @return [Boolean]
20
- def column_and_relation_blank_for?(instance)
21
- instance.public_send(column).blank? && instance.public_send(relation).blank?
22
- end
23
-
24
- def handle_foreign_key_error(instance)
25
- # Hack to not query the database because we know the result already
26
- instance.send("#{relation}=", nil)
27
- instance.errors.add(relation, :blank, message: :required)
28
- end
29
-
30
- def validates_presence_options
31
- { attributes: relation, message: :required }
32
- end
33
-
34
- private
35
-
36
- def raise_if_foreign_key_missed!
37
- raise Errors::ForeignKeyNotFound.new(column, adapter.foreign_keys) unless adapter.find_foreign_key_by_column(column)
38
- end
39
-
40
- def raise_if_unsupported_database!
41
- raise Errors::UnsupportedDatabase.new(:db_belongs_to, adapter.adapter_name) if adapter.adapter_name == :sqlite3
42
- end
43
- end
44
- end
@@ -1,25 +0,0 @@
1
- module DatabaseValidations
2
- module Validations
3
- class BelongsToPresenceValidator < ActiveModel::Validator
4
- attr_reader :attributes
5
-
6
- # This is a hack to simulate presence validator
7
- # It's used for cases when some 3rd parties are relies on the validators
8
- # For example, required option from simple_form checks the validator
9
- def self.kind
10
- :presence
11
- end
12
-
13
- def initialize(options = {})
14
- @attributes = [options[:relation]]
15
- super
16
- end
17
-
18
- def validate(record)
19
- return unless record.public_send(options[:column]).blank? && record.public_send(options[:relation]).blank?
20
-
21
- record.errors.add(options[:relation], :blank, message: :required)
22
- end
23
- end
24
- end
25
- end
@@ -1,69 +0,0 @@
1
- module DatabaseValidations
2
- module Helpers
3
- module_function
4
-
5
- def handle_unique_error!(instance, error) # rubocop:disable Metrics/AbcSize
6
- adapter = Adapters.factory(instance.class)
7
- index_key = generate_key_for_uniqueness_index(adapter.index_name(error.message))
8
- column_key = generate_key_for_uniqueness(adapter.unique_error_columns(error.message))
9
-
10
- each_options_storage(instance.class) do |storage|
11
- return storage[index_key].handle_unique_error(instance) if storage[index_key]
12
- return storage[column_key].handle_unique_error(instance) if storage[column_key]
13
- end
14
-
15
- raise error
16
- end
17
-
18
- def handle_foreign_key_error!(instance, error)
19
- adapter = Adapters.factory(instance.class)
20
- column_key = generate_key_for_belongs_to(adapter.foreign_key_error_column(error.message))
21
-
22
- each_options_storage(instance.class) do |storage|
23
- return storage[column_key].handle_foreign_key_error(instance) if storage[column_key]
24
- end
25
-
26
- raise error
27
- end
28
-
29
- def each_options_storage(klass)
30
- while klass.respond_to?(:validates_db_uniqueness_of)
31
- storage = klass.instance_variable_get(:'@database_validations_opts')
32
- yield(storage) if storage
33
- klass = klass.superclass
34
- end
35
- end
36
-
37
- def each_uniqueness_validator(klass)
38
- each_options_storage(klass) do |storage|
39
- storage.each_uniqueness_validator { |validator| yield(validator) }
40
- end
41
- end
42
-
43
- def each_belongs_to_presence_validator(klass)
44
- each_options_storage(klass) do |storage|
45
- storage.each_belongs_to_presence_validator { |validator| yield(validator) }
46
- end
47
- end
48
-
49
- def unify_columns(*columns)
50
- columns.flatten.compact.map(&:to_s).sort
51
- end
52
-
53
- def generate_key_for_uniqueness_index(index_name)
54
- generate_key(:uniqueness_index, index_name)
55
- end
56
-
57
- def generate_key_for_uniqueness(*columns)
58
- generate_key(:uniqueness, columns)
59
- end
60
-
61
- def generate_key_for_belongs_to(column)
62
- generate_key(:belongs_to, column)
63
- end
64
-
65
- def generate_key(type, *columns)
66
- [type, *unify_columns(columns)].join('__')
67
- end
68
- end
69
- end
@@ -1,34 +0,0 @@
1
- module DatabaseValidations
2
- class OptionsStorage
3
- def initialize(klass)
4
- @adapter = Adapters.factory(klass)
5
- @storage = {}
6
- end
7
-
8
- def push_uniqueness(field, options)
9
- uniqueness_options = UniquenessOptions.new(field, options, adapter)
10
- storage[uniqueness_options.key] = uniqueness_options
11
- end
12
-
13
- def push_belongs_to(field, relation)
14
- belongs_to_options = BelongsToOptions.new(field, relation, adapter)
15
- storage[belongs_to_options.key] = belongs_to_options
16
- end
17
-
18
- def [](key)
19
- storage[key]
20
- end
21
-
22
- def each_uniqueness_validator
23
- storage.values.grep(UniquenessOptions).each { |validator| yield(validator) }
24
- end
25
-
26
- def each_belongs_to_presence_validator
27
- storage.values.grep(BelongsToOptions).each { |validator| yield(validator) }
28
- end
29
-
30
- private
31
-
32
- attr_reader :storage, :adapter
33
- end
34
- end
@@ -1,56 +0,0 @@
1
- module DatabaseValidations
2
- module UniquenessHandlers
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- alias_method :validate, :valid?
7
- end
8
-
9
- def valid?(context = nil)
10
- output = super(context)
11
-
12
- Helpers.each_uniqueness_validator(self.class) do |validator|
13
- if validator.if_and_unless_pass?(self)
14
- validates_with(ActiveRecord::Validations::UniquenessValidator, validator.validates_uniqueness_options)
15
- end
16
- end
17
-
18
- errors.empty? && output
19
- end
20
-
21
- def save(options = {})
22
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
23
- rescue ActiveRecord::RecordNotUnique => e
24
- Helpers.handle_unique_error!(self, e)
25
- false
26
- end
27
-
28
- def save!(options = {})
29
- ActiveRecord::Base.connection.transaction(requires_new: true) { super }
30
- rescue ActiveRecord::RecordNotUnique => e
31
- Helpers.handle_unique_error!(self, e)
32
- raise ActiveRecord::RecordInvalid, self
33
- end
34
-
35
- private
36
-
37
- def perform_validations(options = {})
38
- options[:validate] == false || valid_without_database_validations?(options[:context])
39
- end
40
- end
41
-
42
- module ClassMethods
43
- def validates_db_uniqueness_of(*attributes)
44
- include(DatabaseValidations::ValidWithoutDatabaseValidations)
45
- @database_validations_opts ||= DatabaseValidations::OptionsStorage.new(self)
46
-
47
- options = attributes.extract_options!
48
-
49
- attributes.each do |attribute|
50
- @database_validations_opts.push_uniqueness(attribute, options.merge(attributes: attribute))
51
- end
52
-
53
- include(DatabaseValidations::UniquenessHandlers)
54
- end
55
- end
56
- end
@@ -1,104 +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
- attr_reader :field
7
-
8
- def initialize(field, options, adapter)
9
- @field = field
10
- @options = options
11
- @adapter = adapter
12
-
13
- raise_if_unsupported_options!
14
- raise_if_index_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
15
- end
16
-
17
- def handle_unique_error(instance)
18
- error_options = options.except(:case_sensitive, :scope, :conditions, :attributes, *CUSTOM_OPTIONS)
19
- error_options[:value] = instance.public_send(options[:attributes])
20
-
21
- instance.errors.add(options[:attributes], :taken, error_options)
22
- end
23
-
24
- # @return [Hash<Symbol, Object>]
25
- def validates_uniqueness_options
26
- where_clause_str = where_clause
27
-
28
- DEFAULT_OPTIONS
29
- .merge(options)
30
- .except(*CUSTOM_OPTIONS)
31
- .tap { |opts| opts[:conditions] = -> { where(where_clause_str) } if where_clause }
32
- end
33
-
34
- # @return [Boolean]
35
- def if_and_unless_pass?(instance)
36
- (options[:if].nil? || condition_passes?(options[:if], instance)) &&
37
- (options[:unless].nil? || !condition_passes?(options[:unless], instance))
38
- end
39
-
40
- # @return [String]
41
- def key
42
- @key ||= index_name ? Helpers.generate_key_for_uniqueness_index(index_name) : Helpers.generate_key_for_uniqueness(columns)
43
- end
44
-
45
- # @return [Array<String>]
46
- def columns
47
- @columns ||= Helpers.unify_columns(field, scope)
48
- end
49
-
50
- # @return [String|nil]
51
- def where_clause
52
- @where_clause ||= options[:where]
53
- end
54
-
55
- # @return [String|nil]
56
- def message
57
- @message ||= options[:message]
58
- end
59
-
60
- # @return [Array<String|Symbol>]
61
- def scope
62
- @scope ||= Array.wrap(options[:scope])
63
- end
64
-
65
- # @return [String|Symbol|nil]
66
- def index_name
67
- @index_name ||= options[:index_name]
68
- end
69
-
70
- # @return [Boolean|nil]
71
- def case_sensitive
72
- @case_sensitive ||= options[:case_sensitive]
73
- end
74
-
75
- private
76
-
77
- attr_reader :adapter, :options
78
-
79
- def condition_passes?(condition, instance)
80
- if condition.is_a?(Symbol)
81
- instance.__send__(condition)
82
- elsif condition.is_a?(Proc) && condition.arity.zero?
83
- instance.instance_exec(&condition)
84
- else
85
- instance.instance_eval(&condition)
86
- end
87
- end
88
-
89
- def raise_if_unsupported_options!
90
- options.except(:attributes).each_key do |option|
91
- unless adapter.support_option?(option)
92
- raise Errors::OptionIsNotSupported.new(option, adapter.adapter_name, adapter.supported_options)
93
- end
94
- end
95
- end
96
-
97
- def raise_if_index_missed! # rubocop:disable Metrics/AbcSize
98
- unless (index_name && adapter.find_index_by_name(index_name.to_s)) ||
99
- (!index_name && adapter.find_index(columns, where_clause))
100
- raise Errors::IndexNotFound.new(columns, where_clause, index_name, adapter.indexes, adapter.table_name)
101
- end
102
- end
103
- end
104
- end
@@ -1,9 +0,0 @@
1
- module DatabaseValidations
2
- module ValidWithoutDatabaseValidations
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- alias_method :valid_without_database_validations?, :valid?
7
- end
8
- end
9
- end