Module: RoundhouseUi::Tags
- Defined in:
- lib/roundhouse_ui/tags.rb
Overview
Resolves host-defined tags for a job at read time (no middleware, no storage — works on every backend and applies retroactively to jobs already in the sets). The host supplies the resolver; see ADR 0002.
RoundhouseUi. = RoundhouseUi::Tags.from_constant(:OWNER, as: :squad)
# or any callable:
RoundhouseUi. = ->(klass:, item:) { { squad: :growth } }
Contract: resolver output is normalized to string keys/values, masked via Redaction (redact_args patterns apply to tag keys), and a raising resolver yields no tags — tagging must never break a page.
Constant Summary collapse
- EMPTY =
{}.freeze
Class Method Summary collapse
- .effective_klass(klass, item) ⇒ Object
-
.filters ⇒ Object
The declared filter vocabulary, normalized to { "key" => ["value", ...] }, or nil when the host declared none (the filter UI then discovers values from the entries it scans).
-
.for(klass:, item:, cache: nil) ⇒ Object
Tags for one job entry, as a { "key" => "value" } Hash (EMPTY when no resolver is configured, the resolver declines, or it raises).
-
.from_constant(const_name, as: const_name.to_s.downcase) ⇒ Object
The class-constant convention (Trainual's OWNER pattern) as a resolver:.
-
.match?(tags, key, value) ⇒ Boolean
Does a resolved tag Hash match a key/value filter? Exact match on the normalized (post-redaction) value — so a redacted tag matches only its mask, and the filter can't be used to probe redacted values.
-
.resolve(resolver, klass, item) ⇒ Object
itemis nil in class-cached mode: an args-reading resolver cached by class would poison the cache with first-job-wins values — withholding the payload makes it fail deterministically (rescued → no tags) instead. - .warn_once(message) ⇒ Object
Class Method Details
.effective_klass(klass, item) ⇒ Object
91 92 93 94 |
# File 'lib/roundhouse_ui/tags.rb', line 91 def effective_klass(klass, item) wrapped = item["wrapped"] if item.is_a?(Hash) (wrapped || klass)&.to_s end |
.filters ⇒ Object
The declared filter vocabulary, normalized to { "key" => ["value", ...] }, or nil when the host declared none (the filter UI then discovers values from the entries it scans). Both the whole setting and individual values may be callables, so vocabularies can be dynamic.
65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/roundhouse_ui/tags.rb', line 65 def filters declared = RoundhouseUi.tag_filters declared = declared.call if declared.respond_to?(:call) return nil unless declared.is_a?(Hash) declared.each_with_object({}) do |(key, values), out| values = values.call if values.respond_to?(:call) out[key.to_s] = Array(values).map(&:to_s) end rescue => e warn_once("tag_filters failed: #{e.}") nil end |
.for(klass:, item:, cache: nil) ⇒ Object
Tags for one job entry, as a { "key" => "value" } Hash (EMPTY when no resolver is configured, the resolver declines, or it raises).
klass/item come from the backend entry (entry.klass / entry.item);
the ActiveJob adapter wrapper is unwrapped here, so resolvers always see
the real job class. Pass a Hash as cache to memoize per class across a
request — unused (and unneeded) in per-job mode.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/roundhouse_ui/tags.rb', line 25 def for(klass:, item:, cache: nil) resolver = RoundhouseUi. return EMPTY unless resolver effective = effective_klass(klass, item) return EMPTY unless effective if RoundhouseUi. # Per-job mode still memoizes, keyed by jid rather than class: a job's # tags cannot change within one request, and the same entry is resolved # twice otherwise — once while scanning, once when its badge renders. jid = item["jid"] if item.is_a?(Hash) return resolve(resolver, effective, item) unless cache && jid cache.key?(jid) ? cache[jid] : cache[jid] = resolve(resolver, effective, item) elsif cache # Class-cached mode: item is withheld (deliberately — see resolve). cache.key?(effective) ? cache[effective] : cache[effective] = resolve(resolver, effective, nil) else resolve(resolver, effective, nil) end end |
.from_constant(const_name, as: const_name.to_s.downcase) ⇒ Object
The class-constant convention (Trainual's OWNER pattern) as a resolver:
RoundhouseUi. = RoundhouseUi::Tags.from_constant(:OWNER, as: :squad)
Tags every job whose class (or ancestor — inherited constants count, so a base-class OWNER covers subclasses) defines the constant.
54 55 56 57 58 59 |
# File 'lib/roundhouse_ui/tags.rb', line 54 def from_constant(const_name, as: const_name.to_s.downcase) lambda do |klass:, item:| k = klass.to_s.safe_constantize { as => k.const_get(const_name) } if k&.const_defined?(const_name) end end |
.match?(tags, key, value) ⇒ Boolean
Does a resolved tag Hash match a key/value filter? Exact match on the normalized (post-redaction) value — so a redacted tag matches only its mask, and the filter can't be used to probe redacted values.
82 83 84 |
# File 'lib/roundhouse_ui/tags.rb', line 82 def match?(, key, value) [key.to_s] == value.to_s end |
.resolve(resolver, klass, item) ⇒ Object
item is nil in class-cached mode: an args-reading resolver cached by
class would poison the cache with first-job-wins values — withholding the
payload makes it fail deterministically (rescued → no tags) instead.
Hosts that need the payload set RoundhouseUi.job_tags_per_job = true.
100 101 102 103 104 105 106 107 108 109 |
# File 'lib/roundhouse_ui/tags.rb', line 100 def resolve(resolver, klass, item) = resolver.call(klass: klass, item: item) return EMPTY unless .is_a?(Hash) normalized = .each_with_object({}) { |(k, v), out| out[k.to_s] = v.to_s } Redaction.apply(normalized) rescue => e warn_once("job_tags resolver failed for #{klass}: #{e.}") EMPTY end |
.warn_once(message) ⇒ Object
111 112 113 |
# File 'lib/roundhouse_ui/tags.rb', line 111 def warn_once() Rails.logger&.warn("[roundhouse] #{}") if defined?(Rails) end |