tng 0.1.0
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.
- checksums.yaml +7 -0
- data/Gemfile +15 -0
- data/LICENSE.md +32 -0
- data/README.md +506 -0
- data/Rakefile +124 -0
- data/bin/load_dev +22 -0
- data/bin/tng +1096 -0
- data/binaries/tng.bundle +0 -0
- data/binaries/tng.so +0 -0
- data/lib/generators/tng/install_generator.rb +228 -0
- data/lib/tng/analyzers/controller.rb +114 -0
- data/lib/tng/analyzers/model.rb +122 -0
- data/lib/tng/analyzers/service.rb +149 -0
- data/lib/tng/api/http_client.rb +102 -0
- data/lib/tng/railtie.rb +11 -0
- data/lib/tng/services/test_generator.rb +159 -0
- data/lib/tng/services/testng.rb +100 -0
- data/lib/tng/services/user_app_config.rb +77 -0
- data/lib/tng/ui/about_display.rb +71 -0
- data/lib/tng/ui/configuration_display.rb +46 -0
- data/lib/tng/ui/controller_test_flow_display.rb +89 -0
- data/lib/tng/ui/display_banner.rb +54 -0
- data/lib/tng/ui/goodbye_display.rb +50 -0
- data/lib/tng/ui/model_test_flow_display.rb +95 -0
- data/lib/tng/ui/post_install_box.rb +73 -0
- data/lib/tng/ui/service_test_flow_display.rb +89 -0
- data/lib/tng/ui/show_help.rb +89 -0
- data/lib/tng/ui/system_status_display.rb +57 -0
- data/lib/tng/ui/user_stats_display.rb +153 -0
- data/lib/tng/utils.rb +263 -0
- data/lib/tng/version.rb +6 -0
- data/lib/tng.rb +294 -0
- data/tng.gemspec +54 -0
- metadata +283 -0
data/binaries/tng.bundle
ADDED
Binary file
|
data/binaries/tng.so
ADDED
Binary file
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "yaml"
|
5
|
+
require "tng"
|
6
|
+
|
7
|
+
module Tng
|
8
|
+
module Generators
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
10
|
+
desc "Creates a tng.sh Ruby configuration file with comprehensive authentication and authorization support"
|
11
|
+
|
12
|
+
def self.source_root
|
13
|
+
@source_root ||= File.expand_path("templates", __dir__)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_tng_configuration
|
17
|
+
@test_framework = detect_test_framework
|
18
|
+
@authentication_library = detect_authentication_library
|
19
|
+
@authz_library = detect_authorization_library
|
20
|
+
@factory_library = detect_factory_library
|
21
|
+
|
22
|
+
initializer_path = "config/initializers/tng.rb"
|
23
|
+
|
24
|
+
if File.exist?(initializer_path)
|
25
|
+
say "Configuration file already exists at #{initializer_path}", :yellow
|
26
|
+
if yes?("Do you want to overwrite it? (y/n)")
|
27
|
+
remove_file initializer_path
|
28
|
+
create_file initializer_path, ruby_configuration_template
|
29
|
+
say "TNG configuration file updated!", :green
|
30
|
+
else
|
31
|
+
say "Skipping configuration file creation.", :blue
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
else
|
35
|
+
create_file initializer_path, ruby_configuration_template
|
36
|
+
say "tng.sh configuration file created at #{initializer_path}", :green
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ruby_configuration_template
|
41
|
+
framework_config = generate_framework_config(@test_framework)
|
42
|
+
|
43
|
+
if @authentication_library && @authentication_library != "none"
|
44
|
+
auth_enabled = "config.authentication_enabled = true"
|
45
|
+
auth_comment = " (detected: #{@authentication_library})"
|
46
|
+
auth_lib = "config.authentication_library = \"#{@authentication_library}\""
|
47
|
+
else
|
48
|
+
auth_enabled = "config.authentication_enabled = true"
|
49
|
+
auth_comment = ""
|
50
|
+
auth_lib = "config.authentication_library = nil"
|
51
|
+
end
|
52
|
+
|
53
|
+
if @authz_library && @authz_library != "none"
|
54
|
+
authz_lib = "config.authorization_library = \"#{@authz_library}\""
|
55
|
+
authz_comment = " (detected: #{@authz_library})"
|
56
|
+
else
|
57
|
+
authz_lib = "config.authorization_library = nil"
|
58
|
+
authz_comment = ""
|
59
|
+
end
|
60
|
+
|
61
|
+
factory_lib = "config.factory_library = \"#{@factory_library}\""
|
62
|
+
|
63
|
+
framework_specific = generate_framework_specific_config(@test_framework, framework_config)
|
64
|
+
[
|
65
|
+
"# frozen_string_literal: true",
|
66
|
+
"",
|
67
|
+
"return if Rails.env.production?",
|
68
|
+
"",
|
69
|
+
"Tng.configure do |config|",
|
70
|
+
" config.api_key = nil",
|
71
|
+
" config.base_url = \"https://app.tng.sh/\"",
|
72
|
+
" config.read_source_code = true # Options: true, false",
|
73
|
+
" config.read_test_code = true # Options: true, false",
|
74
|
+
"",
|
75
|
+
" # Testing Framework",
|
76
|
+
" config.testing_framework = \"#{@test_framework}\" # Options: minitest, rspec",
|
77
|
+
framework_specific,
|
78
|
+
" config.mock_library = \"#{framework_config["mock_library"]}\" # Options: mocha, minitest/mock, rspec-mocks, nil",
|
79
|
+
" config.http_mock_library = \"#{framework_config["http_mock_library"]}\" # Options: webmock, vcr, httparty, nil",
|
80
|
+
" #{factory_lib} # Options: factory_bot, factory_girl, fabrication, fabricator, fixtures, active_record",
|
81
|
+
"",
|
82
|
+
" # Authentication#{auth_comment}",
|
83
|
+
" #{auth_enabled} # Options: true, false",
|
84
|
+
" #{auth_lib} # Options: devise, clearance, sorcery, nil",
|
85
|
+
"",
|
86
|
+
" # Authentication Methods (multiple methods supported)",
|
87
|
+
" # Supported authentication types: session, devise, jwt, token_auth, basic_auth, oauth, headers, custom, nil",
|
88
|
+
" # Uncomment and configure your authentication entry points:",
|
89
|
+
" # config.authentication_methods = [",
|
90
|
+
" # {",
|
91
|
+
" # method: \"authenticate_user_via_session!\",",
|
92
|
+
" # file_location: \"app/controllers/application_controller.rb\",",
|
93
|
+
" # auth_type: \"session\"",
|
94
|
+
" # },",
|
95
|
+
" # {",
|
96
|
+
" # method: \"authenticate_user_via_api_key!\",",
|
97
|
+
" # file_location: \"app/controllers/application_controller.rb\",",
|
98
|
+
" # auth_type: \"headers\"",
|
99
|
+
" # }",
|
100
|
+
" # ]",
|
101
|
+
"",
|
102
|
+
" # Authorization#{authz_comment}",
|
103
|
+
" #{authz_lib} # Options: cancancan, pundit, rolify, nil",
|
104
|
+
"end"
|
105
|
+
].join("\n") + "\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def detect_test_framework
|
111
|
+
rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
|
112
|
+
|
113
|
+
return "rspec" if File.exist?(File.join(rails_root, ".rspec")) ||
|
114
|
+
File.exist?(File.join(rails_root, "spec", "spec_helper.rb")) ||
|
115
|
+
Dir.exist?(File.join(rails_root, "spec"))
|
116
|
+
|
117
|
+
return "minitest" if Dir.exist?(File.join(rails_root, "test")) ||
|
118
|
+
File.exist?(File.join(rails_root, "test", "test_helper.rb"))
|
119
|
+
|
120
|
+
begin
|
121
|
+
if defined?(Bundler)
|
122
|
+
gemfile_specs = Bundler.load.specs
|
123
|
+
return "rspec" if gemfile_specs.any? { |spec| spec.name == "rspec-rails" }
|
124
|
+
return "minitest" if gemfile_specs.any? { |spec| spec.name == "minitest" }
|
125
|
+
end
|
126
|
+
rescue StandardError
|
127
|
+
# Fallback if Bundler isn't available
|
128
|
+
end
|
129
|
+
|
130
|
+
"minitest" # Default fallback
|
131
|
+
end
|
132
|
+
|
133
|
+
def detect_authentication_library
|
134
|
+
return unless defined?(Bundler)
|
135
|
+
|
136
|
+
begin
|
137
|
+
gemfile_specs = Bundler.load.specs
|
138
|
+
return "devise" if gemfile_specs.any? { |spec| spec.name == "devise" }
|
139
|
+
return "clearance" if gemfile_specs.any? { |spec| spec.name == "clearance" }
|
140
|
+
return "sorcery" if gemfile_specs.any? { |spec| spec.name == "sorcery" }
|
141
|
+
rescue StandardError
|
142
|
+
# Fallback if Bundler isn't available
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
def detect_authorization_library
|
150
|
+
return unless defined?(Bundler)
|
151
|
+
|
152
|
+
gemfile_specs = Bundler.load.specs
|
153
|
+
return "cancancan" if gemfile_specs.any? { |spec| spec.name == "cancancan" }
|
154
|
+
return "pundit" if gemfile_specs.any? { |spec| spec.name == "pundit" }
|
155
|
+
return "rolify" if gemfile_specs.any? { |spec| spec.name == "rolify" }
|
156
|
+
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
def detect_factory_library
|
161
|
+
return "active_record" unless defined?(Bundler)
|
162
|
+
|
163
|
+
rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
|
164
|
+
|
165
|
+
# Check for fixtures in test/fixtures or spec/fixtures
|
166
|
+
test_fixtures_path = File.join(rails_root, "test", "fixtures")
|
167
|
+
spec_fixtures_path = File.join(rails_root, "spec", "fixtures")
|
168
|
+
|
169
|
+
return "fixtures" if Dir.exist?(test_fixtures_path) && !Dir.empty?(test_fixtures_path)
|
170
|
+
|
171
|
+
return "fixtures" if Dir.exist?(spec_fixtures_path) && !Dir.empty?(spec_fixtures_path)
|
172
|
+
|
173
|
+
gemfile_specs = Bundler.load.specs
|
174
|
+
return "factory_bot" if gemfile_specs.any? { |spec| spec.name.match(/factory_bot/) }
|
175
|
+
return "factory_girl" if gemfile_specs.any? { |spec| spec.name.match(/factory_girl/) }
|
176
|
+
return "fabrication" if gemfile_specs.any? { |spec| spec.name.match(/fabrication/) }
|
177
|
+
return "fabricator" if gemfile_specs.any? { |spec| spec.name.match(/fabricator/) }
|
178
|
+
|
179
|
+
"active_record"
|
180
|
+
rescue StandardError
|
181
|
+
# Fallback if Bundler isn't available
|
182
|
+
"active_record"
|
183
|
+
end
|
184
|
+
|
185
|
+
def generate_framework_config(framework)
|
186
|
+
if framework == "minitest"
|
187
|
+
{
|
188
|
+
"test_style" => "test_block", # Options: spec, unit, test_block
|
189
|
+
"setup_style" => true, # Options: true, false
|
190
|
+
"assertion_style" => "assert/refute", # Options: assert/refute, assert/assert_not, must/wont
|
191
|
+
"teardown_style" => false, # Options: true, false
|
192
|
+
"http_mock_library" => "webmock", # Options: webmock, vcr, httparty, none
|
193
|
+
"mock_library" => "minitest/mock" # Options: mocha, minitest/mock, rspec-mocks, none
|
194
|
+
}
|
195
|
+
else # rspec
|
196
|
+
{
|
197
|
+
"describe_style" => true, # Options: true, false
|
198
|
+
"context_style" => "context", # Options: context, describe
|
199
|
+
"it_style" => "it", # Options: it, specify
|
200
|
+
"before_style" => "before", # Options: before, setup
|
201
|
+
"after_style" => "after", # Options: after, teardown
|
202
|
+
"let_style" => true, # Options: true, false
|
203
|
+
"subject_style" => true, # Options: true, false
|
204
|
+
"mock_library" => "rspec-mocks", # Options: rspec-mocks, mocha, flexmock, none
|
205
|
+
"http_mock_library" => "webmock" # Options: webmock, vcr, httparty, none
|
206
|
+
}
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def generate_framework_specific_config(framework, framework_config)
|
211
|
+
if framework == "minitest"
|
212
|
+
" config.test_style = \"#{framework_config["test_style"]}\" # Options: spec, unit, test_block\n" +
|
213
|
+
" config.setup_style = #{framework_config["setup_style"]} # Options: true, false\n" +
|
214
|
+
" config.assertion_style = \"#{framework_config["assertion_style"]}\" # Options: assert/refute, assert/assert_not, must/wont\n" +
|
215
|
+
" config.teardown_style = #{framework_config["teardown_style"]} # Options: true, false"
|
216
|
+
else # rspec
|
217
|
+
" config.describe_style = #{framework_config["describe_style"]} # Options: true, false\n" +
|
218
|
+
" config.context_style = \"#{framework_config["context_style"]}\" # Options: context, describe\n" +
|
219
|
+
" config.it_style = \"#{framework_config["it_style"]}\" # Options: it, specify\n" +
|
220
|
+
" config.before_style = \"#{framework_config["before_style"]}\" # Options: before, setup\n" +
|
221
|
+
" config.after_style = \"#{framework_config["after_style"]}\" # Options: after, teardown\n" +
|
222
|
+
" config.let_style = #{framework_config["let_style"]} # Options: true, false\n" +
|
223
|
+
" config.subject_style = #{framework_config["subject_style"]} # Options: true, false"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Analyzers
|
5
|
+
class Controller
|
6
|
+
def self.files_in_dir(dir)
|
7
|
+
dir = File.join(Dir.pwd, "app/controllers") if dir.nil?
|
8
|
+
Tng::Analyzer::Controller.files_in_dir(dir.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.routes_for_controller(file_path)
|
12
|
+
raise "file_path is required" if file_path.nil?
|
13
|
+
|
14
|
+
Tng::Analyzer::Controller.routes_for_controller(file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.value_for_controller(file_path)
|
18
|
+
raise "file_path is required" if file_path.nil?
|
19
|
+
|
20
|
+
Tng::Analyzer::Controller.parse_controller_file(file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parents_for_controller(controller)
|
24
|
+
raise "controller is required" if controller.nil?
|
25
|
+
|
26
|
+
path = controller[:path]
|
27
|
+
relative_path = path.gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
|
28
|
+
clean_controller_name = relative_path.split("/").map(&:camelize).join("::")
|
29
|
+
|
30
|
+
begin
|
31
|
+
Tng::Analyzer::Controller.parents_for_controller(clean_controller_name)
|
32
|
+
rescue NameError => e
|
33
|
+
puts "❌ Error analyzing controller #{clean_controller_name}: #{e.message}"
|
34
|
+
puts " Path: #{path}"
|
35
|
+
puts " This might be due to a syntax error in the controller file."
|
36
|
+
[]
|
37
|
+
rescue StandardError => e
|
38
|
+
puts "❌ Error analyzing controller #{clean_controller_name}: #{e.message}"
|
39
|
+
puts " Path: #{path}"
|
40
|
+
puts " This might be due to a syntax error in the controller file."
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.model_info_for_controller(file_path)
|
46
|
+
raise "file_path is required" if file_path.nil?
|
47
|
+
|
48
|
+
Tng::Analyzer::Controller.model_info_for_controller(file_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.find_test_file_for_controller(controller_path)
|
52
|
+
controller_name = File.basename(controller_path, ".rb")
|
53
|
+
testing_framework = Tng.testing_framework
|
54
|
+
return [] if testing_framework.nil?
|
55
|
+
|
56
|
+
paths = if testing_framework.downcase == "rspec"
|
57
|
+
[
|
58
|
+
"spec/controllers/#{controller_name}_spec.rb",
|
59
|
+
"spec/requests/#{controller_name}_spec.rb"
|
60
|
+
]
|
61
|
+
else
|
62
|
+
[
|
63
|
+
"test/controllers/#{controller_name}_test.rb",
|
64
|
+
"test/functional/#{controller_name}_test.rb"
|
65
|
+
]
|
66
|
+
end
|
67
|
+
|
68
|
+
paths.select { |path| File.exist?(path) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.read_test_file_for_controller(controller_path)
|
72
|
+
files = find_test_file_for_controller(controller_path)
|
73
|
+
|
74
|
+
files.map do |file_path|
|
75
|
+
{
|
76
|
+
path: file_path,
|
77
|
+
content: File.read(file_path)
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.methods_for_controller(controller_name)
|
83
|
+
raise "controller_name is required" if controller_name.nil?
|
84
|
+
|
85
|
+
begin
|
86
|
+
# Load the controller class
|
87
|
+
controller_class = controller_name.constantize
|
88
|
+
|
89
|
+
# Get public instance methods (actions) excluding inherited ones
|
90
|
+
public_methods = controller_class.public_instance_methods(false)
|
91
|
+
|
92
|
+
# Filter to methods defined in this controller only
|
93
|
+
action_methods = public_methods.select do |method_name|
|
94
|
+
method = controller_class.instance_method(method_name)
|
95
|
+
method.owner == controller_class
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return method info
|
99
|
+
action_methods.map do |method_name|
|
100
|
+
{
|
101
|
+
name: method_name.to_s
|
102
|
+
}
|
103
|
+
end
|
104
|
+
rescue NameError => e
|
105
|
+
puts "❌ Could not load controller class #{controller_name}: #{e.message}"
|
106
|
+
[]
|
107
|
+
rescue StandardError => e
|
108
|
+
puts "❌ Error analyzing controller #{controller_name}: #{e.message}"
|
109
|
+
[]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Analyzers
|
5
|
+
class Model
|
6
|
+
def self.files_in_dir(dir = nil)
|
7
|
+
dir = File.join(Dir.pwd, "app/models") if dir.nil?
|
8
|
+
Tng::Analyzer::Model.files_in_dir(dir.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.value_for_model(file_path)
|
12
|
+
raise "file_path is required" if file_path.nil?
|
13
|
+
|
14
|
+
Tng::Analyzer::Model.parse_model_file(file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.read_test_file_for_model(model_path)
|
18
|
+
files = find_test_file_for_model(model_path)
|
19
|
+
|
20
|
+
files.map do |file_path|
|
21
|
+
{
|
22
|
+
path: file_path,
|
23
|
+
content: File.read(file_path)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find_test_file_for_model(model_path)
|
29
|
+
model_name = File.basename(model_path, ".rb")
|
30
|
+
testing_framework = Tng.testing_framework
|
31
|
+
return [] if testing_framework.nil?
|
32
|
+
|
33
|
+
paths = if testing_framework.downcase == "rspec"
|
34
|
+
[
|
35
|
+
"spec/models/#{model_name}_spec.rb",
|
36
|
+
"spec/requests/#{model_name}_spec.rb"
|
37
|
+
]
|
38
|
+
else
|
39
|
+
[
|
40
|
+
"test/models/#{model_name}_test.rb",
|
41
|
+
"test/functional/#{model_name}_test.rb"
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
paths.select { |path| File.exist?(path) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.model_connections(model)
|
49
|
+
raise "model is required" if model.nil?
|
50
|
+
|
51
|
+
file_path = model.is_a?(Hash) ? model[:path] : model
|
52
|
+
raise "model path is required" if file_path.nil?
|
53
|
+
|
54
|
+
Tng::Analyzer::Model.model_connections(file_path.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.methods_for_model(model_name)
|
58
|
+
raise "model_name is required" if model_name.nil?
|
59
|
+
|
60
|
+
begin
|
61
|
+
# Load the model class
|
62
|
+
model_class = model_name.constantize
|
63
|
+
|
64
|
+
instance_methods = model_class.public_instance_methods(false)
|
65
|
+
class_methods = model_class.public_methods(false) - Class.public_methods
|
66
|
+
|
67
|
+
model_file = model_class.const_source_location(model_class.name.split("::").last)&.first
|
68
|
+
|
69
|
+
model_methods = if model_file && File.exist?(model_file)
|
70
|
+
source_code = File.read(model_file)
|
71
|
+
result = Prism.parse(source_code)
|
72
|
+
|
73
|
+
defined_methods = []
|
74
|
+
extract_method_names(result.value, defined_methods)
|
75
|
+
|
76
|
+
filtered_instance_methods = instance_methods.select do |method_name|
|
77
|
+
method = model_class.instance_method(method_name)
|
78
|
+
next false unless method.owner == model_class
|
79
|
+
|
80
|
+
defined_methods.include?(method_name.to_s)
|
81
|
+
end
|
82
|
+
|
83
|
+
filtered_class_methods = class_methods.select do |method_name|
|
84
|
+
defined_methods.include?(method_name.to_s)
|
85
|
+
end
|
86
|
+
|
87
|
+
filtered_instance_methods + filtered_class_methods
|
88
|
+
else
|
89
|
+
[]
|
90
|
+
end
|
91
|
+
|
92
|
+
model_methods.map { |method_name| { name: method_name.to_s } }
|
93
|
+
rescue NameError => e
|
94
|
+
puts "❌ Could not load model class #{model_name}: #{e.message}"
|
95
|
+
[]
|
96
|
+
rescue StandardError => e
|
97
|
+
puts "❌ Error analyzing model #{model_name}: #{e.message}"
|
98
|
+
[]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.extract_method_names(node, methods)
|
103
|
+
return unless node.is_a?(Prism::Node)
|
104
|
+
|
105
|
+
case node
|
106
|
+
when Prism::DefNode
|
107
|
+
# Both instance and class methods (def self.method_name)
|
108
|
+
methods << node.name.to_s
|
109
|
+
when Prism::SingletonClassNode
|
110
|
+
# Methods inside class << self blocks
|
111
|
+
node.body&.child_nodes&.each do |child|
|
112
|
+
extract_method_names(child, methods)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
node.child_nodes.each do |child|
|
117
|
+
extract_method_names(child, methods)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Analyzers
|
5
|
+
class Service
|
6
|
+
def self.files_in_dir(dir = nil)
|
7
|
+
base_dirs = if dir.nil?
|
8
|
+
["app/services", "app/service"].select { |d| Dir.exist?(File.join(Dir.pwd, d)) }
|
9
|
+
else
|
10
|
+
[dir]
|
11
|
+
end
|
12
|
+
|
13
|
+
return [] if base_dirs.empty?
|
14
|
+
|
15
|
+
base_dirs.flat_map do |base_dir|
|
16
|
+
full_path = File.join(Dir.pwd, base_dir)
|
17
|
+
find_service_files(full_path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.value_for_service(file_path)
|
22
|
+
raise "file_path is required" if file_path.nil?
|
23
|
+
|
24
|
+
Tng::Analyzer::Service.parse_service_file(file_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.read_test_file_for_service(service_path)
|
28
|
+
files = find_test_file_for_service(service_path)
|
29
|
+
|
30
|
+
files.map do |file_path|
|
31
|
+
{
|
32
|
+
path: file_path,
|
33
|
+
content: File.read(file_path)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find_test_file_for_service(service_path)
|
39
|
+
service_name = File.basename(service_path, ".rb")
|
40
|
+
testing_framework = Tng.testing_framework
|
41
|
+
return [] if testing_framework.nil?
|
42
|
+
|
43
|
+
paths = if testing_framework.downcase == "rspec"
|
44
|
+
[
|
45
|
+
"spec/services/#{service_name}_spec.rb",
|
46
|
+
"spec/service/#{service_name}_spec.rb"
|
47
|
+
]
|
48
|
+
else
|
49
|
+
[
|
50
|
+
"test/services/#{service_name}_test.rb",
|
51
|
+
"test/service/#{service_name}_test.rb"
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
paths.select { |path| File.exist?(path) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.methods_for_service(service_name)
|
59
|
+
raise "service_name is required" if service_name.nil?
|
60
|
+
|
61
|
+
begin
|
62
|
+
# Load the service class
|
63
|
+
service_class = service_name.constantize
|
64
|
+
|
65
|
+
instance_methods = service_class.public_instance_methods(false)
|
66
|
+
class_methods = service_class.public_methods(false) - Class.public_methods
|
67
|
+
|
68
|
+
# Try to get source file from any method, fallback to const_source_location
|
69
|
+
service_file = nil
|
70
|
+
|
71
|
+
# First try to get file from an instance method
|
72
|
+
if instance_methods.any?
|
73
|
+
begin
|
74
|
+
service_file = service_class.instance_method(instance_methods.first).source_location&.first
|
75
|
+
rescue StandardError
|
76
|
+
# Method might not have source location, continue
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Fallback to const_source_location if no method source found
|
81
|
+
service_file ||= service_class.const_source_location(service_class.name.split("::").last)&.first
|
82
|
+
|
83
|
+
service_methods = if service_file && File.exist?(service_file)
|
84
|
+
source_code = File.read(service_file)
|
85
|
+
result = Prism.parse(source_code)
|
86
|
+
|
87
|
+
defined_methods = []
|
88
|
+
extract_method_names(result.value, defined_methods)
|
89
|
+
|
90
|
+
filtered_instance_methods = instance_methods.select do |method_name|
|
91
|
+
method = service_class.instance_method(method_name)
|
92
|
+
next false unless method.owner == service_class
|
93
|
+
|
94
|
+
defined_methods.include?(method_name.to_s)
|
95
|
+
end
|
96
|
+
|
97
|
+
filtered_class_methods = class_methods.select do |method_name|
|
98
|
+
defined_methods.include?(method_name.to_s)
|
99
|
+
end
|
100
|
+
|
101
|
+
filtered_instance_methods + filtered_class_methods
|
102
|
+
else
|
103
|
+
[]
|
104
|
+
end
|
105
|
+
|
106
|
+
service_methods.map { |method_name| { name: method_name.to_s } }
|
107
|
+
rescue NameError => e
|
108
|
+
puts "❌ Could not load service class #{service_name}: #{e.message}"
|
109
|
+
[]
|
110
|
+
rescue StandardError => e
|
111
|
+
puts "❌ Error analyzing service #{service_name}: #{e.message}"
|
112
|
+
[]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.extract_method_names(node, methods)
|
117
|
+
return unless node.is_a?(Prism::Node)
|
118
|
+
|
119
|
+
case node
|
120
|
+
when Prism::DefNode
|
121
|
+
# Both instance and class methods (def self.method_name)
|
122
|
+
methods << node.name.to_s
|
123
|
+
when Prism::SingletonClassNode
|
124
|
+
# Methods inside class << self blocks
|
125
|
+
node.body&.child_nodes&.each do |child|
|
126
|
+
extract_method_names(child, methods)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
node.child_nodes.each do |child|
|
131
|
+
extract_method_names(child, methods)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.find_service_files(dir)
|
136
|
+
return [] unless Dir.exist?(dir)
|
137
|
+
|
138
|
+
Dir.glob(File.join(dir, "**", "*.rb")).map do |file_path|
|
139
|
+
relative_path = file_path.gsub("#{Dir.pwd}/", "")
|
140
|
+
{
|
141
|
+
file: File.basename(file_path),
|
142
|
+
path: file_path,
|
143
|
+
relative_path: relative_path
|
144
|
+
}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|