Class: Ace::Support::Fs::Atoms::PathExpander

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/support/fs/atoms/path_expander.rb

Overview

Path expansion and resolution with automatic context inference

Supports:

  • Instance-based API for context-aware resolution

  • Protocol URIs (wfi://, guide://, tmpl://, task://, prompt://)

  • Source-relative paths (./, ../)

  • Project-relative paths (no prefix)

  • Environment variables ($VAR, $VAR)

  • Backward compatible class methods for utilities

Constant Summary collapse

PROTOCOL_PATTERN =

Protocol pattern for URI detection

%r{^[a-z][a-z0-9+.-]*://}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source_dir:, project_root:) ⇒ PathExpander

Initialize with explicit context

Parameters:

  • source_dir (String)

    Source document directory (REQUIRED)

  • project_root (String)

    Project root directory (REQUIRED)

Raises:

  • (ArgumentError)

    if either parameter is nil



76
77
78
79
80
81
82
83
84
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 76

def initialize(source_dir:, project_root:)
  if source_dir.nil? || project_root.nil?
    raise ArgumentError, "PathExpander requires both 'source_dir' and 'project_root' " \
                         "(got source_dir: #{source_dir.inspect}, project_root: #{project_root.inspect})"
  end

  @source_dir = source_dir
  @project_root = project_root
end

Instance Attribute Details

#project_rootObject (readonly)

Instance attributes



23
24
25
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 23

def project_root
  @project_root
end

#source_dirObject (readonly)

Instance attributes



23
24
25
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 23

def source_dir
  @source_dir
end

Class Method Details

.absolute?(path) ⇒ Boolean

Check if path is absolute

Parameters:

  • path (String)

    Path to check

Returns:

  • (Boolean)

    true if absolute path



222
223
224
225
226
227
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 222

def self.absolute?(path)
  return false if path.nil?

  path_str = path.to_s
  Pathname.new(path_str).absolute?
end

.basename(path, suffix = nil) ⇒ String

Get base name from path

Parameters:

  • path (String)

    File path

Returns:

  • (String)

    Base name



208
209
210
211
212
213
214
215
216
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 208

def self.basename(path, suffix = nil)
  return nil if path.nil?

  if suffix
    File.basename(path.to_s, suffix)
  else
    File.basename(path.to_s)
  end
end

.class_get_env(var_name) ⇒ String?

Access environment variable by name (class-level) Extracted to allow test stubbing without modifying global ENV

Parameters:

  • var_name (String)

    Environment variable name

Returns:

  • (String, nil)

    Environment variable value or nil if not set



179
180
181
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 179

def self.class_get_env(var_name)
  ENV[var_name]
end

.dirname(path) ⇒ String

Get directory name from path

Parameters:

  • path (String)

    File path

Returns:

  • (String)

    Directory path



198
199
200
201
202
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 198

def self.dirname(path)
  return nil if path.nil?

  File.dirname(path.to_s)
end

.expand(path) ⇒ String

Expand path with tilde and environment variables Legacy stateless method for backward compatibility

Parameters:

  • path (String)

    Path to expand

Returns:

  • (String)

    Expanded absolute path



160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 160

def self.expand(path)
  return nil if path.nil?

  expanded = path.to_s.dup

  # Expand environment variables (uses class_get_env for testability)
  expanded.gsub!(/\$([A-Z_][A-Z0-9_]*)/i) do |match|
    class_get_env(match[1..-1]) || match
  end

  # Expand tilde
  File.expand_path(expanded)
end

.for_cli(project_root: nil) ⇒ PathExpander

Create expander for CLI context (no source file) Uses current directory as source_dir

Parameters:

  • project_root (String, nil) (defaults to: nil)

    Optional explicit project root

Returns:



60
61
62
63
64
65
66
67
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 60

def self.for_cli(project_root: nil)
  resolved_root = project_root || Dir.pwd

  new(
    source_dir: Dir.pwd,
    project_root: resolved_root
  )
end

.for_file(source_file, project_root: nil) ⇒ PathExpander

Create expander for a source file (config, workflow, template, prompt) Automatically infers source_dir, uses provided or default project_root

Parameters:

  • source_file (String)

    Path to source file

  • project_root (String, nil) (defaults to: nil)

    Optional explicit project root (recommended)

Returns:



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 43

def self.for_file(source_file, project_root: nil)
  expanded_source = File.expand_path(source_file)
  source_dir = File.dirname(expanded_source)

  # Use provided project_root or fall back to current directory
  # Note: For full project root detection, caller should use
  # Ace::Support::Fs::Molecules::ProjectRootFinder and pass the result
  resolved_root = project_root || Dir.pwd

  new(source_dir: source_dir, project_root: resolved_root)
end

.join(*parts) ⇒ String

Join path components safely

Parameters:

  • parts (Array<String>)

    Path parts to join

Returns:

  • (String)

    Joined path



187
188
189
190
191
192
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 187

def self.join(*parts)
  parts = parts.flatten.compact.map(&:to_s)
  return "" if parts.empty?

  File.join(*parts)
end

.normalize(path) ⇒ String

Normalize path (remove .., ., duplicates slashes)

Parameters:

  • path (String)

    Path to normalize

Returns:

  • (String)

    Normalized path



250
251
252
253
254
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 250

def self.normalize(path)
  return nil if path.nil?

  Pathname.new(path.to_s).cleanpath.to_s
end

.protocol?(path) ⇒ Boolean

Check if path is a protocol URI

Parameters:

  • path (String)

    Path to check

Returns:

  • (Boolean)

    true if protocol format detected



123
124
125
126
127
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 123

def self.protocol?(path)
  return false if path.nil? || path.empty?

  !!(path.to_s =~ PROTOCOL_PATTERN)
end

.protocol_resolverObject?

Get the current protocol resolver (thread-safe)

Returns:

  • (Object, nil)

    Current resolver or nil



141
142
143
144
145
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 141

def self.protocol_resolver
  protocol_resolver_mutex.synchronize do
    @protocol_resolver
  end
end

.register_protocol_resolver(resolver) ⇒ Object

Register a protocol resolver (e.g., ace-nav) Thread-safe registration using mutex.

Parameters:

  • resolver (Object)

    Resolver responding to #resolve(uri)



133
134
135
136
137
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 133

def self.register_protocol_resolver(resolver)
  protocol_resolver_mutex.synchronize do
    @protocol_resolver = resolver
  end
end

.relative(path, base) ⇒ String

Make path relative to base

Parameters:

  • path (String)

    Path to make relative

  • base (String)

    Base path

Returns:

  • (String)

    Relative path



234
235
236
237
238
239
240
241
242
243
244
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 234

def self.relative(path, base)
  return nil if path.nil? || base.nil?

  path_obj = Pathname.new(expand(path))
  base_obj = Pathname.new(expand(base))

  path_obj.relative_path_from(base_obj).to_s
rescue ArgumentError
  # Paths are on different drives or one is relative
  path
end

.reset_protocol_resolver!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Reset protocol resolver (for testing)



149
150
151
152
153
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 149

def self.reset_protocol_resolver!
  protocol_resolver_mutex.synchronize do
    @protocol_resolver = nil
  end
end

Instance Method Details

#resolve(path) ⇒ String

Resolve path using instance context Handles: protocols, source-relative (./), project-relative, env vars, absolute

Parameters:

  • path (String)

    Path to resolve

Returns:

  • (String)

    Resolved absolute path

Raises:

  • (PathError)

    When protocol cannot be resolved



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ace/support/fs/atoms/path_expander.rb', line 92

def resolve(path)
  return nil if path.nil? || path.empty?

  path_str = path.to_s

  # Check for protocol URIs first
  if self.class.protocol?(path_str)
    return resolve_protocol(path_str)
  end

  # Expand environment variables
  expanded = expand_env_vars(path_str)

  # Handle absolute paths
  return File.expand_path(expanded) if Pathname.new(expanded).absolute?

  # Handle source-relative paths (./ or ../)
  if expanded.start_with?("./", "../")
    return File.expand_path(expanded, @source_dir)
  end

  # Default: project-relative paths
  File.expand_path(expanded, @project_root)
end