An ActiveModel comforming Form object based on dry-type / dry-validation.

require 'dry-validation'
require 'dry-struct'
require 'active_model/errors'

require_relative '../create_study'

module MyApp
  module Types
    include Dry::Types.module
  end

  class CreateStudy
    class Form < Dry::Struct
      transform_keys(&:to_sym)

      attribute :title, Types::String.meta(omittable: true)
      attribute :oid, Types::String.meta(omittable: true)
      
      # ActiveModel
      def persisted?
        false
      end
      
      # ActiveModel
      def model_name
        Struct.new(:param_key, :name).new('form', 'form')
      end
      
      # ActiveModel
      def to_key
        nil
      end
      
      def errors
        @errors ||= ActiveModel::Errors.new(self)
      end

      def valid?
        validate
        @validation.success?
      end
      
      private
      
      def schema
        Dry::Validation.Schema do
          required(:name).filled
        end
      end     
      
      def validate
        unless defined?(@validation)
          @validation = schema.call(attributes)
          @validation.messages.each do |field, field_errors|
            field_errors.each do |field_error|
              errors.add(field, field_error)
            end
          end
        end

        self
      end
    end
  end
end

form = MyApp::CreateStudy::Form.new
form.valid? # => false

form = MyApp::CreateStudy::Form.new(title: 'Yellow')
form.valid? # => true

We could remove any dependency on ActiveModel by implementing our own errors object which ducktypes as ActiveModel::Errors.