tng 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "extract_methods"
4
+ require_relative "file_type_detector"
4
5
 
5
6
  module Tng
6
7
  module Services
@@ -16,281 +17,79 @@ module Tng
16
17
  end
17
18
 
18
19
  def run
19
- type = normalize_type(@params[:type])
20
- file_name = @params[:file]
21
- method_name = @params[:method]
20
+ file_path, method_name = @params[:file], @params[:method]
22
21
 
23
- unless type && file_name && method_name
24
- puts @pastel.red("❌ All parameters are required for direct generation: --type, --file, --method")
25
- puts @pastel.yellow("Usage: bundle exec tng --type=controller --file=users --method=index")
26
- puts @pastel.yellow("Shortcuts: bundle exec tng -t=c -f=users -m=index")
22
+ unless file_path && method_name
23
+ puts @pastel.red("❌ Both file and method parameters are required")
24
+ puts @pastel.yellow("Usage: bundle exec tng app/controllers/users_controller.rb index")
25
+ puts @pastel.yellow(" or: bundle exec tng --file=users_controller.rb --method=index")
26
+ puts @pastel.yellow(" or: bundle exec tng f=users_controller.rb m=index")
27
27
  return
28
28
  end
29
29
 
30
- unless %w[controller model service other].include?(type)
31
- puts @pastel.red("❌ Invalid type: #{@params[:type]}")
32
- puts @pastel.yellow("Supported types: controller, model, service, other")
33
- return
34
- end
35
-
36
- case type
37
- when "controller"
38
- run_controller_method_generation(file_name, method_name)
39
- when "model"
40
- run_model_method_generation(file_name, method_name)
41
- when "service"
42
- run_service_method_generation(file_name, method_name)
43
- when "other"
44
- run_other_method_generation(file_name, method_name)
45
- end
46
- end
47
-
48
- def normalize_type(type_param)
49
- case type_param&.downcase
50
- when "c", "controller"
51
- "controller"
52
- when "m", "mo", "model"
53
- "model"
54
- when "s", "se", "service"
55
- "service"
56
- when "o", "other"
57
- "other"
58
- else
59
- type_param&.downcase
60
- end
61
- end
62
-
63
- def run_controller_method_generation(file_name, method_name)
64
- controllers = Tng::Analyzers::Controller.files_in_dir("app/controllers").map do |file|
65
- relative_path = file[:path].gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
66
- namespaced_name = relative_path.split("/").map(&:camelize).join("::")
67
-
68
- {
69
- name: namespaced_name,
70
- path: file[:path]
71
- }
72
- end
30
+ resolved_path = FileTypeDetector.resolve_file_path(file_path)
73
31
 
74
- controller = find_matching_controller(controllers, file_name)
75
-
76
- unless controller
77
- puts @pastel.red("❌ Controller not found: #{file_name}")
78
- puts @pastel.yellow("Available controllers:")
79
- controllers.first(5).each do |ctrl|
80
- puts @pastel.dim(" - #{ctrl[:name]}")
81
- end
82
- puts @pastel.dim(" ... and #{controllers.length - 5} more") if controllers.length > 5
83
- return
84
- end
85
-
86
- methods = extract_controller_methods(controller)
87
-
88
- if methods.empty?
89
- puts @pastel.yellow("⚠️ No methods found in #{controller[:name]}")
90
- return
91
- end
92
-
93
- method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
94
-
95
- unless method_info
96
- puts @pastel.red("❌ Method '#{method_name}' not found in #{controller[:name]}")
97
- puts @pastel.yellow("Available methods:")
98
- methods.first(10).each do |method|
99
- puts @pastel.dim(" - #{method[:name]}")
100
- end
101
- puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
32
+ unless resolved_path && File.exist?(resolved_path)
33
+ puts @pastel.red("❌ File not found: #{file_path}")
34
+ suggest_similar_files(file_path)
102
35
  return
103
36
  end
104
37
 
105
- puts @pastel.bright_white("🎯 Generating test for #{controller[:name]}##{method_info[:name]}...")
106
-
107
- result = Tng::Services::TestGenerator.new(@http_client).run_for_controller_method(controller, method_info)
108
-
109
- if result && result[:file_path]
110
- @show_post_generation_menu.call(result)
111
- else
112
- puts @pastel.red("❌ Failed to generate test")
113
- end
114
- end
115
-
116
- def find_matching_controller(controllers, file_name)
117
- controllers.find do |controller|
118
- controller[:name].downcase == file_name.downcase ||
119
- controller[:name].split("::").last.downcase == file_name.downcase ||
120
- File.basename(controller[:path], ".rb").downcase == file_name.downcase ||
121
- File.basename(controller[:path], ".rb").gsub("_controller", "").downcase == file_name.downcase ||
122
- File.basename(controller[:path], ".rb").downcase == "#{file_name}_controller".downcase
123
- end
38
+ type = FileTypeDetector.detect_type(resolved_path)
39
+ generate_test_for_file(resolved_path, method_name, type)
124
40
  end
125
41
 
126
- def run_model_method_generation(file_name, method_name)
127
- models = Tng::Analyzers::Model.files_in_dir("app/models").map do |file|
128
- relative_path = file[:path].gsub(%r{^.*app/models/}, "").gsub(".rb", "")
129
- namespaced_name = relative_path.split("/").map(&:camelize).join("::")
42
+ private
130
43
 
131
- {
132
- name: namespaced_name,
133
- path: file[:path]
134
- }
135
- end
44
+ def suggest_similar_files(file_path)
45
+ base_name = File.basename(file_path, '.rb')
46
+ puts @pastel.yellow("💡 Did you mean one of these?")
136
47
 
137
- model = find_matching_model(models, file_name)
48
+ similar_files = find_similar_files(base_name)
138
49
 
139
- unless model
140
- puts @pastel.red(" Model not found: #{file_name}")
141
- puts @pastel.yellow("Available models:")
142
- models.first(5).each do |mdl|
143
- puts @pastel.dim(" - #{mdl[:name]}")
144
- end
145
- puts @pastel.dim(" ... and #{models.length - 5} more") if models.length > 5
146
- return
147
- end
148
-
149
- methods = extract_model_methods(model)
150
-
151
- if methods.empty?
152
- puts @pastel.yellow("⚠️ No methods found in #{model[:name]}")
153
- return
154
- end
155
-
156
- method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
157
-
158
- unless method_info
159
- puts @pastel.red("❌ Method '#{method_name}' not found in #{model[:name]}")
160
- puts @pastel.yellow("Available methods:")
161
- methods.first(10).each do |method|
162
- puts @pastel.dim(" - #{method[:name]}")
163
- end
164
- puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
165
- return
166
- end
167
-
168
- puts @pastel.bright_white("🎯 Generating test for #{model[:name]}##{method_info[:name]}...")
169
-
170
- result = Tng::Services::TestGenerator.new(@http_client).run_for_model_method(model, method_info)
171
-
172
- if result && result[:file_path]
173
- @show_post_generation_menu.call(result)
50
+ if similar_files.empty?
51
+ puts @pastel.dim(" No similar files found")
174
52
  else
175
- puts @pastel.red(" Failed to generate test")
176
- end
177
- end
178
-
179
- def find_matching_model(models, file_name)
180
- models.find do |model|
181
- model[:name].downcase == file_name.downcase ||
182
- model[:name].split("::").last.downcase == file_name.downcase ||
183
- File.basename(model[:path], ".rb").downcase == file_name.downcase
53
+ similar_files.first(5).each { |file| puts @pastel.dim(" #{file}") }
184
54
  end
185
55
  end
186
56
 
187
- def run_service_method_generation(file_name, method_name)
188
- services = Tng::Analyzers::Service.files_in_dir.map do |file|
189
- relative_path = file[:path].gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
190
- namespaced_name = relative_path.split("/").map(&:camelize).join("::")
57
+ def find_similar_files(base_name)
58
+ rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
59
+ similar_files = []
191
60
 
192
- {
193
- name: namespaced_name,
194
- path: file[:path]
195
- }
196
- end
61
+ %w[app/controllers app/models app/services app/service].each do |dir|
62
+ next unless Dir.exist?(File.join(rails_root, dir))
197
63
 
198
- service = find_matching_service(services, file_name)
199
-
200
- unless service
201
- puts @pastel.red("❌ Service not found: #{file_name}")
202
- puts @pastel.yellow("Available services:")
203
- services.first(5).each do |svc|
204
- puts @pastel.dim(" - #{svc[:name]}")
64
+ Dir.glob(File.join(rails_root, dir, '**', "*#{base_name}*.rb")).each do |file|
65
+ similar_files << file.gsub(/^#{Regexp.escape(rails_root)}\//, '')
205
66
  end
206
- puts @pastel.dim(" ... and #{services.length - 5} more") if services.length > 5
207
- return
208
- end
209
-
210
- methods = extract_service_methods(service)
211
-
212
- if methods.empty?
213
- puts @pastel.yellow("⚠️ No methods found in #{service[:name]}")
214
- return
215
67
  end
216
68
 
217
- method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
218
-
219
- unless method_info
220
- puts @pastel.red("❌ Method '#{method_name}' not found in #{service[:name]}")
221
- puts @pastel.yellow("Available methods:")
222
- methods.first(10).each do |method|
223
- puts @pastel.dim(" - #{method[:name]}")
224
- end
225
- puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
226
- return
227
- end
228
-
229
- puts @pastel.bright_white("🎯 Generating test for #{service[:name]}##{method_info[:name]}...")
230
-
231
- result = Tng::Services::TestGenerator.new(@http_client).run_for_service_method(service, method_info)
232
-
233
- if result && result[:file_path]
234
- @show_post_generation_menu.call(result)
235
- else
236
- puts @pastel.red("❌ Failed to generate test")
237
- end
238
- end
239
-
240
- def find_matching_service(services, file_name)
241
- services.find do |service|
242
- service[:name].downcase == file_name.downcase ||
243
- service[:name].split("::").last.downcase == file_name.downcase ||
244
- File.basename(service[:path], ".rb").downcase == file_name.downcase
245
- end
69
+ similar_files
246
70
  end
247
71
 
248
- def run_other_method_generation(file_name, method_name)
249
- other_files = Tng::Analyzers::Other.files_in_dir.map do |file|
250
- relative_path = file[:relative_path].gsub(".rb", "")
251
- namespaced_name = relative_path.split("/").map(&:camelize).join("::")
252
-
253
- {
254
- name: namespaced_name,
255
- path: file[:path],
256
- type: file[:type]
257
- }
258
- end
259
-
260
- other_file = find_matching_other_file(other_files, file_name)
261
-
262
- unless other_file
263
- puts @pastel.red("❌ Other file not found: #{file_name}")
264
- puts @pastel.yellow("Available other files:")
265
- other_files.first(5).each do |file|
266
- puts @pastel.dim(" - #{file[:name]}")
267
- end
268
- puts @pastel.dim(" ... and #{other_files.length - 5} more") if other_files.length > 5
269
- return
270
- end
271
-
272
- methods = extract_other_methods(other_file)
72
+ def generate_test_for_file(file_path, method_name, type)
73
+ file_object = FileTypeDetector.build_file_object(file_path, type)
74
+ methods = extract_methods_for_type(file_object, type)
273
75
 
274
76
  if methods.empty?
275
- puts @pastel.yellow("⚠️ No methods found in #{other_file[:name]}")
77
+ puts @pastel.yellow("⚠️ No methods found in #{file_object[:name]}")
276
78
  return
277
79
  end
278
80
 
279
- method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
81
+ method_info = methods.find { |m| m[:name].downcase == method_name.downcase }
280
82
 
281
83
  unless method_info
282
- puts @pastel.red("❌ Method '#{method_name}' not found in #{other_file[:name]}")
84
+ puts @pastel.red("❌ Method '#{method_name}' not found in #{file_object[:name]}")
283
85
  puts @pastel.yellow("Available methods:")
284
- methods.first(10).each do |method|
285
- puts @pastel.dim(" - #{method[:name]}")
286
- end
287
- puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
86
+ methods.first(10).each { |m| puts @pastel.dim(" • #{m[:name]}") }
288
87
  return
289
88
  end
290
89
 
291
- puts @pastel.bright_white("🎯 Generating test for #{other_file[:name]}##{method_info[:name]}...")
90
+ puts @pastel.bright_white("🎯 Generating test for #{file_object[:name]}##{method_info[:name]}...")
292
91
 
293
- result = Tng::Services::TestGenerator.new(@http_client).run_for_other_method(other_file, method_info)
92
+ result = generate_test_result(file_object, method_info, type)
294
93
 
295
94
  if result && result[:file_path]
296
95
  @show_post_generation_menu.call(result)
@@ -299,21 +98,24 @@ module Tng
299
98
  end
300
99
  end
301
100
 
302
- def find_matching_other_file(other_files, file_name)
303
- other_files.find do |file|
304
- file[:name].downcase == file_name.downcase ||
305
- file[:name].split("::").last.downcase == file_name.downcase ||
306
- File.basename(file[:path], ".rb").downcase == file_name.downcase ||
307
- normalize_other_file_path(file[:path]).downcase == file_name.downcase
101
+ def extract_methods_for_type(file_object, type)
102
+ case type
103
+ when "controller" then extract_controller_methods(file_object)
104
+ when "model" then extract_model_methods(file_object)
105
+ when "service" then extract_service_methods(file_object)
106
+ else extract_other_methods(file_object)
308
107
  end
309
108
  end
310
109
 
311
- def normalize_other_file_path(file_path)
312
- Tng::Analyzers::Other::OTHER_DIRECTORIES.each do |dir|
313
- pattern = %r{^.*/#{Regexp.escape(dir)}/}
314
- return file_path.gsub(pattern, "").gsub(".rb", "") if file_path.match?(pattern)
110
+ def generate_test_result(file_object, method_info, type)
111
+ generator = Tng::Services::TestGenerator.new(@http_client)
112
+
113
+ case type
114
+ when "controller" then generator.run_for_controller_method(file_object, method_info)
115
+ when "model" then generator.run_for_model_method(file_object, method_info)
116
+ when "service" then generator.run_for_service_method(file_object, method_info)
117
+ else generator.run_for_other_method(file_object, method_info)
315
118
  end
316
- file_path # fallback if no match
317
119
  end
318
120
  end
319
121
  end
@@ -4,32 +4,28 @@ module Tng
4
4
  module Services
5
5
  module ExtractMethods
6
6
  def extract_controller_methods(controller)
7
- methods = Tng::Analyzers::Controller.methods_for_controller(controller[:name])
8
- Array(methods)
7
+ Tng::Analyzers::Controller.methods_for_controller(controller[:name]) || []
9
8
  rescue StandardError => e
10
9
  puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing controller: #{e.message}", Tng::UI::Theme.color(:error)))
11
10
  []
12
11
  end
13
12
 
14
13
  def extract_model_methods(model)
15
- methods = Tng::Analyzers::Model.methods_for_model(model[:name])
16
- Array(methods)
14
+ Tng::Analyzers::Model.methods_for_model(model[:name]) || []
17
15
  rescue StandardError => e
18
16
  puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing model: #{e.message}", Tng::UI::Theme.color(:error)))
19
17
  []
20
18
  end
21
19
 
22
20
  def extract_service_methods(service)
23
- methods = Tng::Analyzers::Service.methods_for_service(service[:name])
24
- Array(methods)
21
+ Tng::Analyzers::Service.methods_for_service(service[:name]) || []
25
22
  rescue StandardError => e
26
23
  puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing service: #{e.message}", Tng::UI::Theme.color(:error)))
27
24
  []
28
25
  end
29
26
 
30
27
  def extract_other_methods(other_file)
31
- methods = Tng::Analyzers::Other.methods_for_other(other_file[:name], other_file[:path])
32
- Array(methods)
28
+ Tng::Analyzers::Other.methods_for_other(other_file[:name], other_file[:path]) || []
33
29
  rescue StandardError => e
34
30
  puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing file: #{e.message}", Tng::UI::Theme.color(:error)))
35
31
  []
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ module Tng
6
+ module Services
7
+ class FileTypeDetector
8
+ class << self
9
+ TYPE_PATTERNS = {
10
+ /app\/controllers\// => 'controller',
11
+ /app\/models\// => 'model',
12
+ /app\/services?\// => 'service',
13
+ /app\/jobs\// => 'job',
14
+ /app\/helpers\// => 'helper',
15
+ /(?:app\/)?libs?\// => 'lib',
16
+ /app\/mailers\// => 'mailer',
17
+ /app\/channels\// => 'channel',
18
+ /app\/decorators\// => 'decorator',
19
+ /app\/presenters\// => 'presenter',
20
+ /app\/serializers\// => 'serializer',
21
+ /app\/policies\// => 'policy',
22
+ /app\/forms\// => 'form',
23
+ /app\/queries\// => 'query',
24
+ /app\/graphql\/resolvers\// => 'resolver',
25
+ /app\/graphql\/types\// => 'type',
26
+ /app\/graphql\/mutations\// => 'mutation',
27
+ /app\/graphql\/loaders\// => 'loader',
28
+ /app\/graphql\/schemas\// => 'schema',
29
+ /app\/graphql\// => 'graphql'
30
+ }.freeze
31
+
32
+ PATH_MAPPINGS = {
33
+ 'controller' => 'app/controllers',
34
+ 'model' => 'app/models',
35
+ 'service' => 'app/services?',
36
+ 'job' => 'app/jobs',
37
+ 'helper' => 'app/helpers',
38
+ 'lib' => 'lib',
39
+ 'mailer' => 'app/mailers',
40
+ 'channel' => 'app/channels',
41
+ 'decorator' => 'app/decorators',
42
+ 'presenter' => 'app/presenters',
43
+ 'serializer' => 'app/serializers',
44
+ 'policy' => 'app/policies',
45
+ 'form' => 'app/forms',
46
+ 'query' => 'app/queries',
47
+ 'resolver' => 'app/graphql/resolvers',
48
+ 'type' => 'app/graphql/types',
49
+ 'mutation' => 'app/graphql/mutations',
50
+ 'loader' => 'app/graphql/loaders',
51
+ 'schema' => 'app/graphql/schemas',
52
+ 'graphql' => 'app/graphql'
53
+ }.freeze
54
+
55
+ def detect_type(file_path)
56
+ normalized_path = normalize_path(file_path)
57
+ TYPE_PATTERNS.find { |pattern, _| normalized_path.match?(pattern) }&.last || 'other'
58
+ end
59
+
60
+ def normalize_path(file_path)
61
+ path = file_path.sub(/\.rb$/, '')
62
+ path.start_with?('/') ? path : (find_file_in_project(path) || path)
63
+ end
64
+
65
+ SEARCH_PATHS = %w[
66
+ app/controllers app/models app/services app/service
67
+ app/jobs app/helpers app/mailers app/channels
68
+ app/decorators app/presenters app/serializers
69
+ app/policies app/forms app/queries app/graphql
70
+ lib app/lib
71
+ ].freeze
72
+
73
+ def find_file_in_project(file_name)
74
+ file_with_ext = file_name.end_with?('.rb') ? file_name : "#{file_name}.rb"
75
+
76
+ return File.expand_path(file_with_ext) if File.exist?(file_with_ext)
77
+
78
+ rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
79
+
80
+ SEARCH_PATHS.each do |dir|
81
+ full_path = File.join(rails_root, dir, file_with_ext)
82
+ return full_path if File.exist?(full_path)
83
+
84
+ found_files = Dir.glob(File.join(rails_root, dir, '**', file_with_ext))
85
+ return found_files.first unless found_files.empty?
86
+ end
87
+
88
+ nil
89
+ end
90
+
91
+ def resolve_file_path(file_path)
92
+ resolved = if file_path.start_with?('/')
93
+ File.exist?(file_path) ? file_path : File.exist?("#{file_path}.rb") ? "#{file_path}.rb" : nil
94
+ else
95
+ found = find_file_in_project(file_path)
96
+ found ? File.expand_path(found) : nil
97
+ end
98
+
99
+ resolved
100
+ end
101
+
102
+ def extract_relative_path(file_path, type)
103
+ base_path = PATH_MAPPINGS[type]
104
+ return File.basename(file_path, '.rb') unless base_path
105
+
106
+ pattern = base_path == 'lib' ? %r{^.*/#{base_path}/} : %r{^.*#{base_path}/}
107
+ file_path.gsub(pattern, '').gsub('.rb', '')
108
+ end
109
+
110
+ def build_file_object(file_path, type)
111
+ actual_file_path = File.expand_path(file_path)
112
+ relative_path = extract_relative_path(actual_file_path, type)
113
+ namespaced_name = relative_path.split('/').map(&:camelize).join('::')
114
+
115
+ {
116
+ name: namespaced_name,
117
+ path: actual_file_path,
118
+ type: type
119
+ }
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end