database_validations 0.8.5 → 0.9.1
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.
- 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
|