Skip to content

Instantly share code, notes, and snippets.

@tyok
Last active October 24, 2017 10:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tyok/d71998e2aae6dbdc36763e0c3a2ed038 to your computer and use it in GitHub Desktop.
Save tyok/d71998e2aae6dbdc36763e0c3a2ed038 to your computer and use it in GitHub Desktop.
Dry Validation + ActiveModel::Errors + Class-based approach
module MagicalValidation
module Model
extend ActiveSupport::Concern
delegate :errors, :hints, :messages, to: :@result, prefix: :nested
attr_reader :result
def initialize(hash)
@result = self.class.schema.call(hash)
end
def [](key)
@result[key]
end
def output
@result.output
end
def valid?
@result.success?
end
def errors
@errors ||= ActiveModelErrors.(self, @result.errors)
end
def read_attribute_for_validation(attr)
self[attr]
end
included do
extend ActiveModel::Naming
extend ActiveModel::Translation
@_v_mutex = ::Mutex.new
end
module ClassMethods
def inherited(base)
super
dsl = Setup.dsl(Class.new(@dsl.schema_class))
base.instance_variable_set("@dsl", dsl)
base.instance_variable_set("@_v_mutex", ::Mutex.new)
end
def configure(*args, &block)
apply_dsl(:configure, *args, &block)
end
def each(*args, &block)
apply_dsl(:each, *args, &block)
end
def required(*args, &block)
apply_dsl(:required, *args, &block)
end
def optional(*args, &block)
apply_dsl(:optional, *args, &block)
end
def rule(*args, &block)
apply_dsl(:rule, *args, &block)
end
def validate(*args, &block)
apply_dsl(:validate, *args, &block)
end
def schema
return @schema if @schema
@_v_mutex.synchronize do
return @schema if @schema
# note: maybe I should keep the original schema_class
# and get .config from there
@schema = Setup.configured_schema(@dsl, @dsl.schema_class.config).new
end
@schema
end
private def apply_dsl(name, *args, &block)
fail "Schema has already been defined!" if @schema
fail "Cannot modify the base class, please call from subclass!" if !@dsl
@dsl.__send__(name, *args, &block)
end
end
end
module ActiveModelErrors
class << self
def call(base, dry_messages)
ActiveModel::Errors.new(base).tap do |e|
e.instance_variable_set("@messages", messages(dry_messages))
end
end
def messages(dry_messages)
flatten_message_hash(dry_messages).reduce({}) { |h, m| h.merge!(m) }
end
private def flatten_message_hash(dry_messages, crumbs = [])
case dry_messages
when Hash
dry_messages.flat_map { |k, v| flatten_message_hash(v, crumbs + [k]) }
else
[ flatten_key_crumbs(crumbs) => dry_messages ]
end
end
private def flatten_key_crumbs(crumbs)
crumbs.map(&:to_s).join(".").to_sym
end
end
end
module Setup
include Dry::Validation
def self.dsl(source)
options = { registry: source.registry, schema_class: source }
dsl = Schema::Value.new(options)
dsl_ext = source.config.dsl_extensions
dsl_ext.__send__(:extend_object, dsl) if dsl_ext
dsl
end
def self.configured_schema(dsl, config)
target = dsl.schema_class
if config.input
config.input_rule = -> predicates {
Schema::Value
.new(registry: predicates)
.infer_predicates(Array(target.config.input))
.to_ast
}
end
target.configure do |cfg|
cfg.rules = target.config.rules + dsl.rules
cfg.checks = cfg.checks + dsl.checks
cfg.path = dsl.path
cfg.type_map = target.build_type_map(dsl.type_map) if cfg.type_specs
end
target
end
end
module JSON
extend ActiveSupport::Concern
include Model
included { @dsl= Setup.dsl(Class.new(Dry::Validation::Schema::JSON)) }
end
module Form
extend ActiveSupport::Concern
include Model
included { @dsl= Setup.dsl(Class.new(Dry::Validation::Schema::Form)) }
end
end
class ApplicationValidation
include MagicalValidation::JSON
end
class Login < ApplicationValidation
required(:email) { str? }
required(:password) { str? }
end
login = Login.new(email: "haxor", password: 12345)
login.valid? # => false
login.errors # => ActiveModel::Errors
login.nested_errors # => dry messages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment