Undefined local variable or method

I am working on a new shard kemal-form that tries to help validate forms. I am now trying to make a new validator EqualTo that is supposed to make sure that the value of two fields are identical.

I have this macro that generates fields for forms

macro field(decl, **options)
  {% raise "must be type Kemal::Form::FormField" unless decl.type.resolve < Kemal::Form::FormField %}
  {%
    field_name = decl.var
    field_type = decl.type

    id_attr = options[:id]
    if id_attr.nil?
      id_attr = field_name.id.stringify
    end

    name_attr = options[:name]
    if name_attr.nil?
      name_attr = field_name.id.stringify
    end

    extra_attrs = options[:attrs]

    field_value = options[:value]
    if field_value.nil?
      field_value = ""
    end

    required = false
    if options[:required] == true
      required = true
    end

    label = options[:label]
    if label.nil?
      label_for = id_attr
      label_text = id_attr.titleize
    end

    validators = options[:validators]
  %}

  @{{field_name}} : {{field_type}} = {{field_type.id}}.new(
    id: {{id_attr}},
    name: {{name_attr}},
    attrs: {{extra_attrs}},
    value: {{field_value}},
    required: {{required}},
    {% if validators.nil? %}
      validators: [] of Kemal::FormValidator::Validator,
    {% else %}
      validators: {{validators.id}} of Kemal::FormValidator::Validator,
    {% end %}
    {% if label.nil? %}
      label: Kemal::Form::Label.new({{label_for}}, {{label_text}}, nil))
    {% else %}
      label: {{label}})
    {% end %}

  def {{field_name.id}} : {{field_type.id}}
    @{{field_name}}
  end
end

simplified version of EqualTo

class EqualTo < Kemal::FormValidator::Validator
  def initialize(@compare_field : Kemal::Form::FormField); end
end

but when I initialize a new EqualTo

class MyForm < Kemal::Form
  field password : Kemal::Form::PasswordField
  field confirm_password : Kemal::Form::PasswordField, 
                            validators: [
                              Kemal::FormValidator::EqualTo.new(password)
                            ]
end

I will get this error: Error: undefined local variable or method 'password' for MyForm.class.

I have tried to figure out how to solve this for a few days now but still no luck :frowning:

I think you simplified the macro a bit too much here, you’re not making any use of options there which is the critical part.

Sorry, I updated the macro code snippet

So you’re generating

class MyForm < Kemal::Form
  # ...
  @confirm_password : Kemal::Form::PasswordField = Kemal::Form::PasswordField.new(
    # ...
    validators: [Kemal::FormValidator::EqualTo.new(password)] of Kemal::Form::Validator
  )
end

# Or to simplify

class Foo
  @confirm_password = Bar.new(password)

  def password
  end
end

I hope it is evident that you cannot refer to an instance method at the class level.

If it’s an option for you to generate the class initializer then you could try to move the initialization there, something akin to

class MyForm < Kemal::Form
  # ...
  @confirm_password : Kemal::Form::PasswordField

   def initialize
    @confirm_password = Kemal::Form::PasswordField.new(
      # ...
      validators: [Kemal::FormValidator::EqualTo.new(password)] of Kemal::Form::Validator
    )
   end
end

If that’s not an option then you have to redesign either the API of PasswordField or EqualTo to take any parameters in the form of something with delayed execution, that is as a block, which is potentially generated by the macro code. Sensible variants could be yielding the model object into the validator or a “validator list provider”

class MyForm < Kemal::Form
  # ...
  @confirm_password : Kemal::Form::PasswordField = Kemal::Form::PasswordField.new(
    # ...
    validators: [Kemal::FormValidator::EqualTo.new(&.password)] of Kemal::Form::Validator
  )
end

# ---

class MyForm < Kemal::Form
  # ...
  @confirm_password : Kemal::Form::PasswordField = Kemal::Form::PasswordField.new(
    # ...
    validators: -> (form : MyForm) { [Kemal::FormValidator::EqualTo.new(form.password)] }
  )
end

I hope there’s some ideas for you in here.

Actually, this looks like a bug. You should be able to call an instance method from an instance variable initializer.

That said, the initializer seems to be resolved in the class scope, not the instance scope, so changing that would be a breaking change. I have no idea why that’s implemented that way… :man_shrugging:

Of the possible solutions given I like your last suggestion the most.

But I am still confused because I thought that macro code would be expanded into “real” code before the code is compiled and run? If this is true then I would expect it to be possible to use instance variables as arguments to initializers and other methods?

Are you talking about my code or crystal?

Crystal.

We prevented that to avoid dependencies between ivar initializers

2 Likes

If it would be helpful you could look into integrating Validator - Athena. Follows a similar pattern of having classes to represent the constraints, but also supports multiple violations at once, among other features.

1 Like

I’ll take look, thank you :slight_smile:

FWIW you can see what code is generated from macros: Is there a way to see what a Crystal macro expands to? - Stack Overflow

I wonder if the error message could be expanded to explain more?