Module: LcpRuby::Presenter::Enrichment

Defined in:
lib/lcp_ruby/presenter/enrichment.rb

Overview

Per-entry decoration of show fields and index columns. Each enricher is a pure function: (entry, location:, model_def:, loader:) -> entry. Must be a no-op when the enricher’s keys are absent from entry.

Constant Summary collapse

ENTRY_ENRICHERS =
%i[link].freeze

Class Method Summary collapse

Class Method Details

.auto_detect_dot_path(field, model_def:, loader:) ⇒ Object

Walk a dot-path and compute (chain, target_model). Chain = every segment except a scalar terminal; if the terminal is itself a belongs_to, the whole path is the chain.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/lcp_ruby/presenter/enrichment.rb', line 85

def auto_detect_dot_path(field, model_def:, loader:)
  segments = field.split(".")
  current_model = model_def

  segments[0..-2].each do |seg|
    assoc = current_model.find_belongs_to(seg)
    return [ nil, nil ] unless assoc&.target_model

    next_def = loader.model_definitions[assoc.target_model]
    return [ nil, nil ] unless next_def

    current_model = next_def
  end

  last = segments.last
  terminal = current_model.find_belongs_to(last)

  if terminal
    return [ nil, nil ] if terminal.polymorphic || terminal.target_model.nil?

    [ segments, terminal.target_model ]
  else
    [ segments[0..-2], current_model.name ]
  end
end

.enrich(entry, location:, model_def:, loader:) ⇒ Hash

Decorate a single field/column hash with enricher-produced keys.

Parameters:

  • entry (Hash)

    field or column config hash (string keys)

  • location (Symbol)

    :field (show) or :column (index)

  • model_def (Metadata::ModelDefinition)

    owner model of the entry

  • loader (Metadata::Loader)

    current loader (for presenter / target-model lookup)

Returns:

  • (Hash)

    entry with any enricher keys merged in



18
19
20
21
22
23
24
# File 'lib/lcp_ruby/presenter/enrichment.rb', line 18

def enrich(entry, location:, model_def:, loader:)
  return entry unless entry.is_a?(Hash)

  ENTRY_ENRICHERS.reduce(entry) do |e, name|
    send("enrich_#{name}_options", e, location: location, model_def: model_def, loader: loader)
  end
end

Silent on invalid input — the boot-time validator rejects it first.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/lcp_ruby/presenter/enrichment.rb', line 27

def enrich_link_options(entry, location:, model_def:, loader:)
  return entry unless link_opt_in?(entry, location)

  chain, target_model = resolve_chain(entry, location: location, model_def: model_def, loader: loader)
  return entry if chain.nil?

  slug = resolve_presenter_slug(target_model, entry: entry)

  entry.merge(
    "link_chain" => chain,
    "link_target_model" => target_model,
    "link_target_presenter_slug" => slug
  )
end

Returns:

  • (Boolean)


42
43
44
45
46
47
48
# File 'lib/lcp_ruby/presenter/enrichment.rb', line 42

def link_opt_in?(entry, location)
  return true if entry["link"] == true
  return true if entry["link_through"].present?
  return true if location == :column && entry["link_to"].to_s == "show"

  false
end

.resolve_chain(entry, location:, model_def:, loader:) ⇒ Object

Returns [chain_array, target_model_name] or [nil, nil] when enrichment should be skipped.



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
# File 'lib/lcp_ruby/presenter/enrichment.rb', line 51

def resolve_chain(entry, location:, model_def:, loader:)
  # Explicit link_through takes precedence over auto-detection.
  lt = entry["link_through"]
  if lt.present?
    assoc = model_def.find_belongs_to(lt.to_s)
    return [ nil, nil ] unless assoc&.target_model

    return [ [ lt.to_s ], assoc.target_model ]
  end

  field = entry["field"].to_s

  # Column: link_to: :show and link:true on scalar → empty chain (current row).
  if location == :column
    if entry["link_to"].to_s == "show" || (entry["link"] == true && !field.include?(".") && model_def.find_belongs_to(field).nil?)
      return [ [], model_def.name ]
    end
  end

  # From here on, only link: true remains (explicit link_through was handled above).
  return [ nil, nil ] unless entry["link"] == true

  if field.include?(".")
    auto_detect_dot_path(field, model_def: model_def, loader: loader)
  elsif (assoc = model_def.find_belongs_to(field))
    [ [ field ], assoc.target_model ]
  else
    [ nil, nil ] # show-field link:true on scalar — validator V3a errors
  end
end

.resolve_presenter_slug(target_model_name, entry: nil) ⇒ Object

B19 — three-tier resolution for multi-presenter models:

1. Per-link override (`link_presenter:` on entry) wins when present.
2. Single presenter — unambiguous, backward-compatible (most models).
3. Multi-presenter — the one marked `default: true` is canonical.

Returns nil when multi-presenter has no resolution; the validator rejects that configuration at boot, so we don’t have to fall back to alphabetic order at render time (which was the original B19 bug).



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/lcp_ruby/presenter/enrichment.rb', line 119

def resolve_presenter_slug(target_model_name, entry: nil)
  return nil if target_model_name.blank?

  presenters = Resolver.presenters_for_model(target_model_name)

  if entry && (override = entry["link_presenter"]).present?
    return presenters.find { |p| p.slug == override.to_s }&.slug
  end

  return presenters.first&.slug if presenters.length <= 1

  presenters.find(&:default?)&.slug
rescue MetadataError
  nil
end