tng 0.2.1

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +15 -0
  3. data/LICENSE.md +32 -0
  4. data/README.md +413 -0
  5. data/Rakefile +124 -0
  6. data/bin/load_dev +22 -0
  7. data/bin/tng +888 -0
  8. data/binaries/tng.bundle +0 -0
  9. data/binaries/tng.so +0 -0
  10. data/lib/generators/tng/install_generator.rb +236 -0
  11. data/lib/tng/analyzers/controller.rb +114 -0
  12. data/lib/tng/analyzers/model.rb +131 -0
  13. data/lib/tng/analyzers/other.rb +277 -0
  14. data/lib/tng/analyzers/service.rb +150 -0
  15. data/lib/tng/api/http_client.rb +100 -0
  16. data/lib/tng/railtie.rb +11 -0
  17. data/lib/tng/services/direct_generation.rb +320 -0
  18. data/lib/tng/services/extract_methods.rb +39 -0
  19. data/lib/tng/services/test_generator.rb +287 -0
  20. data/lib/tng/services/testng.rb +100 -0
  21. data/lib/tng/services/user_app_config.rb +76 -0
  22. data/lib/tng/ui/about_display.rb +66 -0
  23. data/lib/tng/ui/authentication_warning_display.rb +172 -0
  24. data/lib/tng/ui/configuration_display.rb +52 -0
  25. data/lib/tng/ui/controller_test_flow_display.rb +79 -0
  26. data/lib/tng/ui/display_banner.rb +44 -0
  27. data/lib/tng/ui/goodbye_display.rb +41 -0
  28. data/lib/tng/ui/model_test_flow_display.rb +80 -0
  29. data/lib/tng/ui/other_test_flow_display.rb +78 -0
  30. data/lib/tng/ui/post_install_box.rb +80 -0
  31. data/lib/tng/ui/service_test_flow_display.rb +78 -0
  32. data/lib/tng/ui/show_help.rb +78 -0
  33. data/lib/tng/ui/system_status_display.rb +128 -0
  34. data/lib/tng/ui/theme.rb +258 -0
  35. data/lib/tng/ui/user_stats_display.rb +160 -0
  36. data/lib/tng/utils.rb +325 -0
  37. data/lib/tng/version.rb +5 -0
  38. data/lib/tng.rb +308 -0
  39. data/tng.gemspec +56 -0
  40. metadata +293 -0
Binary file
data/binaries/tng.so ADDED
Binary file
@@ -0,0 +1,236 @@
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
+ " # You dont need to change this url, unless you will instructed by the CLI.",
72
+ " config.base_url = \"https://app.tng.sh/\"",
73
+ " config.read_source_code = true # Options: true, false",
74
+ " config.read_test_code = true # Options: true, false",
75
+ "",
76
+ " # Testing Framework",
77
+ " config.testing_framework = \"#{@test_framework}\" # Options: minitest, rspec",
78
+ framework_specific,
79
+ " config.mock_library = \"#{framework_config["mock_library"]}\" # Options: mocha, minitest/mock, rspec-mocks, nil",
80
+ " config.http_mock_library = \"#{framework_config["http_mock_library"]}\" # Options: webmock, vcr, httparty, nil",
81
+ " #{factory_lib} # Options: factory_bot, factory_girl, fabrication, fabricator, fixtures, active_record",
82
+ "",
83
+ " # Authentication#{auth_comment}",
84
+ " #{auth_enabled} # Options: true, false",
85
+ " #{auth_lib} # Options: devise, clearance, sorcery, nil",
86
+ "",
87
+ "",
88
+ " # ⚠️ IMPORTANT: AUTHENTICATION CONFIGURATION REQUIRED ⚠️",
89
+ " # You MUST configure your authentication methods below for TNG to work properly.",
90
+ " # Uncomment and modify the authentication_methods configuration:",
91
+ "",
92
+ " # Authentication Methods (multiple methods supported)",
93
+ " # Supported authentication types: session, devise, jwt, token_auth, basic_auth, oauth, headers, custom, nil",
94
+ " # EXAMPLE: Uncomment and modify these examples to match your app's authentication:",
95
+ "",
96
+ " # config.authentication_methods = [",
97
+ " # {",
98
+ " # method: \"authenticate_user_via_session!\",",
99
+ " # file_location: \"app/controllers/application_controller.rb\",",
100
+ " # auth_type: \"session\"",
101
+ " # },",
102
+ " # {",
103
+ " # method: \"authenticate_user_via_api_key!\",",
104
+ " # file_location: \"app/controllers/application_controller.rb\",",
105
+ " # auth_type: \"headers\"",
106
+ " # }",
107
+ " # ]",
108
+ " # ⚠️ Remember to configure your authentication methods above! ⚠️",
109
+ "",
110
+ " # Authorization#{authz_comment}",
111
+ " #{authz_lib} # Options: cancancan, pundit, rolify, nil",
112
+ "end"
113
+ ].join("\n") + "\n"
114
+ end
115
+
116
+ private
117
+
118
+ def detect_test_framework
119
+ rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
120
+
121
+ return "rspec" if File.exist?(File.join(rails_root, ".rspec")) ||
122
+ File.exist?(File.join(rails_root, "spec", "spec_helper.rb")) ||
123
+ Dir.exist?(File.join(rails_root, "spec"))
124
+
125
+ return "minitest" if Dir.exist?(File.join(rails_root, "test")) ||
126
+ File.exist?(File.join(rails_root, "test", "test_helper.rb"))
127
+
128
+ begin
129
+ if defined?(Bundler)
130
+ gemfile_specs = Bundler.load.specs
131
+ return "rspec" if gemfile_specs.any? { |spec| spec.name == "rspec-rails" }
132
+ return "minitest" if gemfile_specs.any? { |spec| spec.name == "minitest" }
133
+ end
134
+ rescue StandardError
135
+ # Fallback if Bundler isn't available
136
+ end
137
+
138
+ "minitest" # Default fallback
139
+ end
140
+
141
+ def detect_authentication_library
142
+ return unless defined?(Bundler)
143
+
144
+ begin
145
+ gemfile_specs = Bundler.load.specs
146
+ return "devise" if gemfile_specs.any? { |spec| spec.name == "devise" }
147
+ return "clearance" if gemfile_specs.any? { |spec| spec.name == "clearance" }
148
+ return "sorcery" if gemfile_specs.any? { |spec| spec.name == "sorcery" }
149
+ rescue StandardError
150
+ # Fallback if Bundler isn't available
151
+ nil
152
+ end
153
+
154
+ nil
155
+ end
156
+
157
+ def detect_authorization_library
158
+ return unless defined?(Bundler)
159
+
160
+ gemfile_specs = Bundler.load.specs
161
+ return "cancancan" if gemfile_specs.any? { |spec| spec.name == "cancancan" }
162
+ return "pundit" if gemfile_specs.any? { |spec| spec.name == "pundit" }
163
+ return "rolify" if gemfile_specs.any? { |spec| spec.name == "rolify" }
164
+
165
+ nil
166
+ end
167
+
168
+ def detect_factory_library
169
+ return "active_record" unless defined?(Bundler)
170
+
171
+ rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
172
+
173
+ # Check for fixtures in test/fixtures or spec/fixtures
174
+ test_fixtures_path = File.join(rails_root, "test", "fixtures")
175
+ spec_fixtures_path = File.join(rails_root, "spec", "fixtures")
176
+
177
+ return "fixtures" if Dir.exist?(test_fixtures_path) && !Dir.empty?(test_fixtures_path)
178
+
179
+ return "fixtures" if Dir.exist?(spec_fixtures_path) && !Dir.empty?(spec_fixtures_path)
180
+
181
+ gemfile_specs = Bundler.load.specs
182
+ return "factory_bot" if gemfile_specs.any? { |spec| spec.name.match(/factory_bot/) }
183
+ return "factory_girl" if gemfile_specs.any? { |spec| spec.name.match(/factory_girl/) }
184
+ return "fabrication" if gemfile_specs.any? { |spec| spec.name.match(/fabrication/) }
185
+ return "fabricator" if gemfile_specs.any? { |spec| spec.name.match(/fabricator/) }
186
+
187
+ "active_record"
188
+ rescue StandardError
189
+ # Fallback if Bundler isn't available
190
+ "active_record"
191
+ end
192
+
193
+ def generate_framework_config(framework)
194
+ if framework == "minitest"
195
+ {
196
+ "test_style" => "test_block", # Options: spec, unit, test_block
197
+ "setup_style" => true, # Options: true, false
198
+ "assertion_style" => "assert/refute", # Options: assert/refute, assert/assert_not, must/wont
199
+ "teardown_style" => false, # Options: true, false
200
+ "http_mock_library" => "webmock", # Options: webmock, vcr, httparty, none
201
+ "mock_library" => "minitest/mock" # Options: mocha, minitest/mock, rspec-mocks, none
202
+ }
203
+ else # rspec
204
+ {
205
+ "describe_style" => true, # Options: true, false
206
+ "context_style" => "context", # Options: context, describe
207
+ "it_style" => "it", # Options: it, specify
208
+ "before_style" => "before", # Options: before, setup
209
+ "after_style" => "after", # Options: after, teardown
210
+ "let_style" => true, # Options: true, false
211
+ "subject_style" => true, # Options: true, false
212
+ "mock_library" => "rspec-mocks", # Options: rspec-mocks, mocha, flexmock, none
213
+ "http_mock_library" => "webmock" # Options: webmock, vcr, httparty, none
214
+ }
215
+ end
216
+ end
217
+
218
+ def generate_framework_specific_config(framework, framework_config)
219
+ if framework == "minitest"
220
+ " config.test_style = \"#{framework_config["test_style"]}\" # Options: spec, unit, test_block\n" +
221
+ " config.setup_style = #{framework_config["setup_style"]} # Options: true, false\n" +
222
+ " config.assertion_style = \"#{framework_config["assertion_style"]}\" # Options: assert/refute, assert/assert_not, must/wont\n" +
223
+ " config.teardown_style = #{framework_config["teardown_style"]} # Options: true, false"
224
+ else # rspec
225
+ " config.describe_style = #{framework_config["describe_style"]} # Options: true, false\n" +
226
+ " config.context_style = \"#{framework_config["context_style"]}\" # Options: context, describe\n" +
227
+ " config.it_style = \"#{framework_config["it_style"]}\" # Options: it, specify\n" +
228
+ " config.before_style = \"#{framework_config["before_style"]}\" # Options: before, setup\n" +
229
+ " config.after_style = \"#{framework_config["after_style"]}\" # Options: after, teardown\n" +
230
+ " config.let_style = #{framework_config["let_style"]} # Options: true, false\n" +
231
+ " config.subject_style = #{framework_config["subject_style"]} # Options: true, false"
232
+ end
233
+ end
234
+ end
235
+ end
236
+ 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,131 @@
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::CallNode
110
+ # Handle scope definitions: scope :name, -> { ... }
111
+ if node.name == :scope && node.arguments&.arguments&.any?
112
+ first_arg = node.arguments.arguments.first
113
+ if first_arg.is_a?(Prism::SymbolNode)
114
+ scope_name = first_arg.value
115
+ methods << scope_name if scope_name
116
+ end
117
+ end
118
+ when Prism::SingletonClassNode
119
+ # Methods inside class << self blocks
120
+ node.body&.child_nodes&.each do |child|
121
+ extract_method_names(child, methods)
122
+ end
123
+ end
124
+
125
+ node.child_nodes.each do |child|
126
+ extract_method_names(child, methods)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end