Last active
October 24, 2017 10:53
-
-
Save tyok/d71998e2aae6dbdc36763e0c3a2ed038 to your computer and use it in GitHub Desktop.
Dry Validation + ActiveModel::Errors + Class-based approach
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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