Module: Rigor::Effects::ConfigEnvelopes
- Defined in:
- lib/rigor/effects/config_envelopes.rb
Overview
Envelopes by convention — the effects.envelopes: block of .rigor.yml (ADR-103 WD5 (2);
design note § 6.2).
effects:
envelopes:
- match: "app/presenters/**/*.rb" # File.fnmatch, project-relative — the ADR-28 shape
effect: [] # the empty envelope: `pure`
- namespace: "Policies::*"
effect: [mutate.local]
This is the surface that pays on day one for a project that writes no RBS: one stanza bounds a whole
architectural layer. An entry attaches an envelope to every method of every class it selects and
distributes exactly as a class-level annotation does — reopenings and synthesised attr_* /
define_method members included, never subclasses (a subclass matches only if it matches on its own
account).
Selection
match:selects by the class's defining file. A class matches when any file that defines a method of it matches the glob — a class opened inapp/presenters/user.rband reopened inlib/patch.rbis a presenter.File.fnmatch?withFNM_PATHNAME, project-relative, so**is the only way across a directory boundary: theunused --entry-pointandeffects.snapshot.reach:semantics, spelled once (ConfigEnvelopes.path_match?).namespace:selects by the class's fully-qualified name, segment by segment (ConfigEnvelopes.namespace_match?).
Precedence
Nearest wins, and configuration is the furthest thing from the method:
per-method annotation > class-level annotation > config entry
Among config entries, the first matching entry in file order wins — a list is read top to bottom, and a later entry never silently overrides one an author put above it. There is no merging: one method has at most one envelope, from exactly one source.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- CONFIG_PATH =
What
locationcarries for a configured envelope. Not apath:line— the loader cannot say which line the entry was written on — so it names the key path instead, which is what a reader greps for. ".rigor.yml"
Class Method Summary collapse
-
.build(entries:, registry:) ⇒ Array<Entry>
Resolves
Configuration#effects_envelopesagainst a registry. -
.envelope_for(entry, class_name) ⇒ Object
The Envelope an entry puts on one class.
-
.for_classes(entries:, class_names:, sources: {}, project_root: Dir.pwd) ⇒ Hash{String => Envelope}
The class-level envelopes the entries put on a project.
-
.namespace_match?(glob, class_name) ⇒ Boolean
A constant-path glob, matched segment by segment over the
::-separated FQN:. -
.path_match?(glob, path) ⇒ Boolean
File.fnmatch?withFNM_PATHNAMEover a project-relative path — the ADR-28path_globshape and theunused --entry-pointone.
Class Method Details
.build(entries:, registry:) ⇒ Array<Entry>
Resolves Configuration#effects_envelopes against a registry.
75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/rigor/effects/config_envelopes.rb', line 75 def build(entries:, registry:) entries.each_with_index.map do |entry, index| labels = Array(entry["effect"]).map(&:to_s) unknown = labels.reject { |label| registry.known?(label) } Entry.new( index: index, match: entry["match"], namespace: entry["namespace"], bound: unknown.empty? ? LabelSet.new(labels) : LabelSet::TOP, labels: labels.freeze, unknown_labels: unknown.freeze ) end.freeze end |
.envelope_for(entry, class_name) ⇒ Object
The Envelope an entry puts on one class. Public because EnvelopeIndex resolves the same
entries per call site rather than per project class (for_classes's shape), and the two
must build the identical value: a bound that read differently at a call site and at the def
would make the ≤ lane disagree with the check that enforces it.
110 111 112 113 114 115 116 |
# File 'lib/rigor/effects/config_envelopes.rb', line 110 def envelope_for(entry, class_name) Envelope.build( owner_key: class_name, bound: entry.bound, source: Envelope::CONFIG_SOURCE, location: entry.location, spelling: entry.spelling, unknown_labels: entry.unknown_labels, declared_labels: entry.labels ) end |
.for_classes(entries:, class_names:, sources: {}, project_root: Dir.pwd) ⇒ Hash{String => Envelope}
The class-level envelopes the entries put on a project.
94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rigor/effects/config_envelopes.rb', line 94 def for_classes(entries:, class_names:, sources: {}, project_root: Dir.pwd) return NO_ENVELOPES if entries.empty? files = files_by_class(sources, project_root) class_names.each_with_object({}) do |class_name, out| entry = entries.find { |candidate| selects?(candidate, class_name, files[class_name]) } next if entry.nil? out[class_name] = envelope_for(entry, class_name) end end |
.namespace_match?(glob, class_name) ⇒ Boolean
A constant-path glob, matched segment by segment over the ::-separated FQN:
- a literal segment matches itself, and
*inside one matches any run of characters within that segment (Api::V*matchesApi::V2); *alone matches exactly one segment —Presenters::*matchesPresenters::Userand NOTPresenters::Admin::User, nor barePresenters;**matches one or more consecutive segments —Presenters::**matches bothPresenters::UserandPresenters::Admin::User, and still not barePresenters.
Deliberately not File.fnmatch over a /-substituted name: the semantics above are the ones the
documentation states, and borrowing a path matcher would make them depend on how one library
happens to treat a trailing **.
143 144 145 |
# File 'lib/rigor/effects/config_envelopes.rb', line 143 def namespace_match?(glob, class_name) match_segments?(glob.to_s.split("::"), class_name.to_s.split("::")) end |
.path_match?(glob, path) ⇒ Boolean
File.fnmatch? with FNM_PATHNAME over a project-relative path — the ADR-28 path_glob shape
and the unused --entry-point one. FNM_PATHNAME is what makes app/*/x.rb stop at one
directory and ** the only way past it.
127 128 129 |
# File 'lib/rigor/effects/config_envelopes.rb', line 127 def path_match?(glob, path) File.fnmatch?(glob, path, File::FNM_PATHNAME) end |