class-metrix 0.1.2 → 1.0.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +48 -0
  3. data/.vscode/README.md +72 -0
  4. data/.vscode/extensions.json +28 -0
  5. data/.vscode/launch.json +32 -0
  6. data/.vscode/settings.json +88 -0
  7. data/.vscode/tasks.json +99 -0
  8. data/CHANGELOG.md +71 -4
  9. data/README.md +41 -7
  10. data/docs/ARCHITECTURE.md +501 -0
  11. data/examples/README.md +161 -114
  12. data/examples/basic_usage.rb +88 -0
  13. data/examples/debug_levels_demo.rb +65 -0
  14. data/examples/debug_mode_demo.rb +75 -0
  15. data/examples/inheritance_and_modules.rb +155 -0
  16. data/lib/class_metrix/extractor.rb +106 -11
  17. data/lib/class_metrix/extractors/constants_extractor.rb +155 -21
  18. data/lib/class_metrix/extractors/methods_extractor.rb +186 -21
  19. data/lib/class_metrix/extractors/multi_type_extractor.rb +6 -5
  20. data/lib/class_metrix/formatters/components/footer_component.rb +1 -1
  21. data/lib/class_metrix/formatters/components/table_component/column_width_calculator.rb +56 -0
  22. data/lib/class_metrix/formatters/components/table_component/row_processor.rb +138 -0
  23. data/lib/class_metrix/formatters/components/table_component/table_data_extractor.rb +54 -0
  24. data/lib/class_metrix/formatters/components/table_component/table_renderer.rb +55 -0
  25. data/lib/class_metrix/formatters/components/table_component.rb +30 -244
  26. data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +10 -5
  27. data/lib/class_metrix/formatters/shared/table_builder.rb +84 -21
  28. data/lib/class_metrix/formatters/shared/value_processor.rb +72 -16
  29. data/lib/class_metrix/utils/debug_logger.rb +159 -0
  30. data/lib/class_metrix/version.rb +1 -1
  31. metadata +17 -9
  32. data/examples/advanced/error_handling.rb +0 -199
  33. data/examples/advanced/hash_expansion.rb +0 -180
  34. data/examples/basic/01_simple_constants.rb +0 -56
  35. data/examples/basic/02_simple_methods.rb +0 -99
  36. data/examples/basic/03_multi_type_extraction.rb +0 -116
  37. data/examples/components/configurable_reports.rb +0 -201
  38. data/examples/csv_output_demo.rb +0 -237
  39. data/examples/real_world/microservices_audit.rb +0 -312
@@ -4,54 +4,139 @@ require_relative "../processors/value_processor"
4
4
 
5
5
  module ClassMetrix
6
6
  class MethodsExtractor
7
- def initialize(classes, filters, handle_errors)
7
+ def initialize(classes, filters, handle_errors, options = {})
8
8
  @classes = classes
9
9
  @filters = filters
10
10
  @handle_errors = handle_errors
11
+ @options = default_options.merge(options)
11
12
  end
12
13
 
13
14
  def extract
14
15
  return { headers: [], rows: [] } if @classes.empty?
15
16
 
16
- # Get all class method names across all classes
17
17
  method_names = get_all_class_method_names
18
-
19
- # Apply filters
20
18
  method_names = apply_filters(method_names)
19
+ headers = build_headers
20
+ rows = build_rows(method_names)
21
21
 
22
- # Build headers: ["Method", "Class1", "Class2", ...]
23
- headers = ["Method"] + @classes.map(&:name)
22
+ { headers: headers, rows: rows }
23
+ end
24
24
 
25
- # Build rows: each row represents one method across all classes
26
- rows = method_names.map do |method_name|
27
- row = [method_name]
25
+ private
26
+
27
+ def default_options
28
+ {
29
+ include_inherited: false,
30
+ include_modules: false,
31
+ show_source: false
32
+ }
33
+ end
34
+
35
+ def build_headers
36
+ if @options[:show_source]
37
+ ["Method (Source)"] + @classes.map(&:name)
38
+ else
39
+ ["Method"] + @classes.map(&:name)
40
+ end
41
+ end
28
42
 
43
+ def build_rows(method_names)
44
+ method_names.map do |method_name|
45
+ row = [method_name]
29
46
  @classes.each do |klass|
30
47
  value = call_class_method(klass, method_name)
31
- # Pass the raw value for hash expansion to work properly
32
48
  row << value
33
49
  end
34
-
35
50
  row
36
51
  end
37
-
38
- { headers: headers, rows: rows }
39
52
  end
40
53
 
41
- private
42
-
43
54
  def get_all_class_method_names
44
55
  all_methods = Set.new
45
56
 
46
57
  @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))
58
+ methods = if inheritance_or_modules_enabled?
59
+ get_comprehensive_methods(klass)
60
+ else
61
+ klass.singleton_methods(false)
62
+ end
63
+ all_methods.merge(methods.map(&:to_s))
50
64
  end
51
65
 
52
66
  all_methods.to_a.sort
53
67
  end
54
68
 
69
+ def inheritance_or_modules_enabled?
70
+ @options[:include_inherited] || @options[:include_modules]
71
+ end
72
+
73
+ def get_comprehensive_methods(klass)
74
+ methods = Set.new
75
+ methods.merge(klass.singleton_methods(false))
76
+
77
+ methods.merge(get_inherited_methods(klass)) if @options[:include_inherited]
78
+
79
+ methods.merge(get_module_methods(klass)) if @options[:include_modules]
80
+
81
+ methods.to_a
82
+ end
83
+
84
+ def get_inherited_methods(klass)
85
+ methods = Set.new
86
+ parent = klass.superclass
87
+
88
+ while parent && !core_class?(parent)
89
+ methods.merge(parent.singleton_methods(false))
90
+ parent = parent.superclass
91
+ end
92
+
93
+ methods
94
+ end
95
+
96
+ def get_module_methods(klass)
97
+ methods = Set.new
98
+ all_singleton_modules = get_all_singleton_modules(klass)
99
+
100
+ all_singleton_modules.each do |mod|
101
+ next if core_module?(mod)
102
+
103
+ module_methods = mod.instance_methods(false).map(&:to_s)
104
+ # Filter out methods that shouldn't be called directly
105
+ module_methods = module_methods.reject { |method| excluded_module_method?(method) }
106
+ methods.merge(module_methods)
107
+ end
108
+
109
+ methods
110
+ end
111
+
112
+ def excluded_module_method?(method_name)
113
+ # Methods that require arguments or shouldn't be called directly
114
+ %w[included extended prepended].include?(method_name)
115
+ end
116
+
117
+ def get_all_singleton_modules(klass)
118
+ modules = []
119
+ modules.concat(klass.singleton_class.included_modules)
120
+
121
+ if @options[:include_inherited]
122
+ parent = klass.superclass
123
+ while parent && !core_class?(parent)
124
+ modules.concat(parent.singleton_class.included_modules)
125
+ parent = parent.superclass
126
+ end
127
+ end
128
+
129
+ modules
130
+ end
131
+
132
+ def core_class?(klass)
133
+ [Object, BasicObject].include?(klass)
134
+ end
135
+
136
+ def core_module?(mod)
137
+ [Kernel, Module, Class].include?(mod)
138
+ end
139
+
55
140
  def apply_filters(method_names)
56
141
  return method_names if @filters.empty?
57
142
 
@@ -72,9 +157,10 @@ module ClassMetrix
72
157
  end
73
158
 
74
159
  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)
160
+ method_info = find_method_source(klass, method_name)
161
+
162
+ if method_info
163
+ call_method(method_info, klass, method_name)
78
164
  else
79
165
  @handle_errors ? ValueProcessor.missing_method : nil
80
166
  end
@@ -83,5 +169,84 @@ module ClassMetrix
83
169
  rescue StandardError => e
84
170
  @handle_errors ? ValueProcessor.handle_extraction_error(e) : (raise e)
85
171
  end
172
+
173
+ def call_method(method_info, klass, method_name)
174
+ if method_info[:callable]
175
+ method_info[:callable].call
176
+ else
177
+ klass.public_send(method_name)
178
+ end
179
+ end
180
+
181
+ def find_method_source(klass, method_name)
182
+ method_sym = method_name.to_sym
183
+
184
+ return nil unless klass.respond_to?(method_name, true)
185
+
186
+ # Check own methods first
187
+ return build_method_info(klass.name, :own, nil) if klass.singleton_methods(false).include?(method_sym)
188
+
189
+ # Check inherited methods
190
+ if @options[:include_inherited]
191
+ inherited_info = find_inherited_method(klass, method_sym, method_name)
192
+ return inherited_info if inherited_info
193
+ end
194
+
195
+ # Check module methods
196
+ if @options[:include_modules]
197
+ module_info = find_module_method(klass, method_sym, method_name)
198
+ return module_info if module_info
199
+ end
200
+
201
+ # Fallback for unknown source
202
+ build_method_info("inherited", :unknown, nil)
203
+ end
204
+
205
+ def find_inherited_method(klass, method_sym, method_name)
206
+ parent = klass.superclass
207
+ while parent && !core_class?(parent)
208
+ if parent.singleton_methods(false).include?(method_sym)
209
+ parent_class = parent
210
+ method_to_call = method_name
211
+ callable = -> { parent_class.public_send(method_to_call) }
212
+ return build_method_info(parent.name, :inherited, callable)
213
+ end
214
+ parent = parent.superclass
215
+ end
216
+ nil
217
+ end
218
+
219
+ def find_module_method(klass, method_sym, method_name)
220
+ all_singleton_modules = get_all_singleton_modules(klass)
221
+
222
+ all_singleton_modules.each do |mod|
223
+ next if core_module?(mod)
224
+
225
+ next unless mod.instance_methods(false).include?(method_sym)
226
+
227
+ method_to_call = method_name
228
+ callable = -> { klass.public_send(method_to_call) }
229
+ source = determine_module_source(klass, mod)
230
+ return build_method_info(source, :module, callable)
231
+ end
232
+
233
+ nil
234
+ end
235
+
236
+ def determine_module_source(klass, mod)
237
+ if @options[:include_inherited] && !klass.singleton_class.included_modules.include?(mod)
238
+ "#{mod.name} (via parent)"
239
+ else
240
+ mod.name
241
+ end
242
+ end
243
+
244
+ def build_method_info(source, type, callable)
245
+ {
246
+ source: source,
247
+ type: type,
248
+ callable: callable
249
+ }
250
+ end
86
251
  end
87
252
  end
@@ -5,12 +5,13 @@ require_relative "methods_extractor"
5
5
 
6
6
  module ClassMetrix
7
7
  class MultiTypeExtractor
8
- def initialize(classes, types, filters, modules, handle_errors)
8
+ def initialize(classes, types, filters, extraction_config = {})
9
9
  @classes = classes
10
10
  @types = types
11
11
  @filters = filters
12
- @modules = modules
13
- @handle_errors = handle_errors
12
+ @modules = extraction_config[:modules] || []
13
+ @handle_errors = extraction_config[:handle_errors] || false
14
+ @options = extraction_config[:options] || {}
14
15
  end
15
16
 
16
17
  def extract
@@ -42,9 +43,9 @@ module ClassMetrix
42
43
  def extract_single_type(type)
43
44
  case type
44
45
  when :constants
45
- ConstantsExtractor.new(@classes, @filters, @handle_errors).extract
46
+ ConstantsExtractor.new(@classes, @filters, @handle_errors, @options).extract
46
47
  when :class_methods
47
- MethodsExtractor.new(@classes, @filters, @handle_errors).extract
48
+ MethodsExtractor.new(@classes, @filters, @handle_errors, @options).extract
48
49
  else
49
50
  { headers: [], rows: [] }
50
51
  end
@@ -53,7 +53,7 @@ module ClassMetrix
53
53
 
54
54
  output << "## Report Information"
55
55
  output << ""
56
- output << "- **Generated by**: [ClassMetrix gem](https://github.com/your-username/class-metrix)"
56
+ output << "- **Generated by**: [ClassMetrix gem](https://github.com/patrick204nqh/class-metrix)"
57
57
  output << "- **Generated at**: #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}"
58
58
  output << "- **Ruby version**: #{RUBY_VERSION}"
59
59
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Formatters
5
+ module Components
6
+ class TableComponent
7
+ class ColumnWidthCalculator
8
+ def initialize(table_style: :standard, min_column_width: 3, max_column_width: 50)
9
+ @table_style = table_style
10
+ @min_column_width = min_column_width
11
+ @max_column_width = max_column_width
12
+ end
13
+
14
+ def calculate_widths(headers, rows)
15
+ col_count = headers.length
16
+ widths = initialize_column_widths(col_count, headers)
17
+
18
+ update_widths_from_rows(widths, rows, col_count)
19
+ apply_minimum_widths(widths)
20
+ end
21
+
22
+ private
23
+
24
+ def initialize_column_widths(col_count, headers)
25
+ widths = Array.new(col_count, 0)
26
+ headers.each_with_index do |header, i|
27
+ widths[i] = [widths[i], header.to_s.length].max
28
+ end
29
+ widths
30
+ end
31
+
32
+ def update_widths_from_rows(widths, rows, col_count)
33
+ rows.each do |row|
34
+ row.each_with_index do |cell, i|
35
+ next if i >= col_count
36
+
37
+ cell_width = calculate_cell_width(cell)
38
+ widths[i] = [widths[i], cell_width].max
39
+ end
40
+ end
41
+ end
42
+
43
+ def calculate_cell_width(cell)
44
+ cell_width = cell.to_s.length
45
+ # Apply max width limit for readability
46
+ @table_style == :compact ? [@max_column_width, cell_width].min : cell_width
47
+ end
48
+
49
+ def apply_minimum_widths(widths)
50
+ widths.map { |w| [w, @min_column_width].max }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../processors/value_processor"
4
+
5
+ module ClassMetrix
6
+ module Formatters
7
+ module Components
8
+ class TableComponent
9
+ class RowProcessor
10
+ def initialize(data_extractor, options = {})
11
+ @data_extractor = data_extractor
12
+ @hide_main_row = options.fetch(:hide_main_row, false)
13
+ @hide_key_rows = options.fetch(:hide_key_rows, true) # Default: show only main rows
14
+ end
15
+
16
+ def process_simple_rows(rows)
17
+ rows.map do |row|
18
+ processed_row = [row[0]] # Keep the behavior name as-is
19
+ row[1..].each do |value|
20
+ processed_row << ValueProcessor.process(value)
21
+ end
22
+ processed_row
23
+ end
24
+ end
25
+
26
+ def process_expanded_rows(rows)
27
+ expanded_rows = []
28
+
29
+ rows.each do |row|
30
+ if @data_extractor.row_has_expandable_hash?(row)
31
+ expanded_rows.concat(expand_row(row))
32
+ else
33
+ expanded_rows << process_non_hash_row(row)
34
+ end
35
+ end
36
+
37
+ expanded_rows
38
+ end
39
+
40
+ private
41
+
42
+ def process_non_hash_row(row)
43
+ if @data_extractor.has_type_column?
44
+ [row[0], row[1]] + row[2..].map { |value| ValueProcessor.process(value) }
45
+ else
46
+ [row[0]] + row[1..].map { |value| ValueProcessor.process(value) }
47
+ end
48
+ end
49
+
50
+ def expand_row(row)
51
+ row_data = @data_extractor.extract_row_data(row)
52
+ all_hash_keys = @data_extractor.collect_hash_keys(row_data[:values])
53
+
54
+ return [row] if all_hash_keys.empty?
55
+
56
+ build_expanded_rows(row_data, all_hash_keys, row)
57
+ end
58
+
59
+ def build_expanded_rows(row_data, all_hash_keys, original_row)
60
+ expanded_rows = []
61
+
62
+ # Add main row if configured to show
63
+ expanded_rows << build_main_expanded_row(row_data) if should_show_main_row?
64
+
65
+ # Add key rows if configured to show
66
+ expanded_rows.concat(build_key_rows(all_hash_keys, row_data, original_row)) if should_show_key_rows?
67
+
68
+ expanded_rows
69
+ end
70
+
71
+ def should_show_main_row?
72
+ !@hide_main_row
73
+ end
74
+
75
+ def should_show_key_rows?
76
+ !@hide_key_rows
77
+ end
78
+
79
+ def build_key_rows(all_hash_keys, row_data, original_row)
80
+ all_hash_keys.to_a.sort.map do |key|
81
+ build_key_row(key, row_data, original_row)
82
+ end
83
+ end
84
+
85
+ def build_main_expanded_row(row_data)
86
+ processed_values = row_data[:values].map { |value| ValueProcessor.process(value) }
87
+
88
+ if @data_extractor.has_type_column?
89
+ [row_data[:type_value], row_data[:behavior_name]] + processed_values
90
+ else
91
+ [row_data[:behavior_name]] + processed_values
92
+ end
93
+ end
94
+
95
+ def build_key_row(key, row_data, original_row)
96
+ path_name = ".#{key}"
97
+ key_values = process_key_values(key, row_data[:values], original_row)
98
+
99
+ if @data_extractor.has_type_column?
100
+ ["-", path_name] + key_values
101
+ else
102
+ [path_name] + key_values
103
+ end
104
+ end
105
+
106
+ def process_key_values(key, values, original_row)
107
+ values.map.with_index do |value, index|
108
+ if value.is_a?(Hash)
109
+ extract_hash_value(value, key)
110
+ else
111
+ handle_non_hash_value(original_row, index)
112
+ end
113
+ end
114
+ end
115
+
116
+ def extract_hash_value(hash, key)
117
+ has_key = hash.key?(key.to_sym) || hash.key?(key.to_s)
118
+ if has_key
119
+ hash_value = hash[key.to_sym] || hash[key.to_s]
120
+ ValueProcessor.process(hash_value)
121
+ else
122
+ "—" # Missing hash key
123
+ end
124
+ end
125
+
126
+ def handle_non_hash_value(original_row, index)
127
+ original_value = original_row[@data_extractor.has_type_column? ? (index + 2) : (index + 1)]
128
+ if original_value.to_s.include?("🚫")
129
+ "🚫 Not defined"
130
+ else
131
+ "—" # Non-hash values don't have this key
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Formatters
5
+ module Components
6
+ class TableComponent
7
+ class TableDataExtractor
8
+ def initialize(headers)
9
+ @headers = headers
10
+ end
11
+
12
+ def has_type_column?
13
+ @headers.first == "Type"
14
+ end
15
+
16
+ def value_start_index
17
+ has_type_column? ? 2 : 1
18
+ end
19
+
20
+ def behavior_column_index
21
+ has_type_column? ? 1 : 0
22
+ end
23
+
24
+ def extract_row_data(row)
25
+ if has_type_column?
26
+ {
27
+ type_value: row[0],
28
+ behavior_name: row[1],
29
+ values: row[2..]
30
+ }
31
+ else
32
+ {
33
+ behavior_name: row[0],
34
+ values: row[1..]
35
+ }
36
+ end
37
+ end
38
+
39
+ def row_has_expandable_hash?(row)
40
+ row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
41
+ end
42
+
43
+ def collect_hash_keys(values)
44
+ all_hash_keys = Set.new
45
+ values.each do |value|
46
+ all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
47
+ end
48
+ all_hash_keys
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Formatters
5
+ module Components
6
+ class TableComponent
7
+ class TableRenderer
8
+ def initialize(table_style: :standard, max_column_width: 50)
9
+ @table_style = table_style
10
+ @max_column_width = max_column_width
11
+ end
12
+
13
+ def render_table(headers, rows, column_widths)
14
+ output = []
15
+ output << build_row(headers, column_widths)
16
+ output << build_separator(column_widths)
17
+
18
+ rows.each do |row|
19
+ output << build_row(row, column_widths)
20
+ end
21
+
22
+ output.join("\n")
23
+ end
24
+
25
+ private
26
+
27
+ def build_row(cells, col_widths)
28
+ formatted_cells = format_cells(cells, col_widths)
29
+ "|#{formatted_cells.join("|")}|"
30
+ end
31
+
32
+ def format_cells(cells, col_widths)
33
+ cells.each_with_index.map do |cell, i|
34
+ width = col_widths[i] || 10
35
+ cell_str = format_cell_content(cell)
36
+ " #{cell_str.ljust(width)} "
37
+ end
38
+ end
39
+
40
+ def format_cell_content(cell)
41
+ cell_str = cell.to_s
42
+ return cell_str unless @table_style == :compact && cell_str.length > @max_column_width
43
+
44
+ "#{cell_str[0...(@max_column_width - 3)]}..."
45
+ end
46
+
47
+ def build_separator(col_widths)
48
+ separators = col_widths.map { |width| "-" * (width + 2) }
49
+ "|#{separators.join("|")}|"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end