@jviney there’s a huge problem with such a solution - Dry::Struct
errors out on invalid input, which means that if an attribute from @schema.values
is missing, this line
@form = PersonStruct.new(@schema.values)
will error out. I’ve tried this route myself as well, but the error throwing is quite a problem. The way I did it at the end is to use a regular ruby class with attr_accessor
attribute list, but it’s far from ideal.
This is the full implementation of my “form”
require "dry/monads/all"
class ApplicationForm
# Required dependency for ActiveModel::Errors
extend ActiveModel::Naming
extend ActiveModel::Translation
attr_reader :errors
def initialize(attributes = {})
assign_attributes(attributes) if attributes
@errors = ActiveModel::Errors.new(self)
end
def assign_attributes(attributes)
attributes.each do |key, value|
setter = :"#{key}="
public_send(setter, value) if respond_to?(setter)
end
end
# The following methods are needed to be minimally implemented
def read_attribute_for_validation(attr)
send(attr)
end
class << self
include Dry::Monads
def contract(&block)
@contract = Dry::Validation::Contract.build(&block)
end
def validate(params)
if @contract.nil?
raise NotImplementedError, <<~STR.strip
.contract not defined on form. Must be a Dry::Validation::Contract. Example:
contract do
params do
required(:amount).filled(:integer)
end
end
STR
end
outcome = @contract.call(params)
form = new(outcome.to_h)
if outcome.failure?
assign_errors_to_form(outcome, form)
Failure([:validate, form])
else
Success(form)
end
end
private
def assign_errors_to_form(outcome, form)
msg = build_error_message(outcome.errors)
form.errors.add(:base, msg)
outcome.errors.each do |validation_message|
form.errors.add(validation_message.path[0], validation_message.text, validation_message.meta)
end
end
def build_error_message(errors)
if errors.any?
errors.to_h.map { |k, v| "#{k.to_s.humanize}: #{v.first}" }.join("\n")
else
# Honeybadger.notify(:validation_error, context: errors)
"Something went wrong, please refresh the page and try again!"
end
end
end
end
an example of using it:
class GoalForm < ApplicationForm
attr_accessor :amount, :title, :plan_id, :preset_id
contract do
params do
required(:plan_id).filled(:string)
required(:preset_id).filled(:string)
required(:amount).filled(:integer)
end
end
end
GoalForm.validate(params)
I asked pretty much a similar question a while back:
There’s now an extension, which creates a Validation Schema out of a struct, but not the other way around yet:
I think the lack of good form integration has me rather look back at using Rails for data validation. One way I want to avoid placing validations directly in the model would be subclass the model, and define all validations in the child class. I haven’t tried it yet, but it looks promising.
So, if I have a model
class Goal < ApplicationRecord
belongs_to :plan
# attrs: amount, ...
end
class CreateGoal < Goal
table_name :goals # or somehow get that from the parent class
validates_presence_of :amount
validates_numericality_of :amount
# .. and so on
end