Module: Plutonium::Wizard::Data

Defined in:
lib/plutonium/wizard/data.rb

Overview

Builds the wizard's typed data snapshot (§2.6). data is step-keyed: a container exposing one typed sub-object per step, so fields are addressed as data.<step>.<field> (e.g. data.identity.name, data.profile.tier). Each step sub-object is backed by ActiveModel::Attributes — scalar values are cast to their declared types and uncollected fields read as nil. Step namespacing means two steps may declare the same field name without colliding.

structured_input ..., repeat: collections (which declare no scalar types — their sub-fields come from input declarations) are exposed on their step's sub-object as arrays of typed sub-objects responding to the declared sub-field names (data.members.invites.first.email).

Defined Under Namespace

Classes: Container, StructuredRow

Class Method Summary collapse

Class Method Details

.class_for(schema, options: {}, structured: {}, validations: []) ⇒ Object

Parameters:

  • schema (Hash{Symbol=>Symbol})

    scalar attribute name => type

  • options (Hash{Symbol=>Hash}) (defaults to: {})

    scalar attribute name => options (default:, etc.)

  • structured (Hash{Symbol=>Array<Symbol>}) (defaults to: {})

    structured name => sub-field names

  • validations (Array<[Array, Hash]>) (defaults to: [])

    inline validates declarations ([args, options]) replayed onto the class so the form pipeline can infer required markers via validators_on (§7). The class is never .valid?'d in the form path — validation runs through the runner — so this only feeds introspection.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
# File 'lib/plutonium/wizard/data.rb', line 41

def self.class_for(schema, options: {}, structured: {}, validations: [])
  Class.new do
    include ActiveModel::Model
    include ActiveModel::Attributes

    # Anonymous classes have no name, which breaks label/error translation
    # lookups (`human_attribute_name` / `errors.full_messages` call
    # `model_name`). Supply a stable one so the form/display pipelines can
    # humanize attribute labels.
    def self.model_name = ActiveModel::Name.new(self, nil, "Wizard")

    schema.each do |name, type|
      attribute(name, Plutonium::Wizard.safe_attribute_type(type), **(options[name] || {}))
    end

    validations.each { |args, opts| validates(*args, **opts) }

    structured.each do |name, fields|
      # Backed by a plain accessor (not an ActiveModel attribute) so the raw
      # array survives without coercion, then wrapped on read.
      attr_writer name
      define_method(name) do
        rows = Array(instance_variable_get(:"@#{name}"))
        rows.map do |row|
          values = row.respond_to?(:to_h) ? row.to_h.transform_keys(&:to_s) : {}
          StructuredRow.new(fields, values)
        end
      end
    end

    # Accept the union of scalar + structured keys, ignoring unknown keys.
    define_method(:initialize) do |attrs = {}|
      attrs = (attrs || {}).symbolize_keys
      scalar = attrs.slice(*schema.keys)
      super(scalar)
      structured.each_key do |name|
        instance_variable_set(:"@#{name}", attrs[name] || [])
      end
    end

    # Typed plain-hash view: cast scalars + structured rows as hashes.
    define_method(:to_h) do
      h = {}
      schema.each_key { |name| h[name] = public_send(name) }
      structured.each_key { |name| h[name] = public_send(name).map(&:to_h) }
      h
    end
  end
end