class LittleGhost::Support::Loader
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.
Attributes
Application root and configured relative load directories.
Application root and configured relative load directories.
Public Class Methods
# File lib/little_ghost/support/loader.rb, line 26 def initialize(paths: nil, root: nil, directories: DEFAULT_DIRECTORIES) @root = root && File.expand_path(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.expand_path(path) }.uniq.freeze @registry = nil @loaded_constants = {} @setup = false end
Uses explicit paths or conventional directories beneath root.
Public Instance Methods
Source
# 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
Loads an application constant after installing autoloads.
Source
# 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
Loads every registered constant and verifies its defining file.
Source
# 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
Finds a relative file or raises LoadError.
Source
# 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.expand_path(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
Finds a relative file beneath configured paths, returning its resolved path or nil.
Source
# 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| expanded = File.expand_path(candidate) next unless inside?(expanded, path_root) real_candidate = File.realpath(candidate) real_candidate if inside?(real_candidate, real_root) rescue Errno::ENOENT nil end end.uniq.sort end
Finds resolved paths matching a relative glob without root escapes.
Source
# 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
Checks whether name was loaded from its registered source path.
# File lib/little_ghost/support/loader.rb, line 95 def read(relative_path, encoding: "UTF-8") File.read(fetch(relative_path), encoding:) end
Reads a relative file using encoding.
Source
# File lib/little_ghost/support/loader.rb, line 128 def registered_constants registry.dup.freeze end
Copies registered constant names and resolved paths.
Source
# 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.expand_path(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
Installs autoloads for the current registry and returns self.