Convert Dry::Schema::Result to Dry::Struct

@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
1 Like