Module: Hecks::Forms::Params

Defined in:
lib/hecks/forms/params.rb

Overview

The two directions between a flat, dotted, ALL-STRINGS web payload ({"amount.cents"=>"1050", "amount.currency"=>"USD"}, whether it came off a POST form body or a GET query string — Rack hands back the same flat shape for both as long as nothing uses [] bracket names) and the nested, typed hash Dispatcher#dispatch/#query actually take ({amount: {cents: 1050, currency: "USD"}}).

extract NEEDS the Field tree, not just the raw params — a numeric leaf's own runtime check (Value::Coercion#check_numeric_fields) requires an actual Integer/Float, not a String that merely looks like one (given.is_a?(expected), no coercion attempted there); a web form can only ever hand back strings, so the cast has to happen here, once, using the SAME shape FieldShape already resolved for rendering the input in the first place — one reading of the IR, not two that could disagree.

Class Method Summary collapse

Class Method Details

.cast_scalar(field, text) ⇒ Object



84
85
86
87
88
89
# File 'lib/hecks/forms/params.rb', line 84

def self.cast_scalar(field, text)
  case field.kind
  when :number then field.step == "1" ? Integer(text) : Float(text)
  else text
  end
end

.checkbox(raw_value) ⇒ Object



80
81
82
# File 'lib/hecks/forms/params.rb', line 80

def self.checkbox(raw_value)
  %w[on 1 true].include?(raw_value.to_s.downcase)
end

.collect(field, raw, pairs) ⇒ Object



36
37
38
39
40
41
42
# File 'lib/hecks/forms/params.rb', line 36

def self.collect(field, raw, pairs)
  case field.kind
  when :group, :money then field.children.each { |child| collect(child, raw, pairs) }
  when :list then (value = extract_list(field, raw)) == SKIP || pairs[field.path] = value
  else (value = extract_leaf(field, raw)) == SKIP || pairs[field.path] = value
  end
end

.extract(fields, raw) ⇒ Object

Every LEAF field carries its own full dotted path regardless of how deep FieldShape nested it to get there — a single-attribute value object unwraps to a leaf sitting at the TOP of the fields array with a two-segment path ("reference.value"), the exact same shape a :group's own child carries. So this collects every leaf as (full path -> value) FIRST, and nests by the path's OWN segments last — one nesting rule, blind to how a field arrived at its path.



30
31
32
33
34
# File 'lib/hecks/forms/params.rb', line 30

def self.extract(fields, raw)
  pairs = {}
  fields.each { |field| collect(field, raw, pairs) }
  nest(pairs)
end

.extract_leaf(field, raw) ⇒ Object



71
72
73
74
75
76
77
78
# File 'lib/hecks/forms/params.rb', line 71

def self.extract_leaf(field, raw)
  return checkbox(raw[field.path]) if field.kind == :boolean

  text = raw[field.path]
  return SKIP if (text.nil? || text.empty?) && field.optional?

  cast_scalar(field, text)
end

.extract_list(field, raw) ⇒ Object

One line of the textarea per element. A line that itself needs several fields (a multi-attribute value object as a list element) is read as JSON on that one line — the honest fallback documented in docs/command-form-and-query-form-bluebook.md rather than a second widget this prototype doesn't build yet.



61
62
63
64
65
66
67
68
69
# File 'lib/hecks/forms/params.rb', line 61

def self.extract_list(field, raw)
  text = raw[field.path]
  return SKIP if text.nil? || text.strip.empty?

  item = field.children.first
  text.each_line.map(&:strip).reject(&:empty?).map do |line|
    item.leaf? ? cast_scalar(item, line) : JSON.parse(line, symbolize_names: true)
  end
end

.flatten(fields, values, into: {}) ⇒ Object

The other direction — a nested value hash back to the flat dotted pairs a GET link's query string carries, so a query view's "shareable link" and its filter FORM stay two renderings of the SAME data rather than two formats that can drift. Reads with FieldRenderer.dig — the identical full-path lookup a re-rendered input's own value comes from — for the same reason extract above nests by full path rather than by tree shape.



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/hecks/forms/params.rb', line 98

def self.flatten(fields, values, into: {})
  fields.each do |field|
    case field.kind
    when :group, :money then flatten(field.children, values, into: into)
    when :list then into[field.path] = Array(FieldRenderer.dig(values, field.path)).join("\n")
    else
      value = FieldRenderer.dig(values, field.path)
      into[field.path] = value.to_s unless value.nil?
    end
  end
  into
end

.nest(pairs) ⇒ Object



44
45
46
47
48
49
50
51
# File 'lib/hecks/forms/params.rb', line 44

def self.nest(pairs)
  pairs.each_with_object({}) do |(path, value), result|
    segments = path.to_s.split(".").map(&:to_sym)
    leaf = segments.pop
    node = segments.reduce(result) { |acc, segment| acc[segment] ||= {} }
    node[leaf] = value
  end
end

.paths(fields, into: []) ⇒ Object

Every leaf/list path a field tree carries, independent of any values — what command_form_renderer.rb's inspect panel wants ("which fields does this command take"), where flatten above wants "what does THIS submission look like" and returns nothing for a field nothing was entered for.



116
117
118
119
120
121
122
123
124
# File 'lib/hecks/forms/params.rb', line 116

def self.paths(fields, into: [])
  fields.each do |field|
    case field.kind
    when :group, :money then paths(field.children, into: into)
    else into << field.path
    end
  end
  into
end

.to_query_string(fields, values) ⇒ Object



126
127
128
129
# File 'lib/hecks/forms/params.rb', line 126

def self.to_query_string(fields, values)
  pairs = flatten(fields, values)
  pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
end