Module: Axn::Strategies::Form

Defined in:
lib/axn/strategies/form.rb

Class Method Summary collapse

Class Method Details

.assign_constant(constant_path, klass) ⇒ Object

Helper method to assign a class to a constant path

Parameters:

  • constant_path (String)

    the full constant path (e.g., “CreateUser::Form”)

  • klass (Class)

    the class to assign



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/axn/strategies/form.rb', line 95

def self.assign_constant(constant_path, klass)
  parts = constant_path.split("::")
  constant_name = parts.pop
  parent_path = parts.join("::")

  if parent_path.empty?
    # Top-level constant
    Object.const_set(constant_name, klass)
  else
    # Nested constant - ensure parent namespace exists
    parent = parent_path.constantize
    parent.const_set(constant_name, klass)
  end
end

.configure(expect: :params, expose: :form, type: nil, inject: nil) { ... } ⇒ Object

Parameters:

  • expect (Symbol) (defaults to: :params)

    the attribute name to expect in the context (e.g. :params)

  • expose (Symbol) (defaults to: :form)

    the attribute name to expose in the context (e.g. :form)

  • type (Class, String) (defaults to: nil)

    the form class to use, or a string constant path

  • inject (Array<Symbol>) (defaults to: nil)

    optional additional attributes to include in the form (e.g. [:user, :company])

Yields:

  • block to define the form class (optional). When type is omitted, the block defines an anonymous form class. When type is a string and the constant doesn’t exist, the block defines the class and it is assigned to that constant.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/axn/strategies/form.rb', line 12

def self.configure(expect: :params, expose: :form, type: nil, inject: nil, &block)
  expect ||= :"#{expose.to_s.delete_suffix('_form')}_params"

  # Aliasing to avoid shadowing/any confusion
  expect_attr = expect
  expose_attr = expose

  Module.new do
    extend ActiveSupport::Concern

    included do
      if type.nil? && name.nil? && !block_given?
        raise ArgumentError,
              "form strategy: must pass explicit :type parameter or a block to `use :form` when applying to anonymous classes"
      end

      resolved_type = Axn::Strategies::Form.resolve_type(type, expose_attr, name, &block)

      raise ArgumentError, "form strategy: #{resolved_type} must implement `valid?`" unless resolved_type.method_defined?(:valid?)

      expects expect_attr, type: :params
      exposes(expose_attr, type: resolved_type)

      define_method expose_attr do
        attrs_for_form = public_send(expect_attr)&.dup || {}

        Array(inject).each do |ctx|
          attrs_for_form[ctx] = public_send(ctx)
        end

        resolved_type.new(attrs_for_form)
      end
      memo expose_attr

      before do
        expose expose_attr => public_send(expose_attr)
        fail! unless public_send(expose_attr).valid?
      end
    end
  end
end

.resolve_type(type, expose_attr, action_name) { ... } ⇒ Class

Resolve the form type from the given parameters

Parameters:

  • type (Class, String, nil)

    the form class, constant path, or nil for auto-detection

  • expose_attr (Symbol)

    the attribute name to expose (used for auto-detection)

  • action_name (String, nil)

    the name of the action class (used for auto-detection)

Yields:

  • block to define the form class (when type is nil = anonymous form; when type is a string and constant doesn’t exist = define and assign)

Returns:

  • (Class)

    the resolved form class



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/axn/strategies/form.rb', line 60

def self.resolve_type(type, expose_attr, action_name, &)
  # Simplest case: form defined purely by block, no type/constant
  if type.nil? && block_given?
    form_class_name = action_name ? "#{action_name}::#{expose_attr.to_s.classify}" : "AnonymousForm"
    return Class.new(Axn::FormObject).tap do |klass|
      klass.define_singleton_method(:name) { form_class_name }
      klass.class_eval(&)
    end
  end

  type ||= "#{action_name}::#{expose_attr.to_s.classify}"

  if type.is_a?(Class)
    raise ArgumentError, "form strategy: cannot provide block when type is a Class" if block_given?

    return type
  end

  type.constantize.tap do
    raise ArgumentError, "form strategy: cannot provide block when type constant #{type} already exists" if block_given?
  end
rescue NameError
  # Constant doesn't exist
  raise ArgumentError, "form strategy: type constant #{type} does not exist and no block provided to define it" unless block_given?

  # Create the class using the block, inheriting from Axn::FormObject
  Class.new(Axn::FormObject).tap do |klass|
    klass.class_eval(&)
    assign_constant(type, klass)
  end
end