Skip to content

Instantly share code, notes, and snippets.

@gamecreature
Last active November 3, 2020 11:00
Show Gist options
  • Save gamecreature/a5f17ad8da409e06f7dcdea2e8f6306e to your computer and use it in GitHub Desktop.
Save gamecreature/a5f17ad8da409e06f7dcdea2e8f6306e to your computer and use it in GitHub Desktop.
Schema_validations concern for rails, replacement for the schema_validations gem (Compatible with Rails 6.1)
# Schema Validations Concern - simple basic replacement for https://github.com/SchemaPlus/schema_validations
#
# Warning, uniqueness validators are NOT Added
#
# Constraints:
# - null: false => validates ... presence: true
# - limit: 100 => validates ... length: { maximum: 100 }
#
# Data types:
# - :boolean => :validates ... inclusion: { in: [true, false] }
# - :float => :validates ... numericality: true
# - :integer => :validates ... numericality: { only_integer: true, greater_than_or_equal_to: ..., less_than: ... }
# - :decimal, precision: ... => :validates ... numericality: { greater_than: ..., less_than: ... }
module SchemaValidations
extend ActiveSupport::Concern
class Config < OpenStruct
def initialize
super(
auto_create: true,
log_generated_validations: true,
whitelist: %i[created_at updated_at created_on updated_on],
except: []
)
end
def set(options)
options.each { |k, v| self[k] = v }
end
end
def self.config
@config ||= SchemaValidations::Config.new
end
def self.setup
yield config
end
class AutoValidatorBuilder
attr_reader :model, :config
def initialize(model, config)
@config = config
@model = model
end
## Column validators
def converted_data_type_for_column(column)
return :enum if model.respond_to?(:defined_enums) && model.defined_enums.key?(column.name)
{ integer: :integer,
decimal: :decimal,
money: :decimal,
float: :numeric,
text: :text,
string: :text,
boolean: :boolean }[column.type] || column.type
end
def create_validator_for_column(column)
datatype = converted_data_type_for_column(column)
# create datatype validator
case datatype
when :integer then create_integer_validation(column)
when :decimal then create_decimal_validation(column)
when :numeric then create_validator_logged(column.name, numericality: { allow_blank: true })
when :text
create_validator_logged(column.name, length: { maximum: column.limit, allow_blank: true }) if column.limit
when :datetime, :boolean
else
Rails.logger.warn "** [schema validations] unkown data type: #{datatype}"
end
# create not null validator
create_not_null_validators_for_column(column)
end
def create_not_null_validators_for_column(column)
return if column.null || column.auto_increment?
if column.type == :boolean
create_validator_logged(column.name, inclusion: { in: [true, false], message: :blank })
elsif column.has_default?
create_validator_logged(column.name, presence: true)
else
create_validator_logged(column.name, not_nil: true)
end
end
def create_integer_validation(column)
# returns a ActiveModel::Type::Integer instantce
type = model.connection.lookup_cast_type_from_column(column)
args = {
allow_blank: true,
only_integer: true,
greater_than_or_equal_to: type.send(:min_value),
less_than: type.send(:max_value)
}
create_validator_logged(column.name, numericality: args)
end
def create_decimal_validation(column)
return unless column.precision
limit = 10**(column.precision - (column.scale || 0))
create_validator_logged(column.name, numericality: { allow_blank: true, greater_than: -limit, less_than: limit })
end
## Create Association Validators
def create_association_validations
model.reflect_on_all_associations(:belongs_to).each do |association|
column = model.columns_hash[association.foreign_key]
next unless column
# NOT NULL constraints
create_validator_logged(column.name, presence: true) unless column.null
# UNIQUE constraints
# add_uniqueness_validation(column) if column.unique?
end
end
## Create all validators
def create_validator_logged(name, args)
msg = "[schema_validations] #{model.name}.validate #{name.to_sym}, #{args.inspect}"
Rails.logger&.debug msg if config.log_generated_validations
model.validates name.to_sym, args
end
def create_validators
model.columns.each do |column|
create_validator_for_column(column) if should_create_validator?(column)
end
create_association_validations
end
def should_create_validator?(column)
name = column.name.to_sym
return false if (config.except || []).include?(name)
return false if (config.whitelist || []).include?(name)
true
end
end
included do
class_attribute :schema_validations_loaded
class_attribute :schema_validations_config
before_validation :load_schema_validations unless schema_validations_loaded?
def self.schema_validations_config
@schema_validations_config ||= SchemaValidations.config.dup
end
def self.create_schema_validations?
schema_validations_config.auto_create &&
!(schema_validations_loaded || abstract_class? || name.blank?) && table_exists?
end
def self.load_schema_validations
return unless create_schema_validations?
AutoValidatorBuilder.new(self, schema_validations_config).create_validators
self.schema_validations_loaded = true
end
def self.schema_validations(options)
schema_validations_config.set(options)
end
def self.validators
load_schema_validations unless schema_validations_loaded?
super
end
def self.validators_on(*args)
load_schema_validations unless schema_validations_loaded?
super
end
end
def load_schema_validations
self.class.load_schema_validations
end
end
unless defined?(NotNilValidator)
class NotNilValidator < ActiveModel::EachValidator
def validate_each(record, attr_name, value)
record.errors.add(attr_name, :blank, options) if value.nil?
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment