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
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
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…
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?
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.