Class: RailsAiBridge::PathResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_ai_bridge/path_resolver.rb

Overview

Resolves configured Rails logical paths to filesystem paths without leaking machine-specific absolute paths into generated assistant context.

Architectural role: intentional shared utility

PathResolver is deliberately used by every introspector that needs to locate files on disk (controller, model, view, stimulus, turbo, auth, api, config, action_text, activeStorage, nonArModels — 11 callers as of v3.6.2). It is NOT a god class despite high betweenness centrality in graph analyses: a foundational path-resolution utility is expected to sit at the centre of the introspector graph. Splitting it would spread path-safety logic (traversal guards, safe joins) across multiple files and weaken the single responsibility it currently holds.

The three private helper classes — SafeRelativePath, SafeJoin, and ConfiguredPathsError — are kept nested and private_constant so they cannot leak into the public API or be referenced by callers. This keeps the safety surface area small and auditable in one file.

If a future graph analysis flags this class again, check the betweenness centrality against the caller count before proposing a split: a utility with many callers and a single responsibility is healthy, not smelly.

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ PathResolver

Returns a new instance of PathResolver.

Parameters:

  • app (Rails::Application)

    host Rails application



135
136
137
138
# File 'lib/rails_ai_bridge/path_resolver.rb', line 135

def initialize(app)
  @app = app
  @root = app.root.to_s
end

Instance Method Details

#directories_for(logical_path) ⇒ Array<String>

Resolves directories for a logical Rails path.

Configured app.paths entries are preferred. When none are configured, the conventional root-relative directory is returned.

Parameters:

  • logical_path (String)

    Rails path key, such as "app/models"

Returns:

  • (Array<String>)

    absolute directory paths



147
148
149
150
151
152
# File 'lib/rails_ai_bridge/path_resolver.rb', line 147

def directories_for(logical_path)
  entries = configured_paths_for(logical_path)
  entries = [logical_path] if entries.empty?

  entries.map { |path| File.expand_path(path.to_s, @root) }.uniq
end

#existing_file_for(logical_path, relative_file) ⇒ String?

Finds the first existing file under a logical Rails path.

Missing paths return nil without calling File.realpath. Existing candidates are accepted only when their realpath stays inside the resolved directory or the application root.

Parameters:

  • logical_path (String)

    Rails path key, such as "app/models"

  • relative_file (String)

    file path relative to the resolved directory

Returns:

  • (String, nil)

    absolute file path when found and contained

Raises:

  • (ArgumentError)

    when relative_file is absolute or contains traversal segments



199
200
201
202
203
204
205
206
207
208
# File 'lib/rails_ai_bridge/path_resolver.rb', line 199

def existing_file_for(logical_path, relative_file)
  safe_file = SafeRelativePath.new(relative_file, argument_name: 'relative_file').to_s

  directories_for(logical_path).each do |path|
    candidate = SafeJoin.new(path, safe_file).to_s
    return candidate if contained_existing_path?(candidate, path)
  end

  nil
end

#files_for(logical_path, extension:) ⇒ Array<String>

Finds files under every directory for a logical Rails path.

Results are sorted then truncated to Config::Introspection#max_files_per_path (default 2000).

Parameters:

  • logical_path (String)

    Rails path key, such as "app/models"

  • extension (String)

    file extension without a leading dot

Returns:

  • (Array<String>)

    absolute file paths



162
163
164
# File 'lib/rails_ai_bridge/path_resolver.rb', line 162

def files_for(logical_path, extension:)
  glob_for(logical_path, "**/*.#{extension}")
end

#glob_for(logical_path, pattern) ⇒ Array<String>

Finds files matching a glob under every directory for a logical Rails path.

The pattern must be a safe relative glob. Absolute paths and traversal segments are rejected before Dir.glob runs. Existing matches are kept only when File.realpath stays inside the resolved directory or app root. Matches are then sorted and truncated to Config::Introspection#max_files_per_path (default 2000).

Parameters:

  • logical_path (String)

    Rails path key, such as "app/views"

  • pattern (String)

    glob pattern relative to each resolved directory

Returns:

  • (Array<String>)

    absolute file paths

Raises:

  • (ArgumentError)

    when pattern is absolute or contains traversal segments



178
179
180
181
182
183
184
185
186
187
# File 'lib/rails_ai_bridge/path_resolver.rb', line 178

def glob_for(logical_path, pattern)
  safe_pattern = SafeRelativePath.new(pattern, argument_name: 'pattern').to_s

  matches = directories_for(logical_path).flat_map do |path|
    next [] unless Dir.exist?(path)

    Dir.glob(File.join(path, safe_pattern)).select { |file| contained_existing_path?(file, path) }
  end
  matches.sort.take(max_files_per_path)
end

#logical_file_path(absolute_path, logical_path:) ⇒ String

Converts an absolute file path under a logical Rails path into a stable logical path for generated context.

Parameters:

  • absolute_path (String)

    filesystem path to map

  • logical_path (String)

    Rails path key, such as "app/models"

Returns:

  • (String)

    logical context path, root-relative path, or basename fallback



216
217
218
219
220
221
222
223
# File 'lib/rails_ai_bridge/path_resolver.rb', line 216

def logical_file_path(absolute_path, logical_path:)
  path = File.expand_path(absolute_path.to_s)
  matching_root = matching_root_for(path, logical_path)

  return logical_path_from_root(path, matching_root, logical_path) if matching_root

  relative_to_root(path)
end