1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
|
# frozen-string-literal: true
module Sequel
module Plugins
# The auto_validations plugin automatically sets up the following types of validations
# for your model columns:
#
# 1. type validations for all columns
# 2. not_null validations on NOT NULL columns (optionally, presence validations)
# 3. unique validations on columns or sets of columns with unique indexes
# 4. max length validations on string columns
# 5. no null byte validations on string columns
# 6. minimum and maximum values on columns
#
# To determine the columns to use for the type/not_null/max_length/no_null_byte/max_value/min_value validations,
# the plugin looks at the database schema for the model's table. To determine
# the unique validations, Sequel looks at the indexes on the table. In order
# for this plugin to be fully functional, the underlying database adapter needs
# to support both schema and index parsing. Additionally, unique validations are
# only added for models that select from a simple table, they are not added for models
# that select from a subquery.
#
# This plugin uses the validation_helpers plugin underneath to implement the
# validations. It does not allow for any per-column validation message
# customization, but you can alter the messages for the given type of validation
# on a per-model basis (see the validation_helpers documentation).
#
# You can skip certain types of validations from being automatically added via:
#
# Model.skip_auto_validations(:not_null)
#
# If you want to skip all auto validations (only useful if loading the plugin
# in a superclass):
#
# Model.skip_auto_validations(:all)
#
# It is possible to skip auto validations on a per-model-instance basis via:
#
# instance.skip_auto_validations(:unique, :not_null) do
# puts instance.valid?
# end
#
# By default, the plugin uses a not_null validation for NOT NULL columns, but that
# can be changed to a presence validation using an option:
#
# Model.plugin :auto_validations, not_null: :presence
#
# This is useful if you want to enforce that NOT NULL string columns do not
# allow empty values.
#
# You can also supply hashes to pass options through to the underlying validators:
#
# Model.plugin :auto_validations, unique_opts: {only_if_modified: true}
#
# This works for unique_opts, max_length_opts, schema_types_opts, max_value_opts, min_value_opts, no_null_byte_opts,
# explicit_not_null_opts, and not_null_opts.
#
# If you only want auto_validations to add validations to columns that do not already
# have an error associated with them, you can use the skip_invalid option:
#
# Model.plugin :auto_validations, skip_invalid: true
#
# Usage:
#
# # Make all model subclass use auto validations (called before loading subclasses)
# Sequel::Model.plugin :auto_validations
#
# # Make the Album class use auto validations
# Album.plugin :auto_validations
module AutoValidations
NOT_NULL_OPTIONS = {:from=>:values}.freeze
EXPLICIT_NOT_NULL_OPTIONS = {:from=>:values, :allow_missing=>true}.freeze
MAX_LENGTH_OPTIONS = {:from=>:values, :allow_nil=>true}.freeze
SCHEMA_TYPES_OPTIONS = NOT_NULL_OPTIONS
UNIQUE_OPTIONS = NOT_NULL_OPTIONS
NO_NULL_BYTE_OPTIONS = MAX_LENGTH_OPTIONS
MAX_VALUE_OPTIONS = {:from=>:values, :allow_nil=>true, :skip_invalid=>true}.freeze
MIN_VALUE_OPTIONS = MAX_VALUE_OPTIONS
AUTO_VALIDATE_OPTIONS = {
:no_null_byte=>NO_NULL_BYTE_OPTIONS,
:not_null=>NOT_NULL_OPTIONS,
:explicit_not_null=>EXPLICIT_NOT_NULL_OPTIONS,
:max_length=>MAX_LENGTH_OPTIONS,
:max_value=>MAX_VALUE_OPTIONS,
:min_value=>MIN_VALUE_OPTIONS,
:schema_types=>SCHEMA_TYPES_OPTIONS,
:unique=>UNIQUE_OPTIONS
}.freeze
EMPTY_ARRAY = [].freeze
def self.apply(model, opts=OPTS)
model.instance_exec do
plugin :validation_helpers
@auto_validate_presence = false
@auto_validate_no_null_byte_columns = []
@auto_validate_not_null_columns = []
@auto_validate_explicit_not_null_columns = []
@auto_validate_max_length_columns = []
@auto_validate_max_value_columns = []
@auto_validate_min_value_columns = []
@auto_validate_unique_columns = []
@auto_validate_types = true
@auto_validate_options = AUTO_VALIDATE_OPTIONS
end
end
# Setup auto validations for the model if it has a dataset.
def self.configure(model, opts=OPTS)
model.instance_exec do
setup_auto_validations if @dataset
if opts[:not_null] == :presence
@auto_validate_presence = true
end
h = @auto_validate_options.dup
[:not_null, :explicit_not_null, :max_length, :max_value, :min_value, :no_null_byte, :schema_types, :unique].each do |type|
if type_opts = opts[:"#{type}_opts"]
h[type] = h[type].merge(type_opts).freeze
end
end
if opts[:skip_invalid]
[:not_null, :explicit_not_null, :no_null_byte, :max_length, :schema_types].each do |type|
h[type] = h[type].merge(:skip_invalid=>true).freeze
end
end
@auto_validate_options = h.freeze
end
end
module ClassMethods
# The columns with automatic no_null_byte validations
attr_reader :auto_validate_no_null_byte_columns
# The columns with automatic not_null validations
attr_reader :auto_validate_not_null_columns
# The columns with automatic not_null validations for columns present in the values.
attr_reader :auto_validate_explicit_not_null_columns
# The columns or sets of columns with automatic max_length validations, as an array of
# pairs, with the first entry being the column name and second entry being the maximum length.
attr_reader :auto_validate_max_length_columns
# The columns with automatch max value validations, as an array of
# pairs, with the first entry being the column name and second entry being the maximum value.
attr_reader :auto_validate_max_value_columns
# The columns with automatch min value validations, as an array of
# pairs, with the first entry being the column name and second entry being the minimum value.
attr_reader :auto_validate_min_value_columns
# The columns or sets of columns with automatic unique validations
attr_reader :auto_validate_unique_columns
# Inherited options
attr_reader :auto_validate_options
Plugins.inherited_instance_variables(self,
:@auto_validate_presence=>nil,
:@auto_validate_types=>nil,
:@auto_validate_no_null_byte_columns=>:dup,
:@auto_validate_not_null_columns=>:dup,
:@auto_validate_explicit_not_null_columns=>:dup,
:@auto_validate_max_length_columns=>:dup,
:@auto_validate_max_value_columns=>:dup,
:@auto_validate_min_value_columns=>:dup,
:@auto_validate_unique_columns=>:dup,
:@auto_validate_options => :dup)
Plugins.after_set_dataset(self, :setup_auto_validations)
# Whether to use a presence validation for not null columns
def auto_validate_presence?
@auto_validate_presence
end
# Whether to automatically validate schema types for all columns
def auto_validate_types?
@auto_validate_types
end
# Freeze auto_validation settings when freezing model class.
def freeze
@auto_validate_no_null_byte_columns.freeze
@auto_validate_not_null_columns.freeze
@auto_validate_explicit_not_null_columns.freeze
@auto_validate_max_length_columns.freeze
@auto_validate_max_value_columns.freeze
@auto_validate_min_value_columns.freeze
@auto_validate_unique_columns.freeze
super
end
# Skip automatic validations for the given validation type
# (:not_null, :no_null_byte, :types, :unique, :max_length, :max_value, :min_value).
# If :all is given as the type, skip all auto validations.
#
# Skipping types validation automatically skips max_value and min_value validations,
# since those validations require valid types.
def skip_auto_validations(type)
case type
when :all
[:not_null, :no_null_byte, :types, :unique, :max_length, :max_value, :min_value].each{|v| skip_auto_validations(v)}
when :not_null
auto_validate_not_null_columns.clear
auto_validate_explicit_not_null_columns.clear
when :types
@auto_validate_types = false
else
public_send("auto_validate_#{type}_columns").clear
end
end
private
# Parse the database schema and indexes and record the columns to automatically validate.
def setup_auto_validations
not_null_cols, explicit_not_null_cols = db_schema.select{|col, sch| sch[:allow_null] == false}.partition{|col, sch| sch[:default].nil?}.map{|cs| cs.map{|col, sch| col}}
@auto_validate_not_null_columns = not_null_cols - Array(primary_key)
explicit_not_null_cols += Array(primary_key)
@auto_validate_explicit_not_null_columns = explicit_not_null_cols.uniq
@auto_validate_max_length_columns = db_schema.select{|col, sch| sch[:type] == :string && sch[:max_length].is_a?(Integer)}.map{|col, sch| [col, sch[:max_length]]}
@auto_validate_max_value_columns = db_schema.select{|col, sch| sch[:max_value]}.map{|col, sch| [col, sch[:max_value]]}
@auto_validate_min_value_columns = db_schema.select{|col, sch| sch[:min_value]}.map{|col, sch| [col, sch[:min_value]]}
@auto_validate_no_null_byte_columns = db_schema.select{|_, sch| sch[:type] == :string}.map{|col, _| col}
table = dataset.first_source_table
@auto_validate_unique_columns = if db.supports_index_parsing? && [Symbol, SQL::QualifiedIdentifier, SQL::Identifier, String].any?{|c| table.is_a?(c)}
db.indexes(table).select{|name, idx| idx[:unique] == true}.map{|name, idx| idx[:columns].length == 1 ? idx[:columns].first : idx[:columns]}
else
[]
end
end
end
module InstanceMethods
# Skip the given types of auto validations on this instance inside the block.
def skip_auto_validations(*types)
types << :all if types.empty?
@_skip_auto_validations = types
yield
ensure
@_skip_auto_validations = nil
end
# Validate the model's auto validations columns
def validate
super
skip = @_skip_auto_validations || EMPTY_ARRAY
return if skip.include?(:all)
opts = model.auto_validate_options
unless skip.include?(:no_null_byte) || (no_null_byte_columns = model.auto_validate_no_null_byte_columns).empty?
validates_no_null_byte(no_null_byte_columns, opts[:no_null_byte])
end
unless skip.include?(:not_null)
not_null_method = model.auto_validate_presence? ? :validates_presence : :validates_not_null
unless (not_null_columns = model.auto_validate_not_null_columns).empty?
public_send(not_null_method, not_null_columns, opts[:not_null])
end
unless (not_null_columns = model.auto_validate_explicit_not_null_columns).empty?
public_send(not_null_method, not_null_columns, opts[:explicit_not_null])
end
end
unless skip.include?(:max_length) || (max_length_columns = model.auto_validate_max_length_columns).empty?
max_length_columns.each do |col, len|
validates_max_length(len, col, opts[:max_length])
end
end
unless skip.include?(:types) || !model.auto_validate_types?
validates_schema_types(keys, opts[:schema_types])
unless skip.include?(:max_value) || ((max_value_columns = model.auto_validate_max_value_columns).empty?)
max_value_columns.each do |col, max|
validates_max_value(max, col, opts[:max_value])
end
end
unless skip.include?(:min_value) || ((min_value_columns = model.auto_validate_min_value_columns).empty?)
min_value_columns.each do |col, min|
validates_min_value(min, col, opts[:min_value])
end
end
end
unless skip.include?(:unique)
unique_opts = Hash[opts[:unique]]
if model.respond_to?(:sti_dataset)
unique_opts[:dataset] = model.sti_dataset
end
model.auto_validate_unique_columns.each{|cols| validates_unique(cols, unique_opts)}
end
end
end
end
end
end
|