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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +88 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +41 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +417 -0
  8. data/RELEASE_GUIDE.md +158 -0
  9. data/Rakefile +12 -0
  10. data/examples/README.md +155 -0
  11. data/examples/advanced/error_handling.rb +199 -0
  12. data/examples/advanced/hash_expansion.rb +180 -0
  13. data/examples/basic/01_simple_constants.rb +56 -0
  14. data/examples/basic/02_simple_methods.rb +99 -0
  15. data/examples/basic/03_multi_type_extraction.rb +116 -0
  16. data/examples/components/configurable_reports.rb +201 -0
  17. data/examples/csv_output_demo.rb +237 -0
  18. data/examples/real_world/microservices_audit.rb +312 -0
  19. data/lib/class_metrix/extractor.rb +121 -0
  20. data/lib/class_metrix/extractors/constants_extractor.rb +87 -0
  21. data/lib/class_metrix/extractors/methods_extractor.rb +87 -0
  22. data/lib/class_metrix/extractors/multi_type_extractor.rb +66 -0
  23. data/lib/class_metrix/formatters/base/base_component.rb +62 -0
  24. data/lib/class_metrix/formatters/base/base_formatter.rb +93 -0
  25. data/lib/class_metrix/formatters/components/footer_component.rb +67 -0
  26. data/lib/class_metrix/formatters/components/generic_header_component.rb +87 -0
  27. data/lib/class_metrix/formatters/components/header_component.rb +92 -0
  28. data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +140 -0
  29. data/lib/class_metrix/formatters/components/table_component.rb +268 -0
  30. data/lib/class_metrix/formatters/csv_formatter.rb +98 -0
  31. data/lib/class_metrix/formatters/markdown_formatter.rb +184 -0
  32. data/lib/class_metrix/formatters/shared/csv_table_builder.rb +21 -0
  33. data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +97 -0
  34. data/lib/class_metrix/formatters/shared/table_builder.rb +267 -0
  35. data/lib/class_metrix/formatters/shared/value_processor.rb +78 -0
  36. data/lib/class_metrix/processors/value_processor.rb +40 -0
  37. data/lib/class_metrix/utils/class_resolver.rb +20 -0
  38. data/lib/class_metrix/version.rb +5 -0
  39. data/lib/class_metrix.rb +12 -0
  40. data/sig/class/metrix.rbs +6 -0
  41. 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