Class: Rigor::Plugin::Loader

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/plugin/loader.rb,
sig/rigor/plugin/loader.rbs

Overview

Resolves the project's .rigor.yml plugins: entries into instantiated plugin instances. The other Plugin namespaces (Services, Registry, Base, etc.) are not yet sig-covered, so reference them as untyped for now. The critical declaration here is self.load: without it, Steep matches against Kernel#load's (::String, ?(::Module | bool)) -> bool signature and reports MethodParameterMismatch for the keyword-only shape.

Constant Summary collapse

FEATURE_RESOLVER =

#194 slice 1 — resolve the file that satisfied require <gem> by scanning $LOADED_FEATURES for the entry ending in /<gem>.rb, LAST match preferred. Provenance-agnostic: it pins the actual file for a Bundler path gem, an installed gem, and a -I-injected checkout alike, and — because the feature is already present from the first load — it still resolves when require no-ops on a repeat in-process load (ADR-88 WD4b). Returns nil, never raises, in the degenerate cases: a gem whose entry-file basename differs from the gem name, and a spec's fake requirer that never populated $LOADED_FEATURES.

lambda do |gem_name|
  suffix = "/#{gem_name}.rb"
  $LOADED_FEATURES.rfind { |feature| feature.end_with?(suffix) }
end
ENGINE_ROOT =

#194 slice 2 (ADR-93 WD5) — the engine's own root, anchored from THIS file's location: the loader lives at <root>/lib/rigor/plugin/loader.rb, so three levels up is the engine root. It resolves identically in a git checkout and inside an installed rigortype gem, because the gem packages the plugins/ tree at the same relative path — which is exactly what makes it a trustworthy anchor for the engine's own bundled plugin copies.

File.expand_path("../../..", __dir__)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(services:, requirer: ->(name) { require name }, feature_resolver: FEATURE_RESOLVER) ⇒ Loader

Returns a new instance of Loader.

Parameters:

  • services (Rigor::Plugin::Services)
  • requirer (#call) (defaults to: ->(name) { require name })

    takes a gem name OR an absolute file path (#194 slice 2 — a bundled plugin is required by its bundled_plugin_path) and returns truthy on successful require. Defaulted to Kernel.require via a lambda, which accepts both forms; the spec injects a fake to avoid touching the real load path.

  • feature_resolver (#call) (defaults to: FEATURE_RESOLVER)

    takes a gem name and returns the absolute path require resolved it to (or nil). Defaulted to FEATURE_RESOLVER; the spec injects a fake so it never has to mutate the real $LOADED_FEATURES global.

  • services: (Object)
  • requirer: (^(String) -> untyped) (defaults to: ->(name) { require name })


54
55
56
57
58
# File 'lib/rigor/plugin/loader.rb', line 54

def initialize(services:, requirer: ->(name) { require name }, feature_resolver: FEATURE_RESOLVER)
  @services = services
  @requirer = requirer
  @feature_resolver = feature_resolver
end

Instance Attribute Details

#feature_resolverObject (readonly)

rubocop:disable Metrics/ClassLength



26
27
28
# File 'lib/rigor/plugin/loader.rb', line 26

def feature_resolver
  @feature_resolver
end

#requirer^(String) -> untyped (readonly)

rubocop:disable Metrics/ClassLength

Returns:

  • (^(String) -> untyped)


26
27
28
# File 'lib/rigor/plugin/loader.rb', line 26

def requirer
  @requirer
end

#servicesObject (readonly)

rubocop:disable Metrics/ClassLength

Returns:

  • (Object)


26
27
28
# File 'lib/rigor/plugin/loader.rb', line 26

def services
  @services
end

Class Method Details

.bundled_plugin_path(gem_name) ⇒ Object

#194 slice 2 (ADR-93 WD5) — the absolute path of the engine's OWN bundled copy of a plugin gem (<ENGINE_ROOT>/plugins/<gem>/lib/<gem>.rb), or nil when the engine does not bundle it. A bundled plugin is required by this path rather than by gem name, so a stale installed rigortype gem can never displace the engine's own versioned copy through RubyGems name resolution — the engine and its bundled plugins are versioned together. Returns nil for a third-party / project-bundle plugin and for a trimmed packaging (the ADR-27 single-binary target that ships no plugins/ tree), where the caller falls back to today's gem-name require so no install mode regresses. The rule is uniform across the auto-wired rigor-rbs-inline default and every user-listed entry, because the skew mechanism is identical for both. Also read by rigor doctor's skew check (#194 slice 3) to decide which loaded plugins the engine bundles.



74
75
76
77
# File 'lib/rigor/plugin/loader.rb', line 74

def self.bundled_plugin_path(gem_name)
  path = File.join(ENGINE_ROOT, "plugins", gem_name, "lib", "#{gem_name}.rb")
  File.file?(path) ? path : nil
end

.load(configuration:, services:, requirer: ->(name) { require name }, feature_resolver: FEATURE_RESOLVER) ⇒ Object

Parameters:

  • configuration: (Configuration)
  • services: (Object)
  • requirer: (^(String) -> untyped) (defaults to: ->(name) { require name })

Returns:

  • (Object)


60
61
62
# File 'lib/rigor/plugin/loader.rb', line 60

def self.load(configuration:, services:, requirer: ->(name) { require name }, feature_resolver: FEATURE_RESOLVER)
  new(services: services, requirer: requirer, feature_resolver: feature_resolver).load(configuration.plugins)
end

Instance Method Details

#load(entries) ⇒ Registry

Parameters:

  • entries (Array<String, Hash>)

    the raw plugins: list from the configuration.

Returns:



81
82
83
84
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
110
111
112
113
114
115
116
117
118
# File 'lib/rigor/plugin/loader.rb', line 81

def load(entries)
  plugins = []
  load_errors = []
  seen_ids = {}
  # #194 slice 1 — `gem name => resolved file path` for every gem the loader successfully required.
  # A frozen plugin instance (e.g. `rigor-rbs-inline` self-freezes per ADR-32) can't carry the path,
  # so it rides on the Registry keyed by gem name; the `rigor plugins` loaded row reads it back.
  @resolved_gem_paths = {}

  Array(entries).each_with_index do |raw, index|
    entry = normalise_entry(raw, index)
  rescue LoadError => e
    load_errors << e
  else
    # ADR-93 WD2 — `enabled: false` opts a listed plugin out entirely: it is neither required nor
    # trusted nor id-checked. This is the project-level opt-out for the auto-wired `rigor-rbs-inline`
    # default (a user re-lists it with `enabled: false`), but it works for any entry.
    next unless entry[:enabled]

    begin
      plugin = resolve_and_instantiate(entry, seen_ids)
      plugins << plugin if plugin
    rescue LoadError => e
      load_errors << e
    end
  end

  # ADR-9 slice 5 — topological sort by `manifest(consumes:)` so producers run before consumers, plus
  # early `missing-producer` validation. Cycles surface as `dependency-cycle` LoadErrors. When
  # validation fails, the offending plugin(s) drop from the returned plugins list and the LoadError
  # surfaces alongside any earlier failure.
  plugins, sort_errors = topo_sort_plugins(plugins)
  load_errors.concat(sort_errors)

  blueprints = plugins.map { |plugin| Blueprint.new(klass_name: plugin.class.name, config: plugin.config) }
  Registry.new(plugins: plugins, blueprints: blueprints, load_errors: load_errors,
               resolved_gem_paths: @resolved_gem_paths)
end