Given the following params defined based on dry-validation:
class UserParams < Dry::Validation::Contract
params do
required(:email).value(:string)
end
end
If I want to take email param after validations I have only one choice now - it’s transform to Ruby hash and use [] method:
result = UserParams.new.call(email: ‘user@example.com’)
result[:email] # => ‘user@example.com’
But, If I have a type in the :email key, I will get nil, that’s basically not much better than just take params[:email].
I want Ruby validate my mistakes in the keys. For that reason I would like to get a struct with a strict API after passed validations. By struct here I mean an object has #email method, so that when I have a type on it, I have a clear exception immediately about that.
In short, I want the following code work:
result.email # => 'user@example.com'
But I understand that it would have an obscured API, if the validations don’t pass. For that reason there could be another method that does the transformation into struct:
result.to_struct.email # => 'user@example.com'
to_struct would fail if validations don’t pass.
What do you think if dry-validation implement this method?
Maybe, I’m missing something and it’s already possible to do that transition, but I’ve not found it anywhere. If it’s already there, please point me out. Thanks!
I am no expert, but my understanding is that you need to extract your schema from your validation here. You have different abstractions but you currently all stuff them in your dry-validation so you cannot benefit from them:
Your schema
Your validation operation
Your struct (once the data is validated)
So, define your schema independently.
Use it to create a struct type:
class MyStruct < Dry::Struct
schema my_schema.strict
end
Now:
Validate your hash with dry-validation & your schema
Now do: result = MyStruct.new(my_validated_hash)
Basically, you were trying to do 2 things in one step: validating & creating your struct. The doc explains why such an approach should be best avoided and why you should separate the two actions.
Yes, thanks! It’s not clear though, how to define this schema and reuse inside the validation class. So do I understand correctly that:
1.You suggest to define the schema with dry-schema?
2. It’s possible to reuse this schema inside both places (the struct and the validator)?
Humm
Trying to put together a small example, I realized apis don’t seem to handle that well indeed. I’m not familiar enough with the codebase, but it seems Dry::Struct has nothing built-in to accept a Dry::Schema.
Either my understanding of the abstractions are not on point, or the api could be created. Mainly a bridge between the two representations I guess (although type information is not yet required in dry-schema, which would make the Dry::Struct impossible to build in some cases, which suggests the bridge would not be just easy object transformation).
Someone with more experience with dry-rb should be able to confirm that or not
I’ve encountered the same problem when using dry-rb with Rails, and resorted to defining both a Dry::Validation::Contract to process the form params, and a Dry::Struct used when rendering the form.
The structure of the Contract schema and Struct are identical.
I end up with the following pattern in the controller:
def new
@form = PersonStruct.new # includes a #errors method to work with form helpers
end
def create
@schema = PersonContract.new.call(params[:person].permit!)
@form = PersonStruct.new(@schema.values)
@form.errors = @schema.errors.to_h
@form.errors.default_proc = proc { [] }
if @schema.success?
# happy path, perhaps do something with @form or redirect
else
render 'new'
end
end
Having to duplicate the schema on the Contract and the Struct has been one of my main issues using dry-rb with Rails for form processing. I might look into generating the struct automatically.
Could someone familiar with dry-rb internals chime in as to whether this seems like a reasonable use case?
@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
I just stumbled on the dry-rb libraries and it is a little tedious to have to declare the same fields for the struct as well as for the schema. I have a background in Elm so conceptually it makes sense to me that you might have one type for internal representation and another type for converting to/from wire format, but at the same time, the schema doesn’t give you a lot of tools to change the format, for example I don’t even see an easy way to rename a field (except for transform_keys in dry-struct). It seems like this issue indicates that the relationship between the two abstractions is not clear. Should I open a Github issue, do you think?