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.
data/lib/tng/utils.rb ADDED
@@ -0,0 +1,263 @@
1
+ require "yaml"
2
+ require "json"
3
+
4
+ module Tng
5
+ module Utils
6
+ def clear_screen
7
+ system("clear") || system("cls")
8
+ end
9
+
10
+ def center_text(text, width = nil)
11
+ width ||= @terminal_width || 80 # Use fallback if still nil
12
+ lines = text.split("\n")
13
+ lines.map do |line|
14
+ # Remove ANSI color codes for length calculation
15
+ clean_line = line.gsub(/\e\[[0-9;]*m/, "")
16
+ padding = [(width - clean_line.length) / 2, 0].max
17
+ " " * padding + line
18
+ end.join("\n")
19
+ end
20
+
21
+ def copy_to_clipboard(text)
22
+ # Try to copy to clipboard
23
+ if system("which pbcopy > /dev/null 2>&1")
24
+ system("echo '#{text}' | pbcopy")
25
+ success_msg = @pastel.green("✅ Command copied to clipboard!")
26
+ elsif system("which xclip > /dev/null 2>&1")
27
+ system("echo '#{text}' | xclip -selection clipboard")
28
+ success_msg = @pastel.green("✅ Command copied to clipboard!")
29
+ else
30
+ success_msg = @pastel.yellow("📋 Copy this command:")
31
+ puts center_text(success_msg)
32
+ puts center_text(@pastel.bright_white(text))
33
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
34
+ return
35
+ end
36
+
37
+ puts center_text(success_msg)
38
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
39
+ end
40
+
41
+ def load_rails_environment
42
+ # Use bundler environment to avoid gem conflicts
43
+ require "bundler/setup"
44
+ require "./config/environment"
45
+ true
46
+ rescue LoadError => e
47
+ puts "Failed to load Rails: #{e.message}"
48
+ false
49
+ end
50
+
51
+ def find_rails_root
52
+ current_dir = Dir.pwd
53
+ while current_dir != "/"
54
+ return current_dir if File.exist?(File.join(current_dir, "config", "application.rb"))
55
+
56
+ current_dir = File.dirname(current_dir)
57
+ end
58
+ nil
59
+ end
60
+
61
+ def camelize(str, first_letter = :upper)
62
+ # Use Rails' camelize method if available, otherwise use custom implementation
63
+ if defined?(ActiveSupport::Inflector) && ActiveSupport::Inflector.respond_to?(:camelize)
64
+ case first_letter
65
+ when :upper
66
+ ActiveSupport::Inflector.camelize(str, true)
67
+ when :lower
68
+ ActiveSupport::Inflector.camelize(str, false)
69
+ else
70
+ raise ArgumentError, "Invalid option, use either :upper or :lower."
71
+ end
72
+ elsif str.respond_to?(:camelize)
73
+ str.camelize(first_letter)
74
+ else
75
+ # Custom implementation
76
+ result = str.gsub(/(?:^|_)([a-z])/) { Regexp.last_match(1).upcase }
77
+ case first_letter
78
+ when :upper
79
+ result
80
+ when :lower
81
+ result[0].downcase + result[1..] if result.length.positive?
82
+ else
83
+ raise ArgumentError, "Invalid option, use either :upper or :lower."
84
+ end
85
+ end
86
+ end
87
+
88
+ def self.has_gem?(gem_name)
89
+ return false unless defined?(Bundler)
90
+
91
+ gemfile_specs = Bundler.load.specs
92
+ gemfile_specs.any? { |spec| spec.name == gem_name }
93
+ rescue StandardError
94
+ false
95
+ end
96
+
97
+ def self.save_test_file(test_content)
98
+ puts "📋 Raw API response: #{test_content[0..200]}..." if ENV["DEBUG"]
99
+ parsed_response = JSON.parse(test_content)
100
+
101
+ # Validate required fields
102
+ unless parsed_response["file_content"]
103
+ puts "❌ API response missing file_content field"
104
+ puts "📋 Response keys: #{parsed_response.keys.inspect}"
105
+ return nil
106
+ end
107
+
108
+ # Handle both possible field names for file path
109
+ file_path = parsed_response["test_file_path"] || parsed_response["file_path"]
110
+ unless file_path
111
+ puts "❌ API response missing test_file_path or file_path field"
112
+ puts "📋 Response keys: #{parsed_response.keys.inspect}"
113
+ return nil
114
+ end
115
+
116
+ File.write(file_path, parsed_response["file_content"])
117
+ puts "✅ Test generated successfully!"
118
+ absolute_path = File.expand_path(file_path)
119
+ puts "Please review the generated tests at \e]8;;file://#{absolute_path}\e\\#{file_path}\e]8;;\e\\"
120
+
121
+ # Determine run command based on test framework
122
+ run_command = if file_path.include?("/spec/")
123
+ "bundle exec rspec #{file_path}"
124
+ else
125
+ "bundle exec rails test #{file_path}"
126
+ end
127
+
128
+ # Return file information for CLI to use
129
+ {
130
+ file_path: file_path,
131
+ absolute_path: absolute_path,
132
+ run_command: run_command,
133
+ test_class_name: parsed_response["test_class_name"]
134
+ }
135
+ end
136
+
137
+ def self.fixture_content
138
+ factory_library = Tng.factory_library || "active_record"
139
+
140
+ case factory_library.downcase
141
+ when "fixtures"
142
+ load_all_fixtures_data
143
+ when "fabrication", "fabricator"
144
+ load_all_fabricator_data
145
+ when "factory_girl", "factory_bot"
146
+ load_all_factory_data
147
+ when "active_record"
148
+ load_all_active_record_data
149
+ else
150
+ puts "⚠️ Warning: Unknown factory library '#{factory_library}'. Using Active Record object creation as fallback."
151
+ load_all_fixtures_data
152
+ end
153
+ end
154
+
155
+ def self.load_all_fixtures_data
156
+ fixture_data = {}
157
+ # TODO: Load proper folder for Rspec. This is only valid for minitest.
158
+ fixtures_dir = Rails.root.join("test", "fixtures")
159
+
160
+ return fixture_data unless Dir.exist?(fixtures_dir)
161
+
162
+ fixture_files = Dir.glob("#{fixtures_dir}/*.yml")
163
+
164
+ fixture_files.each do |fixture_file|
165
+ model_name = File.basename(fixture_file, ".yml")
166
+
167
+ begin
168
+ fixtures = YAML.load_file(fixture_file)
169
+ fixture_data[model_name] = fixtures
170
+ rescue StandardError => e
171
+ puts "⚠️ Warning: Could not load fixture file #{fixture_file}: #{e.message}"
172
+ end
173
+ end
174
+
175
+ fixture_data
176
+ end
177
+
178
+ def self.load_all_fabricator_data
179
+ fabricator_data = {}
180
+ fabricators_dir = Rails.root.join("spec", "fabricators")
181
+
182
+ return fabricator_data unless Dir.exist?(fabricators_dir)
183
+
184
+ Dir.glob("#{fabricators_dir}/*_fabricator.rb").each do |fabricator_file|
185
+ model_name = File.basename(fabricator_file, "_fabricator.rb")
186
+
187
+ begin
188
+ content = File.read(fabricator_file)
189
+ fabricator_data[model_name] = parse_fabricator_structure(content, model_name)
190
+ rescue StandardError => e
191
+ puts "⚠️ Warning: Could not load fabricator file #{fabricator_file}: #{e.message}"
192
+ end
193
+ end
194
+
195
+ fabricator_data
196
+ end
197
+
198
+ def self.load_all_factory_data
199
+ factory_data = {}
200
+ factory_dirs = [
201
+ Rails.root.join("spec", "factories"),
202
+ Rails.root.join("test", "factories")
203
+ ]
204
+
205
+ factory_dirs.each do |factory_dir|
206
+ next unless Dir.exist?(factory_dir)
207
+
208
+ Dir.glob("#{factory_dir}/*.rb").each do |factory_file|
209
+ content = File.read(factory_file)
210
+
211
+ begin
212
+ # Extract all factory definitions from the file
213
+ content.scan(/factory\s+:(\w+)\s+do/) do |match|
214
+ model_name = match[0]
215
+ factory_data[model_name] = parse_factory_structure(content, model_name)
216
+ end
217
+ rescue StandardError => e
218
+ puts "⚠️ Warning: Could not load factory file #{factory_file}: #{e.message}"
219
+ end
220
+ end
221
+ end
222
+
223
+ factory_data
224
+ end
225
+
226
+ def self.parse_fabricator_structure(content, model_name)
227
+ # Parse fabricator file to extract attribute structure
228
+ attributes = {}
229
+
230
+ # Look for Fabricator definitions
231
+ content.scan(/Fabricator\(:#{model_name}\) do \|f\|(.*?)end/m) do |block|
232
+ block[0].scan(/f\.(\w+)\s+(.+)/) do |attr, value|
233
+ attributes[attr] = parse_attribute_value(value)
234
+ end
235
+ end
236
+
237
+ { "attributes" => attributes, "type" => "fabricator" }
238
+ end
239
+
240
+ def self.parse_factory_structure(content, model_name)
241
+ # Parse factory file to extract attribute structure
242
+ attributes = {}
243
+
244
+ # Look for factory definitions
245
+ content.scan(/factory :#{model_name} do(.*?)end/m) do |block|
246
+ block[0].scan(/(\w+)\s+(.+)/) do |attr, value|
247
+ attributes[attr] = parse_attribute_value(value)
248
+ end
249
+ end
250
+
251
+ { "attributes" => attributes, "type" => "factory" }
252
+ end
253
+
254
+ def self.parse_attribute_value(value)
255
+ # Clean up and parse attribute values
256
+ value.strip.gsub(/^["']|["']$/, "")
257
+ end
258
+
259
+ def self.load_all_active_record_data
260
+ []
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tng
4
+ VERSION = "0.1.0"
5
+ API_ENDPOINT = "http://localhost:3001"
6
+ end
data/lib/tng.rb ADDED
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ $VERBOSE = nil
4
+
5
+ require_relative "tng/version"
6
+ require_relative "tng/utils"
7
+ require_relative "tng/api/http_client"
8
+ require_relative "tng/ui/post_install_box"
9
+ require_relative "tng/services/test_generator"
10
+
11
+ require_relative "tng/railtie" if defined?(Rails)
12
+
13
+ begin
14
+ platform = RUBY_PLATFORM
15
+ binary_name = if platform.include?("darwin") # macOS
16
+ "tng.bundle"
17
+ elsif platform.include?("linux")
18
+ "tng.so"
19
+ else
20
+ raise "Unsupported platform: #{platform}"
21
+ end
22
+
23
+ binary_paths = [
24
+ File.expand_path("../binaries/#{binary_name}", __dir__), # From gem root
25
+ File.expand_path("../../binaries/#{binary_name}", __FILE__), # Alternative path
26
+ File.expand_path("binaries/#{binary_name}", __dir__) # Direct from lib
27
+ ]
28
+
29
+ loaded = false
30
+ binary_paths.each do |path|
31
+ next unless File.exist?(path)
32
+
33
+ require path
34
+ loaded = true
35
+ break
36
+ end
37
+
38
+ raise LoadError, "Could not find binary at any of: #{binary_paths.join(", ")}" unless loaded
39
+ rescue LoadError => e
40
+ puts "Warning: Could not load native extension: #{e.message}"
41
+ puts "Some functionality may not be available."
42
+ end
43
+
44
+ # Load Ruby analyzers after native binary to ensure Rust classes are available
45
+ # Ruby analyzers depend on Rust classes (e.g., Tng::Analyzer::Controller)
46
+ require_relative "tng/analyzers/controller"
47
+ require_relative "tng/analyzers/model"
48
+ require_relative "tng/analyzers/service"
49
+
50
+ module Tng
51
+ class Error < StandardError; end
52
+
53
+ @config = {
54
+ api_key: nil,
55
+ base_url: "https://app.tng.sh/",
56
+ read_source_code: true,
57
+ read_test_code: true,
58
+ testing_framework: "minitest",
59
+ authentication_enabled: false,
60
+ authorization_library: nil,
61
+ authentication_library: nil,
62
+ authentication_methods: [],
63
+ mock_library: "minitest/mock",
64
+ http_mock_library: "webmock",
65
+ factory_library: "active_record"
66
+ }
67
+
68
+ def self.configure
69
+ yield(self) if block_given?
70
+ end
71
+
72
+ def self.config
73
+ @config
74
+ end
75
+
76
+ def self.api_key=(value)
77
+ @config[:api_key] = value
78
+ end
79
+
80
+ def self.api_key
81
+ @config[:api_key]
82
+ end
83
+
84
+ def self.base_url=(value)
85
+ @config[:base_url] = value
86
+ end
87
+
88
+ def self.base_url
89
+ @config[:base_url]
90
+ end
91
+
92
+ def self.read_source_code=(value)
93
+ @config[:read_source_code] = value
94
+ end
95
+
96
+ def self.read_source_code
97
+ @config[:read_source_code]
98
+ end
99
+
100
+ def self.read_test_code=(value)
101
+ @config[:read_test_code] = value
102
+ end
103
+
104
+ def self.read_test_code
105
+ @config[:read_test_code]
106
+ end
107
+
108
+ def self.testing_framework=(value)
109
+ @config[:testing_framework] = value
110
+ initialize_framework_defaults(value)
111
+ end
112
+
113
+ def self.testing_framework
114
+ @config[:testing_framework]
115
+ end
116
+
117
+ def self.authentication_enabled=(value)
118
+ @config[:authentication_enabled] = value
119
+ end
120
+
121
+ def self.authentication_enabled
122
+ @config[:authentication_enabled]
123
+ end
124
+
125
+ def self.authentication_library=(value)
126
+ @config[:authentication_library] = value
127
+ end
128
+
129
+ def self.authentication_library
130
+ @config[:authentication_library]
131
+ end
132
+
133
+ def self.authorization_library=(value)
134
+ @config[:authorization_library] = value
135
+ end
136
+
137
+ def self.authorization_library
138
+ @config[:authorization_library]
139
+ end
140
+
141
+ def self.authentication_methods=(value)
142
+ @config[:authentication_methods] = value
143
+ end
144
+
145
+ def self.authentication_methods
146
+ @config[:authentication_methods]
147
+ end
148
+
149
+ def self.mock_library=(value)
150
+ @config[:mock_library] = value
151
+ end
152
+
153
+ def self.mock_library
154
+ @config[:mock_library]
155
+ end
156
+
157
+ def self.http_mock_library=(value)
158
+ @config[:http_mock_library] = value
159
+ end
160
+
161
+ def self.http_mock_library
162
+ @config[:http_mock_library]
163
+ end
164
+
165
+ def self.factory_library=(value)
166
+ @config[:factory_library] = value
167
+ end
168
+
169
+ def self.factory_library
170
+ @config[:factory_library]
171
+ end
172
+
173
+ def self.test_style=(value)
174
+ @config[:test_style] = value
175
+ end
176
+
177
+ def self.test_style
178
+ @config[:test_style]
179
+ end
180
+
181
+ def self.setup_style=(value)
182
+ @config[:setup_style] = value
183
+ end
184
+
185
+ def self.setup_style
186
+ @config[:setup_style]
187
+ end
188
+
189
+ def self.assertion_style=(value)
190
+ @config[:assertion_style] = value
191
+ end
192
+
193
+ def self.assertion_style
194
+ @config[:assertion_style]
195
+ end
196
+
197
+ def self.teardown_style=(value)
198
+ @config[:teardown_style] = value
199
+ end
200
+
201
+ def self.teardown_style
202
+ @config[:teardown_style]
203
+ end
204
+
205
+ def self.describe_style=(value)
206
+ @config[:describe_style] = value
207
+ end
208
+
209
+ def self.describe_style
210
+ @config[:describe_style]
211
+ end
212
+
213
+ def self.context_style=(value)
214
+ @config[:context_style] = value
215
+ end
216
+
217
+ def self.context_style
218
+ @config[:context_style]
219
+ end
220
+
221
+ def self.it_style=(value)
222
+ @config[:it_style] = value
223
+ end
224
+
225
+ def self.it_style
226
+ @config[:it_style]
227
+ end
228
+
229
+ def self.before_style=(value)
230
+ @config[:before_style] = value
231
+ end
232
+
233
+ def self.before_style
234
+ @config[:before_style]
235
+ end
236
+
237
+ def self.after_style=(value)
238
+ @config[:after_style] = value
239
+ end
240
+
241
+ def self.after_style
242
+ @config[:after_style]
243
+ end
244
+
245
+ def self.let_style=(value)
246
+ @config[:let_style] = value
247
+ end
248
+
249
+ def self.let_style
250
+ @config[:let_style]
251
+ end
252
+
253
+ def self.subject_style=(value)
254
+ @config[:subject_style] = value
255
+ end
256
+
257
+ def self.subject_style
258
+ @config[:subject_style]
259
+ end
260
+
261
+ def self.initialize_framework_defaults(framework)
262
+ if framework == "minitest"
263
+ @config.merge!({
264
+ test_style: "spec",
265
+ setup_style: false,
266
+ assertion_style: "assert",
267
+ teardown_style: false
268
+ })
269
+ @config.delete(:describe_style)
270
+ @config.delete(:context_style)
271
+ @config.delete(:it_style)
272
+ @config.delete(:before_style)
273
+ @config.delete(:after_style)
274
+ @config.delete(:let_style)
275
+ @config.delete(:subject_style)
276
+ elsif framework == "rspec"
277
+ @config.merge!({
278
+ describe_style: true,
279
+ context_style: "context",
280
+ it_style: "it",
281
+ before_style: "before",
282
+ after_style: "after",
283
+ let_style: true,
284
+ subject_style: true
285
+ })
286
+ @config.delete(:test_style)
287
+ @config.delete(:setup_style)
288
+ @config.delete(:assertion_style)
289
+ @config.delete(:teardown_style)
290
+ end
291
+ end
292
+
293
+ initialize_framework_defaults(@config[:testing_framework])
294
+ end
data/tng.gemspec ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/tng/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "tng"
7
+ spec.version = Tng::VERSION
8
+ spec.authors = ["claudiug"]
9
+ spec.email = ["claudiu.garba@gmail.com"]
10
+
11
+ spec.summary = "TNG is a library for working with the TNG format."
12
+ spec.description = "TNG is a library for working with the TNG format."
13
+ spec.homepage = "https://github.com/claudiug/tng"
14
+ spec.required_ruby_version = ">= 3.1.0"
15
+ spec.required_rubygems_version = ">= 3.3.11"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/claudiug/tng"
20
+
21
+ # Package pre-compiled binaries and exclude the Rust source code.
22
+ spec.files = Dir.glob("lib/**/*.rb") +
23
+ Dir.glob("binaries/*") +
24
+ Dir.glob("bin/**/*") +
25
+ ["LICENSE.md", "README.md", "Gemfile", "Rakefile", "tng.gemspec"]
26
+
27
+ spec.bindir = "bin"
28
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+ spec.extensions = []
31
+
32
+ spec.add_dependency "httpx", "~> 1.5"
33
+ spec.add_dependency "pastel", "~> 0.8.0"
34
+ spec.add_dependency "prism", "~> 1.4.0"
35
+ spec.add_dependency "rb_sys", "~> 0.9.91"
36
+ spec.add_dependency "tty-box", "~> 0.7"
37
+ spec.add_dependency "tty-file", "~> 0.10"
38
+ spec.add_dependency "tty-option", "~> 0.3"
39
+ spec.add_dependency "tty-progressbar", "~> 0.18.3"
40
+ spec.add_dependency "tty-prompt", "~> 0.23"
41
+ spec.add_dependency "tty-reader", "~> 0.9"
42
+ spec.add_dependency "tty-screen", "~> 0.8"
43
+ spec.add_dependency "tty-spinner", "~> 0.9.3"
44
+ spec.add_dependency "tty-table", "~> 0.12"
45
+
46
+ spec.post_install_message = begin
47
+ require_relative "lib/tng/ui/post_install_box"
48
+ PostInstallBox.new(spec.version).render
49
+ rescue LoadError, StandardError
50
+ "TestNG v#{spec.version} installed successfully!\n" +
51
+ "Run 'rails g tng:install' to get started.\n" +
52
+ "Use 'bundle exec tng --help' for usage information."
53
+ end
54
+ end