Class: Rigor::Effects::Registry

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/effects/registry.rb

Overview

The effect-label vocabulary: which spellings are recognised, which have been retired, and who may open a new root (ADR-103 WD2; normative in docs/type-specification/effect-labels.md).

The shared layer is hand-written in data/effects/registry.yml — Steins' v1 set verbatim, Ruby's mutate leaves, the proposed shared core leaves and the application-meaning roots — so a later slice or a plugin extends the data, not this Ruby. Extensions arrive through #with, which is the one place root ownership is enforced.

A Registry is a frozen value object; #with returns a new one rather than mutating.

Defined Under Namespace

Classes: Error, InvalidLabelError, OwnershipError

Constant Summary collapse

DATA_PATH =
File.expand_path("../../../data/effects/registry.yml", __dir__)
SUGGESTION_DISTANCE_CAP =

How far a misspelling may be from a known label before #suggest declines to guess. Two edits catches a transposition or a dropped segment character ("io.nte", "nondet.tim") without proposing an unrelated label for a genuinely new spelling.

2

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(vocabulary_version:, labels:, retired: {}) ⇒ Registry

Returns a new instance of Registry.



83
84
85
86
87
88
89
90
# File 'lib/rigor/effects/registry.rb', line 83

def initialize(vocabulary_version:, labels:, retired: {})
  @vocabulary_version = vocabulary_version
  @labels = labels.map(&:to_s).uniq.sort.freeze
  @known = build_known(@labels)
  @roots = @known.select { |label| Label.parent(label).nil? }.sort.freeze
  @retired = build_retired(retired)
  freeze
end

Instance Attribute Details

#labelsObject (readonly)

Every declared label, sorted. Implied ancestors (email, because email.send is declared) are recognised by #known? but are not rows of the vocabulary and are not listed here.



94
95
96
# File 'lib/rigor/effects/registry.rb', line 94

def labels
  @labels
end

#rootsObject (readonly)

The roots of the vocabulary — the outermost segments #with treats as already owned.



97
98
99
# File 'lib/rigor/effects/registry.rb', line 97

def roots
  @roots
end

#vocabulary_versionObject (readonly)

Returns the value of attribute vocabulary_version.



81
82
83
# File 'lib/rigor/effects/registry.rb', line 81

def vocabulary_version
  @vocabulary_version
end

Class Method Details

.defaultObject

The shared registry as shipped. Memoised: the YAML parse is a once-per-process cost, and nothing consumes the registry yet, so it stays lazy rather than paying at require "rigor".



40
41
42
# File 'lib/rigor/effects/registry.rb', line 40

def self.default
  @default ||= load_file(DATA_PATH)
end

.for_configuration(configuration, plugin_facts: nil) ⇒ Object

The vocabulary one run works in: the shipped registry plus whatever effects.labels: opened (ADR-103 WD2 / #385). The project is the one extender that may open ANY root — listing a label in its own configuration is the vouching act — so this can only fail on a spelling Configuration already rejected at load, and a failure degrades to the shipped vocabulary rather than taking the run down.

Memoised on the label list, because the answer is a frozen value object and every effects surface in a run asks for the same one: the envelope pass, the unknown-label check, the snapshot header.

plugin_facts (#387) folds in every loaded plugin's effect_labels: FIRST, each under its own owner, so that a project's effects.labels: can then name a plugin-opened root and an envelope may bound rails.activejob.enqueue without the project having to re-declare the framework's vocabulary. The plugin layer is not memoised on its own — PluginFacts is already per-process — and the project layer keeps its memo keyed on the pair.



58
59
60
61
62
63
64
65
66
# File 'lib/rigor/effects/registry.rb', line 58

def self.for_configuration(configuration, plugin_facts: nil)
  base = plugin_facts.nil? || plugin_facts.empty? ? default : plugin_facts.extend_registry(default)
  labels = configuration.effects_labels
  return base if labels.nil? || labels.empty?

  (@extended ||= {})[[labels, base.labels]] ||= base.with(labels: labels, owner: nil)
rescue Error
  default
end

.load_file(path) ⇒ Object

Build a registry from a registry-shaped YAML file. Missing or unreadable data degrades to an empty vocabulary rather than raising, matching the built-in catalogues' posture for a bare install that opted data out; every label then reads as unknown, which is fail-open.



71
72
73
74
75
76
77
78
79
# File 'lib/rigor/effects/registry.rb', line 71

def self.load_file(path)
  raw = File.exist?(path) ? YAML.safe_load_file(path) : nil
  raw = {} unless raw.is_a?(Hash)
  new(
    vocabulary_version: raw.fetch("vocabulary", 0),
    labels: raw.fetch("labels", nil) || [],
    retired: raw.fetch("retired", nil) || {}
  )
end

Instance Method Details

#known?(label) ⇒ Boolean

Whether the vocabulary recognises label: an exact row, or an ancestor of one. A declared io is recognised because io.net exists, so a bound may name an interior node the data file never spells out on its own line.

Returns:

  • (Boolean)


102
103
104
# File 'lib/rigor/effects/registry.rb', line 102

def known?(label)
  @known.include?(label)
end

#retired(label) ⇒ Object

The replacement labels for a retired spelling, or nil when the spelling was never retired. A rename or a removal bumps #vocabulary_version and records the old spelling here so a snapshot written by an older Rigor still reads.



128
129
130
# File 'lib/rigor/effects/registry.rb', line 128

def retired(label)
  @retired[label]
end

#suggest(label) ⇒ Object

The nearest recognised label to a misspelling, within SUGGESTION_DISTANCE_CAP edits, or nil when nothing is close enough. A recognised label suggests nothing — ask #known? first.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/rigor/effects/registry.rb', line 109

def suggest(label)
  return nil unless Label.valid?(label)
  return nil if known?(label)

  best = nil
  best_distance = SUGGESTION_DISTANCE_CAP + 1
  @known.each do |candidate|
    distance = levenshtein(label, candidate, best_distance)
    next unless distance < best_distance

    best = candidate
    best_distance = distance
  end
  best
end

#with(labels:, owner:) ⇒ Object

A new registry carrying labels on top of this one.

owner is the identity opening a root: a plugin id, or — for a first-party plugin that models a framework — the framework root it owns. nil is the project, which may open any root. Every added label must either descend from a root this registry already knows or open a root equal to owner.



138
139
140
141
142
143
144
145
146
# File 'lib/rigor/effects/registry.rb', line 138

def with(labels:, owner:)
  added = labels.map(&:to_s)
  added.each do |label|
    raise InvalidLabelError, "not a well-formed effect label: #{label.inspect}" unless Label.valid?(label)

    check_ownership(label, owner)
  end
  self.class.new(vocabulary_version: @vocabulary_version, labels: @labels + added, retired: @retired)
end