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 in app/presenters/user.rb and reopened in lib/patch.rb is a presenter. File.fnmatch? with FNM_PATHNAME, project-relative, so ** is the only way across a directory boundary: the unused --entry-point and effects.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 location carries for a configured envelope. Not a path: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

Class Method Details

.build(entries:, registry:) ⇒ Array<Entry>

Resolves Configuration#effects_envelopes against a registry.

Parameters:

  • entries (Array<Hash>)

    the loaded, shape-validated entries

  • registry (Registry)

    the vocabulary, project extensions included

Returns:



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.

Parameters:

  • entries (Array<Entry>)
  • class_names (Enumerable<String>)

    every class the run collected units for

  • sources (Hash{String => Array<String>}) (defaults to: {})

    Runner#effect_sources{method key => [path]}

  • project_root (String) (defaults to: Dir.pwd)

    what sources paths are relativised against

Returns:

  • (Hash{String => Envelope})

    one envelope per selected class, keyed by class name



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* matches Api::V2);
  • * alone matches exactly one segment — Presenters::* matches Presenters::User and NOT Presenters::Admin::User, nor bare Presenters;
  • ** matches one or more consecutive segments — Presenters::** matches both Presenters::User and Presenters::Admin::User, and still not bare Presenters.

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 **.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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