Class: LittleGhost::Support::Loader
- Inherits:
-
Object
- Object
- LittleGhost::Support::Loader
- Defined in:
- lib/little_ghost/support/loader.rb
Overview
Loader finds agents and tools from a conventional application layout. Ruby files map to constants by path, so applications can add extension classes without maintaining a manual require list.
loader = LittleGhost::Support::Loader.new(root: Dir.pwd)
loader.setup.eager_load
Setup is process-serialized, collisions are rejected, and real paths are checked for symbolic-link escapes. Application load roots are trusted code, not a sandbox for untrusted files.
Defined Under Namespace
Classes: ConflictError, ExpectedConstantError
Constant Summary collapse
- DEFAULT_DIRECTORIES =
:nodoc:
%w[app/agents app/tools].freeze
- PROCESS_LOCK =
:nodoc:
Monitor.new
Instance Attribute Summary collapse
-
#directories ⇒ Object
readonly
Application root and configured relative load directories.
-
#root ⇒ Object
readonly
Application root and configured relative load directories.
Instance Method Summary collapse
-
#constant(name) ⇒ Object
Loads an application constant after installing autoloads.
-
#eager_load ⇒ Object
Loads every registered constant and verifies its defining file.
-
#fetch(relative_path) ⇒ Object
Finds a relative file or raises LoadError.
-
#find(relative_path) ⇒ Object
Finds a relative file beneath configured paths, returning its resolved path or nil.
-
#glob(pattern) ⇒ Object
Finds resolved paths matching a relative glob without root escapes.
-
#initialize(paths: nil, root: nil, directories: DEFAULT_DIRECTORIES) ⇒ Loader
constructor
Uses explicit
pathsor conventional directories beneathroot. -
#loaded_constant?(name) ⇒ Boolean
Checks whether
namewas loaded from its registered source path. -
#read(relative_path, encoding: "UTF-8") ⇒ Object
Reads a relative file using
encoding. -
#registered_constants ⇒ Object
Copies registered constant names and resolved paths.
-
#setup ⇒ Object
Installs autoloads for the current registry and returns self.
Constructor Details
#initialize(paths: nil, root: nil, directories: DEFAULT_DIRECTORIES) ⇒ Loader
Uses explicit paths or conventional directories beneath root.
26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/little_ghost/support/loader.rb', line 26 def initialize(paths: nil, root: nil, directories: DEFAULT_DIRECTORIES) @root = root && File.(root) @directories = Array(directories).map(&:to_s).freeze if @root && @directories.any? { |directory| Pathname.new(directory).absolute? || directory.split(File::SEPARATOR).include?("..") } raise ArgumentError, "application load directories must stay within root" end roots = paths || @directories.map { |directory| File.join(@root || Dir.pwd, directory) } @paths = Array(roots).map { |path| File.(path) }.uniq.freeze @registry = nil @loaded_constants = {} @setup = false end |
Instance Attribute Details
#directories ⇒ Object (readonly)
Application root and configured relative load directories.
23 24 25 |
# File 'lib/little_ghost/support/loader.rb', line 23 def directories @directories end |
#root ⇒ Object (readonly)
Application root and configured relative load directories.
23 24 25 |
# File 'lib/little_ghost/support/loader.rb', line 23 def root @root end |
Instance Method Details
#constant(name) ⇒ Object
Loads an application constant after installing autoloads.
117 118 119 120 121 122 123 124 125 |
# File 'lib/little_ghost/support/loader.rb', line 117 def constant(name) PROCESS_LOCK.synchronize do setup constantize(name.to_s) end rescue NameError => error raise unless expected_constant_missing?(error, name.to_s) raise ExpectedConstantError, "Unknown application constant: #{name}" end |
#eager_load ⇒ Object
Loads every registered constant and verifies its defining file.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/little_ghost/support/loader.rb', line 60 def eager_load PROCESS_LOCK.synchronize do setup registry.each_key do |constant_name| validate_registered_path!(constant_name) constantize(constant_name) @loaded_constants[constant_name] = registry.fetch(constant_name) rescue NameError => error raise unless expected_constant_missing?(error, constant_name) raise ExpectedConstantError, "#{registry.fetch(constant_name)} must define #{constant_name}" end end self end |
#fetch(relative_path) ⇒ Object
Finds a relative file or raises LoadError.
90 91 92 |
# File 'lib/little_ghost/support/loader.rb', line 90 def fetch(relative_path) find(relative_path) || raise(LoadError, "Could not find #{relative_path} in configured paths") end |
#find(relative_path) ⇒ Object
Finds a relative file beneath configured paths, returning its resolved path or nil.
77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/little_ghost/support/loader.rb', line 77 def find(relative_path) clean = clean_path(relative_path) @paths.each do |path_root| candidate = File.(clean, path_root) next unless inside?(candidate, path_root) next unless File.file?(candidate) real_candidate = File.realpath(candidate) return real_candidate if inside?(real_candidate, real_path_root(path_root)) end nil end |
#glob(pattern) ⇒ Object
Finds resolved paths matching a relative glob without root escapes.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/little_ghost/support/loader.rb', line 100 def glob(pattern) clean = clean_path(pattern) @paths.flat_map do |path_root| next [] unless Dir.exist?(path_root) real_root = real_path_root(path_root) Dir.glob(File.join(path_root, clean)).filter_map do |candidate| = File.(candidate) next unless inside?(, path_root) real_candidate = File.realpath(candidate) real_candidate if inside?(real_candidate, real_root) rescue Errno::ENOENT nil end end.uniq.sort end |
#loaded_constant?(name) ⇒ Boolean
Checks whether name was loaded from its registered source path.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/little_ghost/support/loader.rb', line 133 def loaded_constant?(name) PROCESS_LOCK.synchronize do path = registry[name.to_s] return false unless path names = name.to_s.split("::") leaf = names.pop namespace = names.inject(Object) do |parent, child| break unless parent.const_defined?(child, false) parent.const_get(child, false) end autoload_path = namespace&.autoload?(leaf) return File.realpath(autoload_path) == path if autoload_path location = constant_source_location(name.to_s) location && File.realpath(location.first) == path end rescue Errno::ENOENT false end |
#read(relative_path, encoding: "UTF-8") ⇒ Object
Reads a relative file using encoding.
95 96 97 |
# File 'lib/little_ghost/support/loader.rb', line 95 def read(relative_path, encoding: "UTF-8") File.read(fetch(relative_path), encoding:) end |
#registered_constants ⇒ Object
Copies registered constant names and resolved paths.
128 129 130 |
# File 'lib/little_ghost/support/loader.rb', line 128 def registered_constants registry.dup.freeze end |
#setup ⇒ Object
Installs autoloads for the current registry and returns self.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/little_ghost/support/loader.rb', line 40 def setup PROCESS_LOCK.synchronize do return self if @setup registry.each do |constant_name, path| namespace, name = namespace_for(constant_name) if namespace.const_defined?(name, false) || namespace.autoload?(name) existing = namespace.autoload?(name) next if existing && File.(existing) == path next if !existing && constant_source_location(constant_name)&.then { |location| File.realpath(location.first) == path } raise ConflictError, "#{constant_name} is already defined" end namespace.autoload(name, path) end @setup = true end self end |