Module: Hecks::Forms::RecordRenderer

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

Overview

The two pages a .bluebook doesn't declare but a browsing developer always wants: every record of an aggregate, and one record's own state plus what it can legally do next. Neither is a query — they read the repository directly, the same way AggregateDoor#all/#find do (facade/surface/aggregate_door.rb) — so they exist for every aggregate whether or not its bluebook declared a query at all.

Class Method Summary collapse

Class Method Details

.applies?(aggregate, command, current_state) ⇒ Boolean

Returns:

  • (Boolean)


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

def self.applies?(aggregate, command, current_state)
  transitions = aggregate.lifecycle&.transitions_for(command.hecks_name) || []
  return true if transitions.empty?

  transitions.any? { |t| t.from.nil? || Array(t.from).map(&:to_s).include?(current_state.to_s) }
end

Every non-creating command, EXCEPT a lifecycle transition that does not apply from the record's current state — the same rule Rules#admissible_transition enforces at dispatch, read here so a link that would only refuse is never offered in the first place.



74
75
76
77
78
79
80
81
82
83
# File 'lib/hecks/forms/record_renderer.rb', line 74

def self.command_links(domain, aggregate, id, current_state)
  commands = aggregate.commands.reject(&:creates?).select { |cmd| applies?(aggregate, cmd, current_state) }
  return "<p><em>No commands act on an existing #{Escape.html(aggregate.hecks_name)}.</em></p>" if commands.empty?

  items = commands.map do |cmd|
    href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.attr(id)}"
    %(<li><a href="#{href}"><span>#{Escape.html(cmd.hecks_name)}</span><span class="kind">#{Escape.html(cmd.goal.to_s)}</span></a></li>)
  end
  %(<ul class="verb-list">#{items.join}</ul>)
end


26
27
28
29
30
31
32
33
34
# File 'lib/hecks/forms/record_renderer.rb', line 26

def self.creating_links(domain, aggregate)
  creators = aggregate.commands.select(&:creates?)
  return "" if creators.empty?

  links = creators.map { |cmd|
    %(<a class="button" href="/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html">+ #{Escape.html(cmd.hecks_name)}</a>)
  }
  %(<div class="actions">#{links.join}</div>)
end

.index(registry:, domain:, aggregate:) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
# File 'lib/hecks/forms/record_renderer.rb', line 14

def self.index(registry:, domain:, aggregate:)
  instances = registry.repository(domain, aggregate).all
  <<~HTML
    <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")}</h1>
    #{aggregate.description ? %(<p class="goal">#{Escape.html(aggregate.description)}</p>) : ''}
    #{creating_links(domain, aggregate)}
    <h2>Records (#{instances.size})</h2>
    #{RecordTable.render(aggregate, instances, domain: domain)}
    #{verb_list(domain, aggregate)}
  HTML
end


92
93
94
95
96
97
98
99
# File 'lib/hecks/forms/record_renderer.rb', line 92

def self.query_links(domain, aggregate)
  return "" if aggregate.queries.empty?

  items = aggregate.queries.map do |query|
    %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html"><span>#{Escape.html(query.hecks_name)}</span><span class="kind">query</span></a></li>)
  end
  %(<h2>Queries</h2><ul class="verb-list">#{items.join}</ul>)
end

.render_value(value) ⇒ Object

Value.materialize first — a stored field is a Runtime::Value wherever its attribute is a value object, not a plain Hash (see record_table.rb's own note on the same read).



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

def self.render_value(value)
  case (value = Runtime::Value.materialize(value))
  when Hash then value.map { |k, v| "#{k}: #{render_value(v)}" }.join(", ")
  when Array then value.map { |v| render_value(v) }.join("; ")
  else value.to_s
  end
end

.show(registry:, domain:, aggregate:, id:) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/hecks/forms/record_renderer.rb', line 36

def self.show(registry:, domain:, aggregate:, id:)
  instance = registry.repository(domain, aggregate).find(id)
  return nil unless instance

  state = instance.state
  current = aggregate.lifecycle && state[aggregate.lifecycle.field]
  <<~HTML
    <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")} <span class="mono">#{Escape.html(id)}</span></h1>
    #{current ? %(<span class="badge role">#{Escape.html(aggregate.lifecycle.field)}: #{Escape.html(current)}</span>) : ''}
    #{state_table(state)}
    <h2>Commands</h2>
    #{command_links(domain, aggregate, id, current)}
    #{query_links(domain, aggregate)}
  HTML
end

.state_table(state) ⇒ Object



52
53
54
55
56
57
# File 'lib/hecks/forms/record_renderer.rb', line 52

def self.state_table(state)
  rows = state.map { |key, value|
    "<tr><th>#{Escape.html(Humanize.label(key.to_s))}</th><td>#{Escape.html(render_value(value))}</td></tr>"
  }
  %(<div class="table-scroll"><table><tbody>#{rows.join}</tbody></table></div>)
end

.verb_list(domain, aggregate) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/hecks/forms/record_renderer.rb', line 101

def self.verb_list(domain, aggregate)
  commands = aggregate.commands.reject(&:creates?)
  return "" if commands.empty? && aggregate.queries.empty?

  cmd_items = commands.map { |c|
    %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{c.hecks_name}.html"><span>#{Escape.html(c.hecks_name)}</span><span class="kind">command</span></a></li>)
  }
  query_items = aggregate.queries.map { |q|
    %(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{q.hecks_name}.html"><span>#{Escape.html(q.hecks_name)}</span><span class="kind">query</span></a></li>)
  }
  %(<h2>Every command &amp; query on #{Escape.html(aggregate.hecks_name)}</h2><ul class="verb-list">#{(cmd_items + query_items).join}</ul>)
end