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.
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