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.
- checksums.yaml +4 -4
- data/lib/database_validations.rb +14 -10
- data/lib/database_validations/lib/adapters.rb +20 -0
- data/lib/database_validations/{validations → lib}/adapters/base_adapter.rb +1 -17
- data/lib/database_validations/lib/adapters/mysql_adapter.rb +19 -0
- data/lib/database_validations/lib/adapters/postgresql_adapter.rb +21 -0
- data/lib/database_validations/lib/adapters/sqlite_adapter.rb +19 -0
- data/lib/database_validations/lib/attribute_validator.rb +3 -0
- data/lib/database_validations/lib/checkers/db_presence_validator.rb +36 -0
- data/lib/database_validations/lib/checkers/db_uniqueness_validator.rb +47 -0
- data/lib/database_validations/{validations → lib}/errors.rb +1 -12
- data/lib/database_validations/lib/injector.rb +13 -0
- data/lib/database_validations/lib/key_generator.rb +32 -0
- data/lib/database_validations/lib/presence_key_extractor.rb +18 -0
- data/lib/database_validations/lib/rescuer.rb +36 -0
- data/lib/database_validations/lib/storage.rb +28 -0
- data/lib/database_validations/lib/uniqueness_key_extractor.rb +38 -0
- data/lib/database_validations/lib/validations.rb +31 -0
- data/lib/database_validations/lib/validators/db_presence_validator.rb +63 -0
- data/lib/database_validations/lib/validators/db_uniqueness_validator.rb +48 -0
- data/lib/database_validations/rspec/uniqueness_validator_matcher.rb +19 -10
- data/lib/database_validations/version.rb +1 -1
- metadata +52 -41
- data/lib/database_validations/validations/adapters.rb +0 -20
- data/lib/database_validations/validations/adapters/mysql_adapter.rb +0 -20
- data/lib/database_validations/validations/adapters/postgresql_adapter.rb +0 -20
- data/lib/database_validations/validations/adapters/sqlite_adapter.rb +0 -22
- data/lib/database_validations/validations/belongs_to_handlers.rb +0 -58
- data/lib/database_validations/validations/belongs_to_options.rb +0 -44
- data/lib/database_validations/validations/belongs_to_presence_validator.rb +0 -25
- data/lib/database_validations/validations/helpers.rb +0 -69
- data/lib/database_validations/validations/options_storage.rb +0 -34
- data/lib/database_validations/validations/uniqueness_handlers.rb +0 -56
- data/lib/database_validations/validations/uniqueness_options.rb +0 -104
- 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
|