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
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tng
4
+ module Analyzers
5
+ class Other
6
+ # Directories to scan for "other" files
7
+ OTHER_DIRECTORIES = %w[
8
+ app/jobs
9
+ app/helpers
10
+ lib
11
+ libs
12
+ app/lib
13
+ app/libs
14
+ app/mailers
15
+ app/channels
16
+ app/decorators
17
+ app/presenters
18
+ app/serializers
19
+ app/policies
20
+ app/forms
21
+ app/queries
22
+ app/graphql
23
+ app/graphql/resolvers
24
+ app/graphql/types
25
+ app/graphql/mutations
26
+ app/graphql/loaders
27
+ app/graphql/schemas
28
+ ].freeze
29
+
30
+ def self.files_in_dir(dir = nil)
31
+ base_dirs = if dir.nil?
32
+ OTHER_DIRECTORIES.select { |d| Dir.exist?(File.join(Dir.pwd, d)) }
33
+ else
34
+ [dir]
35
+ end
36
+
37
+ return [] if base_dirs.empty?
38
+
39
+ base_dirs.flat_map do |base_dir|
40
+ full_path = File.join(Dir.pwd, base_dir)
41
+ find_other_files(full_path, base_dir)
42
+ end
43
+ end
44
+
45
+ def self.value_for_other(file_path)
46
+ raise "file_path is required" if file_path.nil?
47
+
48
+ # For now, use the same parsing logic as services
49
+ # We can enhance this later if needed for specific file types
50
+ Tng::Analyzer::Service.parse_service_file(file_path)
51
+ end
52
+
53
+ def self.read_test_file_for_other(other_path)
54
+ files = find_test_file_for_other(other_path)
55
+
56
+ files.map do |file_path|
57
+ {
58
+ path: file_path,
59
+ content: File.read(file_path)
60
+ }
61
+ end
62
+ end
63
+
64
+ def self.find_test_file_for_other(other_path)
65
+ # Extract the file type and name for test file discovery
66
+ file_type = determine_file_type(other_path)
67
+ file_name = File.basename(other_path, ".rb")
68
+ testing_framework = Tng.testing_framework
69
+ return [] if testing_framework.nil?
70
+
71
+ paths = if testing_framework.downcase == "rspec"
72
+ build_rspec_test_paths(file_type, file_name)
73
+ else
74
+ build_minitest_test_paths(file_type, file_name)
75
+ end
76
+
77
+ paths.select { |path| File.exist?(path) }
78
+ end
79
+
80
+ def self.methods_for_other(other_name, file_path)
81
+ raise "other_name is required" if other_name.nil?
82
+ raise "file_path is required" if file_path.nil?
83
+
84
+ begin
85
+ # Try to load the class/module if possible
86
+ if File.exist?(file_path)
87
+ source_code = File.read(file_path)
88
+ result = Prism.parse(source_code)
89
+
90
+ public_methods = []
91
+ extract_public_method_names(result.value, public_methods, :public)
92
+
93
+ # Return the public methods found in the source file
94
+ public_methods.map { |method_name| { name: method_name.to_s } }
95
+ else
96
+ []
97
+ end
98
+ rescue StandardError => e
99
+ puts "❌ Error analyzing file #{other_name}: #{e.message}"
100
+ []
101
+ end
102
+ end
103
+
104
+ def self.extract_public_method_names(node, methods, current_visibility = :public)
105
+ return unless node.is_a?(Prism::Node)
106
+
107
+ case node
108
+ when Prism::DefNode
109
+ # Only add methods that are public
110
+ methods << node.name.to_s if current_visibility == :public
111
+ when Prism::CallNode
112
+ # Handle visibility modifiers (private, protected, public)
113
+ if node.receiver.nil? && node.arguments.nil?
114
+ case node.name
115
+ when :private
116
+ current_visibility = :private
117
+ when :protected
118
+ current_visibility = :protected
119
+ when :public
120
+ current_visibility = :public
121
+ end
122
+
123
+ end
124
+ when Prism::SingletonClassNode
125
+ # Methods inside class << self blocks - always public by default
126
+ node.body&.child_nodes&.each do |child|
127
+ extract_public_method_names(child, methods, :public)
128
+ end
129
+ end
130
+
131
+ # Recursively process child nodes with current visibility
132
+ node.child_nodes.each do |child|
133
+ extract_public_method_names(child, methods, current_visibility)
134
+ end
135
+ end
136
+
137
+ def self.find_other_files(dir, _base_dir)
138
+ return [] unless Dir.exist?(dir)
139
+
140
+ Dir.glob(File.join(dir, "**", "*.rb")).map do |file_path|
141
+ relative_path = file_path.gsub("#{Dir.pwd}/", "")
142
+ file_type = determine_file_type(file_path)
143
+
144
+ {
145
+ file: File.basename(file_path),
146
+ path: file_path,
147
+ relative_path: relative_path,
148
+ type: file_type
149
+ }
150
+ end
151
+ end
152
+
153
+ def self.determine_file_type(file_path)
154
+ case file_path
155
+ when %r{app/jobs}
156
+ "job"
157
+ when %r{app/helpers}
158
+ "helper"
159
+ when %r{(?:app/)?libs?(?:/|$)}
160
+ "lib"
161
+ when %r{app/mailers}
162
+ "mailer"
163
+ when %r{app/channels}
164
+ "channel"
165
+ when %r{app/decorators}
166
+ "decorator"
167
+ when %r{app/presenters}
168
+ "presenter"
169
+ when %r{app/serializers}
170
+ "serializer"
171
+ when %r{app/policies}
172
+ "policy"
173
+ when %r{app/forms}
174
+ "form"
175
+ when %r{app/queries}
176
+ "query"
177
+ when %r{app/graphql/resolvers}
178
+ "resolver"
179
+ when %r{app/graphql/types}
180
+ "type"
181
+ when %r{app/graphql/mutations}
182
+ "mutation"
183
+ when %r{app/graphql/loaders}
184
+ "loader"
185
+ when %r{app/graphql/schemas}
186
+ "schema"
187
+ when %r{app/graphql}
188
+ "graphql"
189
+ else
190
+ "other"
191
+ end
192
+ end
193
+
194
+ def self.build_rspec_test_paths(file_type, file_name)
195
+ case file_type
196
+ when "job"
197
+ ["spec/jobs/#{file_name}_spec.rb"]
198
+ when "helper"
199
+ ["spec/helpers/#{file_name}_spec.rb"]
200
+ when "lib"
201
+ ["spec/lib/#{file_name}_spec.rb"]
202
+ when "mailer"
203
+ ["spec/mailers/#{file_name}_spec.rb"]
204
+ when "channel"
205
+ ["spec/channels/#{file_name}_spec.rb"]
206
+ when "query"
207
+ ["spec/queries/#{file_name}_spec.rb"]
208
+ when "decorator"
209
+ ["spec/decorators/#{file_name}_spec.rb"]
210
+ when "presenter"
211
+ ["spec/presenters/#{file_name}_spec.rb"]
212
+ when "serializer"
213
+ ["spec/serializers/#{file_name}_spec.rb"]
214
+ when "policy"
215
+ ["spec/policies/#{file_name}_spec.rb"]
216
+ when "form"
217
+ ["spec/forms/#{file_name}_spec.rb"]
218
+ when "resolver"
219
+ ["spec/graphql/resolvers/#{file_name}_spec.rb"]
220
+ when "type"
221
+ ["spec/graphql/types/#{file_name}_spec.rb"]
222
+ when "mutation"
223
+ ["spec/graphql/mutations/#{file_name}_spec.rb"]
224
+ when "loader"
225
+ ["spec/graphql/loaders/#{file_name}_spec.rb"]
226
+ when "schema"
227
+ ["spec/graphql/schemas/#{file_name}_spec.rb"]
228
+ when "graphql"
229
+ ["spec/graphql/#{file_name}_spec.rb"]
230
+ else
231
+ ["spec/#{file_type}s/#{file_name}_spec.rb"]
232
+ end
233
+ end
234
+
235
+ def self.build_minitest_test_paths(file_type, file_name)
236
+ case file_type
237
+ when "job"
238
+ ["test/jobs/#{file_name}_test.rb"]
239
+ when "helper"
240
+ ["test/helpers/#{file_name}_test.rb"]
241
+ when "lib"
242
+ ["test/lib/#{file_name}_test.rb"]
243
+ when "mailer"
244
+ ["test/mailers/#{file_name}_test.rb"]
245
+ when "channel"
246
+ ["test/channels/#{file_name}_test.rb"]
247
+ when "query"
248
+ ["test/queries/#{file_name}_test.rb"]
249
+ when "decorator"
250
+ ["test/decorators/#{file_name}_test.rb"]
251
+ when "presenter"
252
+ ["test/presenters/#{file_name}_test.rb"]
253
+ when "serializer"
254
+ ["test/serializers/#{file_name}_test.rb"]
255
+ when "policy"
256
+ ["test/policies/#{file_name}_test.rb"]
257
+ when "form"
258
+ ["test/forms/#{file_name}_test.rb"]
259
+ when "resolver"
260
+ ["test/graphql/resolvers/#{file_name}_test.rb"]
261
+ when "type"
262
+ ["test/graphql/types/#{file_name}_test.rb"]
263
+ when "mutation"
264
+ ["test/graphql/mutations/#{file_name}_test.rb"]
265
+ when "loader"
266
+ ["test/graphql/loaders/#{file_name}_test.rb"]
267
+ when "schema"
268
+ ["test/graphql/schemas/#{file_name}_test.rb"]
269
+ when "graphql"
270
+ ["test/graphql/#{file_name}_test.rb"]
271
+ else
272
+ ["test/#{file_type}s/#{file_name}_test.rb"]
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,150 @@
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
+
124
+ when Prism::SingletonClassNode
125
+ # Methods inside class << self blocks
126
+ node.body&.child_nodes&.each do |child|
127
+ extract_method_names(child, methods)
128
+ end
129
+ end
130
+
131
+ node.child_nodes.each do |child|
132
+ extract_method_names(child, methods)
133
+ end
134
+ end
135
+
136
+ def self.find_service_files(dir)
137
+ return [] unless Dir.exist?(dir)
138
+
139
+ Dir.glob(File.join(dir, "**", "*.rb")).map do |file_path|
140
+ relative_path = file_path.gsub("#{Dir.pwd}/", "")
141
+ {
142
+ file: File.basename(file_path),
143
+ path: file_path,
144
+ relative_path: relative_path
145
+ }
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,100 @@
1
+ require "httpx"
2
+
3
+ module Tng
4
+ class HttpClient
5
+ def initialize(api_endpoint, api_key)
6
+ @api_endpoint = api_endpoint
7
+ @api_key = api_key
8
+ @timeout = {
9
+ connect_timeout: 300,
10
+ read_timeout: 300,
11
+ write_timeout: 300,
12
+ request_timeout: 300
13
+ }
14
+ end
15
+
16
+ def post(path, payload: {}, headers: {})
17
+ merged_headers = json_default_headers.merge(headers)
18
+
19
+ response = HTTPX.with(timeout: @timeout).post(
20
+ "#{@api_endpoint}/#{path}",
21
+ json: payload,
22
+ headers: merged_headers
23
+ )
24
+
25
+ debug_response("POST #{path}", response) if debug_enabled?
26
+ response
27
+ end
28
+
29
+ def post_binary(path, data, headers: {})
30
+ merged_headers = stream_default_headers.merge(headers)
31
+
32
+ response = HTTPX.with(timeout: @timeout).post(
33
+ "#{@api_endpoint}/#{path}",
34
+ body: data,
35
+ headers: merged_headers
36
+ )
37
+
38
+ debug_response("POST #{path} (binary)", response) if debug_enabled?
39
+ response
40
+ end
41
+
42
+ def get(path, headers: {})
43
+ merged_headers = json_default_headers.merge(headers)
44
+
45
+ response = HTTPX.with(timeout: @timeout).get(
46
+ "#{@api_endpoint}/#{path}",
47
+ headers: merged_headers
48
+ )
49
+
50
+ debug_response("GET #{path}", response) if debug_enabled?
51
+ response
52
+ end
53
+
54
+ def ping
55
+ response = HTTPX.with(timeout: @timeout).get("#{@api_endpoint}/ping")
56
+ debug_response("GET /ping", response) if debug_enabled?
57
+ response
58
+ end
59
+
60
+ private
61
+
62
+ def stream_default_headers
63
+ {
64
+ "Content-Type" => "application/octet-stream",
65
+ "Authorization" => "Bearer #{@api_key}",
66
+ "User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
67
+ }
68
+ end
69
+
70
+ def json_default_headers
71
+ {
72
+ "Content-Type" => "application/json",
73
+ "Authorization" => "Bearer #{@api_key}",
74
+ "User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
75
+ }
76
+ end
77
+
78
+ def debug_enabled?
79
+ ENV["DEBUG"] == "1"
80
+ end
81
+
82
+ def debug_response(request_info, response)
83
+ puts "\n -> DEBUG: #{request_info}"
84
+
85
+ if response.is_a?(HTTPX::ErrorResponse)
86
+ puts " Error: #{response.error&.message}"
87
+ else
88
+ puts " Status: #{response.status}"
89
+ puts " Headers: #{response.headers.to_h}"
90
+ body = response.body.to_s
91
+ if body.length > 500
92
+ puts " Body: #{body[0..500]}... (truncated, total length: #{body.length})"
93
+ else
94
+ puts " Body: #{body}"
95
+ end
96
+ end
97
+ puts " " + "─" * 50
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Tng
6
+ class Railtie < Rails::Railtie
7
+ generators do
8
+ require_relative "../generators/tng/install_generator"
9
+ end
10
+ end
11
+ end