How to add an instance method from inside of a class method which accepts a block to be executed in the context of the instance

StackOverflow https://stackoverflow.com/questions/21216439

Question

I've entirely reworded this question as I feel this more accurately reflects what I wanted to ask the first time in a less roundabout way.

After instantiating a FormObject, calls to dynamically defined methods do not evaluate their block parameter in the context I'm trying for. For example:

@registration = RegistrationForm.new
@registration.user
# undefined local variable or method `user_params' for RegistrationForm:Class

RegistrationForm calls a class method exposing(:user) { User.new(user_params) } which I would like to have define a new method that looks like this:

def user
  @user ||= User.new(user_params)
end

My implementation doesn't use @ivar ||= to cache the value (since falsey values will cause the method to be re-evaluated). I borrowed the idea from rspec's memoized_helpers and I 'think' I understand how it works. What I don't understand is what I should replace class_eval with in lib/form_object/memoized_helpers.rb.

Thank you

lib/form_object/base.rb

class FormObject::Base
  include ActiveModel::Model
  include FormObject::MemoizedHelpers

  attr_reader :params, :errors

  def initialize(params = {})
    @params = ActionController::Parameters.new(params)
    @errors = ActiveModel::Errors.new(self)
  end

  def save
    valid? && persist
  end
end

lib/form_object/memoized_helpers.rb

module FormObject
  module MemoizedHelpers
    private
    def __memoized
      @__memoized ||= {}
    end

    def self.included(mod)
      mod.extend(ClassMethods)
    end

    module ClassMethods
      def exposing(name, &block)
        raise "#exposing called without a block" unless block_given?

        class_eval do
          define_method(name) { __memoized.fetch(name) { |k| __memoized[k] = block.call } }
        end
      end
    end
  end
end

app/forms/registration_form.rb

class RegistrationForm < FormObject::Base
  exposing(:user)   { User.new(user_params) { |u| u.is_admin = true } }
  exposing(:tenant) { user.build_tenant(tenant_params) }

  validate do
    tenant.errors.each do |key, value|
      errors.add("#{tenant.class.name.underscore}_#{key}", value)
    end unless tenant.valid?
  end

  validate do
    user.errors.each do |key, value|
      errors.add("#{user.class.name.underscore}_#{key}", value)
    end unless user.valid?
  end

  private

  def persist
    user.save
  end

  def user_params
    params.fetch(:user, {}).permit(:first_name, :last_name, :email, :password, :password_confirmation)
  end

  def tenant_params
    params.fetch(:tenant, {}).permit(:name)
  end
end
Was it helpful?

Solution

So, I might have simplified this example too much, but I think this is what you want:

module Exposing
  def exposing(name, &block)
    instance_eval do
      define_method(name, block)
    end
  end
end

class Form
  extend Exposing

  exposing(:user) { user_params }

  def user_params
    {:hi => 'ho'}
  end
end

Form.new.user

You can fiddle around here: http://repl.it/OCa

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top