Module: Mbeditor::RailsRelatedFilesService

Defined in:
app/services/mbeditor/rails_related_files_service.rb

Overview

Given a workspace-relative file path, finds related Rails files grouped by type: controller, model, views, helper, tests, and custom directories.

Only groups with at least one existing file are included in the result. All returned paths are workspace-relative strings.

Class Method Summary collapse

Class Method Details

.find(workspace_root, relative_path, custom_paths: []) ⇒ Object

Returns a hash of groups. Example:

{
  controller: [{path: "app/controllers/users_controller.rb", name: "users_controller.rb"}],
  model:      [{path: "app/models/user.rb",                  name: "user.rb"}],
  views:      [{path: "app/views/users/index.html.erb",      name: "index.html.erb"}],
  helper:     [{path: "app/helpers/users_helper.rb",         name: "users_helper.rb"}],
  tests:      [{path: "test/controllers/users_controller_test.rb", name: "users_controller_test.rb"}],
  custom:     {"app/assets/javascripts/app" => [{path: "...", name: "..."}]}
}

Returns {} when the path does not match any known Rails convention.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
# File 'app/services/mbeditor/rails_related_files_service.rb', line 24

def find(workspace_root, relative_path, custom_paths: [])
  plural, singular = extract_resource_names(relative_path, custom_paths: custom_paths)
  return {} unless plural && singular

  result = {}

  # ── controller ────────────────────────────────────────────────────────────
  controller_path = "app/controllers/#{plural}_controller.rb"
  if file_exists?(workspace_root, controller_path)
    result[:controller] = [entry(controller_path, kind: 'Controller')]
  end

  # ── model ─────────────────────────────────────────────────────────────────
  model_path = "app/models/#{singular}.rb"
  if file_exists?(workspace_root, model_path)
    result[:model] = [entry(model_path, kind: 'Model')]
  end

  # ── views ─────────────────────────────────────────────────────────────────
  views_dir = File.join(workspace_root, "app", "views", plural)
  if File.directory?(views_dir)
    children = dir_children(workspace_root, "app/views/#{plural}", kind: 'View')
    result[:views] = children unless children.empty?
  end

  # ── helper ────────────────────────────────────────────────────────────────
  helper_path = "app/helpers/#{plural}_helper.rb"
  if file_exists?(workspace_root, helper_path)
    result[:helper] = [entry(helper_path, kind: 'Helper')]
  end

  # ── tests ─────────────────────────────────────────────────────────────────
  test_candidates = [
    "test/controllers/#{plural}_controller_test.rb",
    "test/models/#{singular}_test.rb",
    "spec/controllers/#{plural}_controller_spec.rb",
    "spec/models/#{singular}_spec.rb"
  ]
  tests = test_candidates.select { |p| file_exists?(workspace_root, p) }.map { |p| entry(p, kind: 'Test') }
  result[:tests] = tests unless tests.empty?

  # ── concerns ──────────────────────────────────────────────────────────────
  plain_plural   = plural.split('/').last
  plain_singular = singular.split('/').last
  concern_dirs = ['app/models/concerns', 'app/controllers/concerns']
  concern_files = []
  concern_dirs.each do |dir|
    children = dir_children(workspace_root, dir, kind: 'Concern')
    matching = children.select { |c|
      base = File.basename(c[:path], '.*')
      base == plain_plural || base == plain_singular ||
        base.start_with?("#{plain_plural}_") || base.start_with?("#{plain_singular}_") ||
        base.end_with?("_#{plain_plural}") || base.end_with?("_#{plain_singular}")
    }
    concern_files.concat(matching)
  end
  result[:concerns] = concern_files.uniq { |c| c[:path] } unless concern_files.empty?

  # ── custom paths ──────────────────────────────────────────────────────────
  custom_result = {}
  Array(custom_paths).each do |base|
    base = base.to_s.strip
    next if base.empty?

    # Derive a human-readable kind from the last segment of the base path
    custom_kind = base.split('/').last.to_s.tr('_', ' ').split.map(&:capitalize).join(' ')

    [plural, singular].uniq.each do |name|
      rel_dir = "#{base}/#{name}"
      abs_dir = File.join(workspace_root, rel_dir)
      next unless File.directory?(abs_dir)

      begin
        real_ws = File.realpath(workspace_root)
        real_dir = File.realpath(abs_dir)
        next unless real_dir.start_with?("#{real_ws}/") || real_dir == real_ws
      rescue Errno::ENOENT, Errno::EACCES
        next
      end

      children = dir_children(workspace_root, rel_dir, kind: custom_kind)
      next if children.empty?

      custom_result[base] ||= []
      custom_result[base].concat(children)
    end
  end
  result[:custom] = custom_result unless custom_result.empty?

  result
end