class-metrix 0.1.2
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/.rspec +3 -0
- data/.rubocop.yml +88 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +417 -0
- data/RELEASE_GUIDE.md +158 -0
- data/Rakefile +12 -0
- data/examples/README.md +155 -0
- data/examples/advanced/error_handling.rb +199 -0
- data/examples/advanced/hash_expansion.rb +180 -0
- data/examples/basic/01_simple_constants.rb +56 -0
- data/examples/basic/02_simple_methods.rb +99 -0
- data/examples/basic/03_multi_type_extraction.rb +116 -0
- data/examples/components/configurable_reports.rb +201 -0
- data/examples/csv_output_demo.rb +237 -0
- data/examples/real_world/microservices_audit.rb +312 -0
- data/lib/class_metrix/extractor.rb +121 -0
- data/lib/class_metrix/extractors/constants_extractor.rb +87 -0
- data/lib/class_metrix/extractors/methods_extractor.rb +87 -0
- data/lib/class_metrix/extractors/multi_type_extractor.rb +66 -0
- data/lib/class_metrix/formatters/base/base_component.rb +62 -0
- data/lib/class_metrix/formatters/base/base_formatter.rb +93 -0
- data/lib/class_metrix/formatters/components/footer_component.rb +67 -0
- data/lib/class_metrix/formatters/components/generic_header_component.rb +87 -0
- data/lib/class_metrix/formatters/components/header_component.rb +92 -0
- data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +140 -0
- data/lib/class_metrix/formatters/components/table_component.rb +268 -0
- data/lib/class_metrix/formatters/csv_formatter.rb +98 -0
- data/lib/class_metrix/formatters/markdown_formatter.rb +184 -0
- data/lib/class_metrix/formatters/shared/csv_table_builder.rb +21 -0
- data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +97 -0
- data/lib/class_metrix/formatters/shared/table_builder.rb +267 -0
- data/lib/class_metrix/formatters/shared/value_processor.rb +78 -0
- data/lib/class_metrix/processors/value_processor.rb +40 -0
- data/lib/class_metrix/utils/class_resolver.rb +20 -0
- data/lib/class_metrix/version.rb +5 -0
- data/lib/class_metrix.rb +12 -0
- data/sig/class/metrix.rbs +6 -0
- metadata +118 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../../lib/class_metrix"
|
|
5
|
+
|
|
6
|
+
puts "=== Real-World Example: Microservices Configuration Audit ==="
|
|
7
|
+
puts
|
|
8
|
+
|
|
9
|
+
# Realistic microservice configuration classes
|
|
10
|
+
class UserService
|
|
11
|
+
# Service metadata
|
|
12
|
+
SERVICE_NAME = "user-service"
|
|
13
|
+
VERSION = "2.1.0"
|
|
14
|
+
PORT = 8001
|
|
15
|
+
ENVIRONMENT = "production"
|
|
16
|
+
|
|
17
|
+
# Feature flags
|
|
18
|
+
ENABLE_CACHING = true
|
|
19
|
+
ENABLE_METRICS = true
|
|
20
|
+
ENABLE_TRACING = false
|
|
21
|
+
RATE_LIMITING_ENABLED = true
|
|
22
|
+
|
|
23
|
+
# Configuration methods
|
|
24
|
+
def self.database_config
|
|
25
|
+
{
|
|
26
|
+
host: "user-db.internal",
|
|
27
|
+
port: 5432,
|
|
28
|
+
database: "users_production",
|
|
29
|
+
pool_size: 20,
|
|
30
|
+
timeout: 30,
|
|
31
|
+
ssl: true,
|
|
32
|
+
backup_enabled: true
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.redis_config
|
|
37
|
+
{
|
|
38
|
+
host: "user-redis.internal",
|
|
39
|
+
port: 6379,
|
|
40
|
+
database: 0,
|
|
41
|
+
timeout: 5,
|
|
42
|
+
max_connections: 50
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.rate_limit_config
|
|
47
|
+
{
|
|
48
|
+
requests_per_minute: 1000,
|
|
49
|
+
burst_size: 100,
|
|
50
|
+
enabled: true
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.health_status
|
|
55
|
+
"healthy"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.memory_usage_mb
|
|
59
|
+
512
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.active_connections
|
|
63
|
+
45
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class OrderService
|
|
68
|
+
# Service metadata
|
|
69
|
+
SERVICE_NAME = "order-service"
|
|
70
|
+
VERSION = "1.8.5"
|
|
71
|
+
PORT = 8002
|
|
72
|
+
ENVIRONMENT = "production"
|
|
73
|
+
|
|
74
|
+
# Feature flags
|
|
75
|
+
ENABLE_CACHING = true
|
|
76
|
+
ENABLE_METRICS = true
|
|
77
|
+
ENABLE_TRACING = true
|
|
78
|
+
RATE_LIMITING_ENABLED = true
|
|
79
|
+
|
|
80
|
+
# Configuration methods
|
|
81
|
+
def self.database_config
|
|
82
|
+
{
|
|
83
|
+
host: "order-db.internal",
|
|
84
|
+
port: 5432,
|
|
85
|
+
database: "orders_production",
|
|
86
|
+
pool_size: 30,
|
|
87
|
+
timeout: 45,
|
|
88
|
+
ssl: true,
|
|
89
|
+
backup_enabled: true,
|
|
90
|
+
read_replicas: 2
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.redis_config
|
|
95
|
+
{
|
|
96
|
+
host: "order-redis.internal",
|
|
97
|
+
port: 6379,
|
|
98
|
+
database: 1,
|
|
99
|
+
timeout: 3,
|
|
100
|
+
max_connections: 100,
|
|
101
|
+
cluster_enabled: true
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.rate_limit_config
|
|
106
|
+
{
|
|
107
|
+
requests_per_minute: 500,
|
|
108
|
+
burst_size: 50,
|
|
109
|
+
enabled: true,
|
|
110
|
+
premium_multiplier: 2.0
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.health_status
|
|
115
|
+
"healthy"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.memory_usage_mb
|
|
119
|
+
768
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.active_connections
|
|
123
|
+
127
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.queue_size
|
|
127
|
+
42
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class PaymentService
|
|
132
|
+
# Service metadata
|
|
133
|
+
SERVICE_NAME = "payment-service"
|
|
134
|
+
VERSION = "3.0.1"
|
|
135
|
+
PORT = 8003
|
|
136
|
+
ENVIRONMENT = "production"
|
|
137
|
+
|
|
138
|
+
# Feature flags
|
|
139
|
+
ENABLE_CACHING = false # Disabled for security
|
|
140
|
+
ENABLE_METRICS = true
|
|
141
|
+
ENABLE_TRACING = true
|
|
142
|
+
RATE_LIMITING_ENABLED = true
|
|
143
|
+
|
|
144
|
+
# Configuration methods
|
|
145
|
+
def self.database_config
|
|
146
|
+
{
|
|
147
|
+
host: "payment-db.internal",
|
|
148
|
+
port: 5432,
|
|
149
|
+
database: "payments_production",
|
|
150
|
+
pool_size: 15,
|
|
151
|
+
timeout: 60,
|
|
152
|
+
ssl: true,
|
|
153
|
+
backup_enabled: true,
|
|
154
|
+
encryption: "AES-256"
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.redis_config
|
|
159
|
+
{
|
|
160
|
+
host: "payment-redis.internal",
|
|
161
|
+
port: 6379,
|
|
162
|
+
database: 2,
|
|
163
|
+
timeout: 2,
|
|
164
|
+
max_connections: 25,
|
|
165
|
+
ssl: true
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.rate_limit_config
|
|
170
|
+
{
|
|
171
|
+
requests_per_minute: 100,
|
|
172
|
+
burst_size: 10,
|
|
173
|
+
enabled: true,
|
|
174
|
+
strict_mode: true
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.health_status
|
|
179
|
+
"healthy"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.memory_usage_mb
|
|
183
|
+
256
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.active_connections
|
|
187
|
+
12
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.encryption_status
|
|
191
|
+
"AES-256-GCM"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
puts "=== Microservices Defined ==="
|
|
196
|
+
puts "UserService, OrderService, PaymentService"
|
|
197
|
+
puts "Each service has metadata, feature flags, and configuration methods"
|
|
198
|
+
puts
|
|
199
|
+
|
|
200
|
+
# 1. Service Overview
|
|
201
|
+
puts "🏢 1. SERVICE OVERVIEW"
|
|
202
|
+
puts "=" * 70
|
|
203
|
+
result = ClassMetrix.extract(:constants)
|
|
204
|
+
.from([UserService, OrderService, PaymentService])
|
|
205
|
+
.filter(/SERVICE_NAME|VERSION|PORT|ENVIRONMENT/)
|
|
206
|
+
.to_markdown
|
|
207
|
+
|
|
208
|
+
puts result
|
|
209
|
+
puts
|
|
210
|
+
|
|
211
|
+
# 2. Feature Flags Comparison
|
|
212
|
+
puts "🚩 2. FEATURE FLAGS COMPARISON"
|
|
213
|
+
puts "=" * 70
|
|
214
|
+
result = ClassMetrix.extract(:constants)
|
|
215
|
+
.from([UserService, OrderService, PaymentService])
|
|
216
|
+
.filter(/ENABLE_|RATE_LIMITING/)
|
|
217
|
+
.to_markdown
|
|
218
|
+
|
|
219
|
+
puts result
|
|
220
|
+
puts
|
|
221
|
+
|
|
222
|
+
# 3. Database Configuration Analysis
|
|
223
|
+
puts "🗄️ 3. DATABASE CONFIGURATION ANALYSIS"
|
|
224
|
+
puts "=" * 70
|
|
225
|
+
result = ClassMetrix.extract(:class_methods)
|
|
226
|
+
.from([UserService, OrderService, PaymentService])
|
|
227
|
+
.filter(/database_config/)
|
|
228
|
+
.expand_hashes
|
|
229
|
+
.to_markdown
|
|
230
|
+
|
|
231
|
+
puts result
|
|
232
|
+
puts
|
|
233
|
+
|
|
234
|
+
# 4. Redis Configuration Comparison
|
|
235
|
+
puts "🔴 4. REDIS CONFIGURATION COMPARISON"
|
|
236
|
+
puts "=" * 70
|
|
237
|
+
result = ClassMetrix.extract(:class_methods)
|
|
238
|
+
.from([UserService, OrderService, PaymentService])
|
|
239
|
+
.filter(/redis_config/)
|
|
240
|
+
.expand_hashes
|
|
241
|
+
.to_markdown
|
|
242
|
+
|
|
243
|
+
puts result
|
|
244
|
+
puts
|
|
245
|
+
|
|
246
|
+
# 5. Rate Limiting Configuration
|
|
247
|
+
puts "⚡ 5. RATE LIMITING CONFIGURATION"
|
|
248
|
+
puts "=" * 70
|
|
249
|
+
result = ClassMetrix.extract(:class_methods)
|
|
250
|
+
.from([UserService, OrderService, PaymentService])
|
|
251
|
+
.filter(/rate_limit_config/)
|
|
252
|
+
.expand_hashes
|
|
253
|
+
.to_markdown
|
|
254
|
+
|
|
255
|
+
puts result
|
|
256
|
+
puts
|
|
257
|
+
|
|
258
|
+
# 6. Service Health & Performance
|
|
259
|
+
puts "📊 6. SERVICE HEALTH & PERFORMANCE METRICS"
|
|
260
|
+
puts "=" * 70
|
|
261
|
+
result = ClassMetrix.extract(:class_methods)
|
|
262
|
+
.from([UserService, OrderService, PaymentService])
|
|
263
|
+
.filter(/health_status|memory_usage|active_connections/)
|
|
264
|
+
.to_markdown
|
|
265
|
+
|
|
266
|
+
puts result
|
|
267
|
+
puts
|
|
268
|
+
|
|
269
|
+
# 7. Security Features
|
|
270
|
+
puts "🔒 7. SECURITY FEATURES ANALYSIS"
|
|
271
|
+
puts "=" * 70
|
|
272
|
+
result = ClassMetrix.extract(:constants, :class_methods)
|
|
273
|
+
.from([UserService, OrderService, PaymentService])
|
|
274
|
+
.filter(/CACHING|ssl|encryption/)
|
|
275
|
+
.to_markdown
|
|
276
|
+
|
|
277
|
+
puts result
|
|
278
|
+
puts
|
|
279
|
+
|
|
280
|
+
# 8. Complete Configuration Audit
|
|
281
|
+
puts "📋 8. COMPLETE CONFIGURATION AUDIT"
|
|
282
|
+
puts "=" * 70
|
|
283
|
+
result = ClassMetrix.extract(:constants, :class_methods)
|
|
284
|
+
.from([UserService, OrderService, PaymentService])
|
|
285
|
+
.handle_errors
|
|
286
|
+
.expand_hashes
|
|
287
|
+
.to_markdown
|
|
288
|
+
|
|
289
|
+
puts "Full audit contains #{result.lines.count} lines"
|
|
290
|
+
puts "Saving to microservices_audit_report.md..."
|
|
291
|
+
|
|
292
|
+
# Save comprehensive report
|
|
293
|
+
ClassMetrix.extract(:constants, :class_methods)
|
|
294
|
+
.from([UserService, OrderService, PaymentService])
|
|
295
|
+
.handle_errors
|
|
296
|
+
.expand_hashes
|
|
297
|
+
.to_markdown("microservices_audit_report.md")
|
|
298
|
+
|
|
299
|
+
puts "✅ Complete audit saved to: microservices_audit_report.md"
|
|
300
|
+
puts
|
|
301
|
+
|
|
302
|
+
puts "🎯 Real-World Use Cases Demonstrated:"
|
|
303
|
+
puts "• Service metadata comparison across microservices"
|
|
304
|
+
puts "• Feature flag consistency analysis"
|
|
305
|
+
puts "• Database configuration standardization check"
|
|
306
|
+
puts "• Cache configuration comparison"
|
|
307
|
+
puts "• Rate limiting policy analysis"
|
|
308
|
+
puts "• Performance metrics monitoring"
|
|
309
|
+
puts "• Security configuration audit"
|
|
310
|
+
puts "• Complete infrastructure overview"
|
|
311
|
+
puts "• Configuration drift detection"
|
|
312
|
+
puts "• Compliance reporting"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "extractors/constants_extractor"
|
|
4
|
+
require_relative "extractors/methods_extractor"
|
|
5
|
+
require_relative "extractors/multi_type_extractor"
|
|
6
|
+
require_relative "formatters/markdown_formatter"
|
|
7
|
+
require_relative "formatters/csv_formatter"
|
|
8
|
+
require_relative "utils/class_resolver"
|
|
9
|
+
|
|
10
|
+
module ClassMetrix
|
|
11
|
+
class Extractor
|
|
12
|
+
def initialize(*types)
|
|
13
|
+
@types = types.flatten
|
|
14
|
+
@classes = []
|
|
15
|
+
@filters = []
|
|
16
|
+
@expand_hashes = false
|
|
17
|
+
@handle_errors = false
|
|
18
|
+
@modules = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def from(classes)
|
|
22
|
+
@classes = ClassResolver.normalize_classes(classes)
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def filter(pattern)
|
|
27
|
+
@filters << pattern
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def expand_hashes
|
|
32
|
+
@expand_hashes = true
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_errors
|
|
37
|
+
@handle_errors = true
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def modules(module_list)
|
|
42
|
+
@modules = module_list
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_markdown(filename = nil, **options)
|
|
47
|
+
data = extract_all_data
|
|
48
|
+
|
|
49
|
+
# Merge default options with passed options
|
|
50
|
+
format_options = {
|
|
51
|
+
extraction_types: @types,
|
|
52
|
+
show_missing_summary: false,
|
|
53
|
+
show_footer: true,
|
|
54
|
+
footer_style: :default,
|
|
55
|
+
show_timestamp: false,
|
|
56
|
+
show_metadata: true,
|
|
57
|
+
show_classes: true,
|
|
58
|
+
show_extraction_info: true,
|
|
59
|
+
table_style: :standard,
|
|
60
|
+
summary_style: :grouped
|
|
61
|
+
}.merge(options)
|
|
62
|
+
|
|
63
|
+
formatted = MarkdownFormatter.new(data, @expand_hashes, format_options).format
|
|
64
|
+
|
|
65
|
+
File.write(filename, formatted) if filename
|
|
66
|
+
formatted
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_csv(filename = nil, **options)
|
|
70
|
+
data = extract_all_data
|
|
71
|
+
|
|
72
|
+
# Merge default options with passed options
|
|
73
|
+
format_options = {
|
|
74
|
+
extraction_types: @types,
|
|
75
|
+
show_metadata: true,
|
|
76
|
+
separator: ",",
|
|
77
|
+
quote_char: '"',
|
|
78
|
+
flatten_hashes: true,
|
|
79
|
+
null_value: "",
|
|
80
|
+
comment_char: "#"
|
|
81
|
+
}.merge(options)
|
|
82
|
+
|
|
83
|
+
formatted = CsvFormatter.new(data, @expand_hashes, format_options).format
|
|
84
|
+
|
|
85
|
+
File.write(filename, formatted) if filename
|
|
86
|
+
formatted
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def extract_all_data
|
|
92
|
+
# Handle single or multiple extraction types
|
|
93
|
+
if @types.size == 1
|
|
94
|
+
extract_single_type(@types.first)
|
|
95
|
+
else
|
|
96
|
+
extract_multiple_types
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extract_single_type(type)
|
|
101
|
+
extractor = get_extractor(type)
|
|
102
|
+
extractor.extract
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def extract_multiple_types
|
|
106
|
+
# Combine multiple extraction types into one table
|
|
107
|
+
MultiTypeExtractor.new(@classes, @types, @filters, @modules, @handle_errors).extract
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def get_extractor(type)
|
|
111
|
+
case type
|
|
112
|
+
when :constants
|
|
113
|
+
ConstantsExtractor.new(@classes, @filters, @handle_errors)
|
|
114
|
+
when :class_methods
|
|
115
|
+
MethodsExtractor.new(@classes, @filters, @handle_errors)
|
|
116
|
+
else
|
|
117
|
+
raise ArgumentError, "Unknown extraction type: #{type}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../processors/value_processor"
|
|
4
|
+
|
|
5
|
+
module ClassMetrix
|
|
6
|
+
class ConstantsExtractor
|
|
7
|
+
def initialize(classes, filters, handle_errors)
|
|
8
|
+
@classes = classes
|
|
9
|
+
@filters = filters
|
|
10
|
+
@handle_errors = handle_errors
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def extract
|
|
14
|
+
return { headers: [], rows: [] } if @classes.empty?
|
|
15
|
+
|
|
16
|
+
# Get all constant names across all classes
|
|
17
|
+
constant_names = get_all_constant_names
|
|
18
|
+
|
|
19
|
+
# Apply filters
|
|
20
|
+
constant_names = apply_filters(constant_names)
|
|
21
|
+
|
|
22
|
+
# Build headers: ["Constant", "Class1", "Class2", ...]
|
|
23
|
+
headers = ["Constant"] + @classes.map(&:name)
|
|
24
|
+
|
|
25
|
+
# Build rows: each row represents one constant across all classes
|
|
26
|
+
rows = constant_names.map do |const_name|
|
|
27
|
+
row = [const_name]
|
|
28
|
+
|
|
29
|
+
@classes.each do |klass|
|
|
30
|
+
value = extract_constant_value(klass, const_name)
|
|
31
|
+
# Pass the raw value for hash expansion to work properly
|
|
32
|
+
row << value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
row
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{ headers: headers, rows: rows }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def get_all_constant_names
|
|
44
|
+
all_constants = Set.new
|
|
45
|
+
|
|
46
|
+
@classes.each do |klass|
|
|
47
|
+
# Get constants defined directly in this class (not inherited)
|
|
48
|
+
class_constants = klass.constants(false)
|
|
49
|
+
all_constants.merge(class_constants.map(&:to_s))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
all_constants.to_a.sort
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def apply_filters(constant_names)
|
|
56
|
+
return constant_names if @filters.empty?
|
|
57
|
+
|
|
58
|
+
@filters.each do |filter|
|
|
59
|
+
constant_names = constant_names.select do |name|
|
|
60
|
+
case filter
|
|
61
|
+
when Regexp
|
|
62
|
+
name.match?(filter)
|
|
63
|
+
when String
|
|
64
|
+
name.include?(filter)
|
|
65
|
+
else
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
constant_names
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_constant_value(klass, const_name)
|
|
75
|
+
# Check if constant exists before trying to get it
|
|
76
|
+
if klass.const_defined?(const_name, false)
|
|
77
|
+
klass.const_get(const_name)
|
|
78
|
+
else
|
|
79
|
+
@handle_errors ? ValueProcessor.missing_constant : nil
|
|
80
|
+
end
|
|
81
|
+
rescue NameError => e
|
|
82
|
+
@handle_errors ? ValueProcessor.handle_extraction_error(e) : (raise e)
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
@handle_errors ? ValueProcessor.handle_extraction_error(e) : (raise e)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../processors/value_processor"
|
|
4
|
+
|
|
5
|
+
module ClassMetrix
|
|
6
|
+
class MethodsExtractor
|
|
7
|
+
def initialize(classes, filters, handle_errors)
|
|
8
|
+
@classes = classes
|
|
9
|
+
@filters = filters
|
|
10
|
+
@handle_errors = handle_errors
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def extract
|
|
14
|
+
return { headers: [], rows: [] } if @classes.empty?
|
|
15
|
+
|
|
16
|
+
# Get all class method names across all classes
|
|
17
|
+
method_names = get_all_class_method_names
|
|
18
|
+
|
|
19
|
+
# Apply filters
|
|
20
|
+
method_names = apply_filters(method_names)
|
|
21
|
+
|
|
22
|
+
# Build headers: ["Method", "Class1", "Class2", ...]
|
|
23
|
+
headers = ["Method"] + @classes.map(&:name)
|
|
24
|
+
|
|
25
|
+
# Build rows: each row represents one method across all classes
|
|
26
|
+
rows = method_names.map do |method_name|
|
|
27
|
+
row = [method_name]
|
|
28
|
+
|
|
29
|
+
@classes.each do |klass|
|
|
30
|
+
value = call_class_method(klass, method_name)
|
|
31
|
+
# Pass the raw value for hash expansion to work properly
|
|
32
|
+
row << value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
row
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{ headers: headers, rows: rows }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def get_all_class_method_names
|
|
44
|
+
all_methods = Set.new
|
|
45
|
+
|
|
46
|
+
@classes.each do |klass|
|
|
47
|
+
# Get class methods (singleton methods)
|
|
48
|
+
class_methods = klass.singleton_methods(false)
|
|
49
|
+
all_methods.merge(class_methods.map(&:to_s))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
all_methods.to_a.sort
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def apply_filters(method_names)
|
|
56
|
+
return method_names if @filters.empty?
|
|
57
|
+
|
|
58
|
+
@filters.each do |filter|
|
|
59
|
+
method_names = method_names.select do |name|
|
|
60
|
+
case filter
|
|
61
|
+
when Regexp
|
|
62
|
+
name.match?(filter)
|
|
63
|
+
when String
|
|
64
|
+
name.include?(filter)
|
|
65
|
+
else
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
method_names
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def call_class_method(klass, method_name)
|
|
75
|
+
# Check if the method exists and is callable
|
|
76
|
+
if klass.respond_to?(method_name, true)
|
|
77
|
+
klass.public_send(method_name)
|
|
78
|
+
else
|
|
79
|
+
@handle_errors ? ValueProcessor.missing_method : nil
|
|
80
|
+
end
|
|
81
|
+
rescue NoMethodError => e
|
|
82
|
+
@handle_errors ? ValueProcessor.handle_extraction_error(e) : (raise e)
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
@handle_errors ? ValueProcessor.handle_extraction_error(e) : (raise e)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants_extractor"
|
|
4
|
+
require_relative "methods_extractor"
|
|
5
|
+
|
|
6
|
+
module ClassMetrix
|
|
7
|
+
class MultiTypeExtractor
|
|
8
|
+
def initialize(classes, types, filters, modules, handle_errors)
|
|
9
|
+
@classes = classes
|
|
10
|
+
@types = types
|
|
11
|
+
@filters = filters
|
|
12
|
+
@modules = modules
|
|
13
|
+
@handle_errors = handle_errors
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def extract
|
|
17
|
+
return { headers: [], rows: [] } if @classes.empty? || @types.empty?
|
|
18
|
+
|
|
19
|
+
# Build headers: ["Type", "Behavior", "Class1", "Class2", ...]
|
|
20
|
+
headers = %w[Type Behavior] + @classes.map(&:name)
|
|
21
|
+
|
|
22
|
+
all_rows = []
|
|
23
|
+
|
|
24
|
+
@types.each do |type|
|
|
25
|
+
type_data = extract_single_type(type)
|
|
26
|
+
|
|
27
|
+
# Add rows with type prefix
|
|
28
|
+
type_data[:rows].each do |row|
|
|
29
|
+
behavior_name = row[0]
|
|
30
|
+
values = row[1..]
|
|
31
|
+
|
|
32
|
+
new_row = [type_label(type), behavior_name] + values
|
|
33
|
+
all_rows << new_row
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{ headers: headers, rows: all_rows }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def extract_single_type(type)
|
|
43
|
+
case type
|
|
44
|
+
when :constants
|
|
45
|
+
ConstantsExtractor.new(@classes, @filters, @handle_errors).extract
|
|
46
|
+
when :class_methods
|
|
47
|
+
MethodsExtractor.new(@classes, @filters, @handle_errors).extract
|
|
48
|
+
else
|
|
49
|
+
{ headers: [], rows: [] }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def type_label(type)
|
|
54
|
+
case type
|
|
55
|
+
when :constants
|
|
56
|
+
"Constant"
|
|
57
|
+
when :class_methods
|
|
58
|
+
"Class Method"
|
|
59
|
+
when :module_methods
|
|
60
|
+
"Module Method"
|
|
61
|
+
else
|
|
62
|
+
type.to_s.split("_").map(&:capitalize).join(" ")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|