class-metrix 0.1.2 → 1.0.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +48 -0
  3. data/.vscode/README.md +128 -0
  4. data/.vscode/extensions.json +31 -0
  5. data/.vscode/keybindings.json +26 -0
  6. data/.vscode/launch.json +32 -0
  7. data/.vscode/rbs.code-snippets +61 -0
  8. data/.vscode/settings.json +112 -0
  9. data/.vscode/tasks.json +240 -0
  10. data/CHANGELOG.md +73 -4
  11. data/README.md +86 -22
  12. data/Steepfile +26 -0
  13. data/docs/ARCHITECTURE.md +501 -0
  14. data/docs/CHANGELOG_EVOLUTION_EXAMPLE.md +95 -0
  15. data/examples/README.md +161 -114
  16. data/examples/basic_usage.rb +88 -0
  17. data/examples/debug_levels_demo.rb +65 -0
  18. data/examples/debug_mode_demo.rb +75 -0
  19. data/examples/inheritance_and_modules.rb +155 -0
  20. data/lib/class_metrix/extractor.rb +106 -11
  21. data/lib/class_metrix/extractors/constants_extractor.rb +155 -21
  22. data/lib/class_metrix/extractors/methods_extractor.rb +186 -21
  23. data/lib/class_metrix/extractors/multi_type_extractor.rb +8 -7
  24. data/lib/class_metrix/formatters/base/base_formatter.rb +3 -3
  25. data/lib/class_metrix/formatters/components/footer_component.rb +4 -4
  26. data/lib/class_metrix/formatters/components/generic_header_component.rb +2 -2
  27. data/lib/class_metrix/formatters/components/header_component.rb +4 -4
  28. data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +7 -7
  29. data/lib/class_metrix/formatters/components/table_component/column_width_calculator.rb +56 -0
  30. data/lib/class_metrix/formatters/components/table_component/row_processor.rb +141 -0
  31. data/lib/class_metrix/formatters/components/table_component/table_data_extractor.rb +57 -0
  32. data/lib/class_metrix/formatters/components/table_component/table_renderer.rb +55 -0
  33. data/lib/class_metrix/formatters/components/table_component.rb +32 -245
  34. data/lib/class_metrix/formatters/csv_formatter.rb +3 -3
  35. data/lib/class_metrix/formatters/markdown_formatter.rb +3 -4
  36. data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +12 -7
  37. data/lib/class_metrix/formatters/shared/table_builder.rb +92 -27
  38. data/lib/class_metrix/formatters/shared/value_processor.rb +72 -16
  39. data/lib/class_metrix/utils/debug_logger.rb +159 -0
  40. data/lib/class_metrix/version.rb +1 -1
  41. data/sig/class_metrix.rbs +8 -0
  42. data/sig/extractor.rbs +54 -0
  43. data/sig/extractors.rbs +84 -0
  44. data/sig/formatters_base.rbs +59 -0
  45. data/sig/formatters_components.rbs +133 -0
  46. data/sig/formatters_main.rbs +20 -0
  47. data/sig/formatters_shared.rbs +102 -0
  48. data/sig/manifest.yaml +32 -0
  49. data/sig/utils.rbs +57 -0
  50. data/sig/value_processor.rbs +11 -0
  51. data/sig/version.rbs +4 -0
  52. metadata +60 -10
  53. data/examples/advanced/error_handling.rb +0 -199
  54. data/examples/advanced/hash_expansion.rb +0 -180
  55. data/examples/basic/01_simple_constants.rb +0 -56
  56. data/examples/basic/02_simple_methods.rb +0 -99
  57. data/examples/basic/03_multi_type_extraction.rb +0 -116
  58. data/examples/components/configurable_reports.rb +0 -201
  59. data/examples/csv_output_demo.rb +0 -237
  60. data/examples/real_world/microservices_audit.rb +0 -312
  61. data/sig/class/metrix.rbs +0 -6
@@ -0,0 +1,57 @@
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
+ values = row[value_start_index..] || []
41
+ return false if values.nil?
42
+
43
+ values.any? { |cell| cell.is_a?(Hash) }
44
+ end
45
+
46
+ def collect_hash_keys(values)
47
+ all_hash_keys = Set.new
48
+ values.each do |value|
49
+ all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
50
+ end
51
+ all_hash_keys
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ 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 = [] # : Array[String]
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
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
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../processors/value_processor"
3
+ require_relative "table_component/table_data_extractor"
4
+ require_relative "table_component/row_processor"
5
+ require_relative "table_component/column_width_calculator"
6
+ require_relative "table_component/table_renderer"
4
7
 
5
8
  module ClassMetrix
6
9
  module Formatters
@@ -10,257 +13,41 @@ module ClassMetrix
10
13
  @data = data
11
14
  @options = options
12
15
  @expand_hashes = options.fetch(:expand_hashes, false)
13
- @table_style = options.fetch(:table_style, :standard) # :standard, :compact, :wide
16
+ @table_style = options.fetch(:table_style, :standard)
14
17
  @min_column_width = options.fetch(:min_column_width, 3)
15
18
  @max_column_width = options.fetch(:max_column_width, 50)
19
+ @hide_main_row = options.fetch(:hide_main_row, false)
20
+ @hide_key_rows = options.fetch(:hide_key_rows, true) # Default: show only main rows
21
+
22
+ # Initialize helper objects
23
+ @data_extractor = TableDataExtractor.new(@data[:headers])
24
+ @row_processor = RowProcessor.new(@data_extractor, {
25
+ hide_main_row: @hide_main_row,
26
+ hide_key_rows: @hide_key_rows
27
+ })
28
+ @width_calculator = ColumnWidthCalculator.new(
29
+ table_style: @table_style,
30
+ min_column_width: @min_column_width,
31
+ max_column_width: @max_column_width
32
+ )
33
+ @renderer = TableRenderer.new(
34
+ table_style: @table_style,
35
+ max_column_width: @max_column_width
36
+ )
16
37
  end
17
38
 
18
39
  def generate
19
- return "" if @data[:headers].empty? || @data[:rows].empty?
40
+ return [""] if @data[:headers].empty? || @data[:rows].empty?
20
41
 
21
- if @expand_hashes
22
- format_with_hash_expansion
23
- else
24
- format_simple_table
25
- end
26
- end
27
-
28
- private
29
-
30
- def format_simple_table
31
- headers = @data[:headers]
32
- rows = @data[:rows]
33
-
34
- processed_rows = process_simple_rows(rows)
35
- build_table(headers, processed_rows)
36
- end
37
-
38
- def format_with_hash_expansion
39
42
  headers = @data[:headers]
40
- rows = @data[:rows]
41
-
42
- expanded_rows = process_expanded_rows(rows, headers)
43
- build_table(headers, expanded_rows)
44
- end
45
-
46
- def process_simple_rows(rows)
47
- rows.map do |row|
48
- processed_row = [row[0]] # Keep the behavior name as-is
49
- row[1..].each do |value|
50
- processed_row << ValueProcessor.process(value)
51
- end
52
- processed_row
53
- end
54
- end
55
-
56
- def process_expanded_rows(rows, headers)
57
- expanded_rows = []
58
-
59
- rows.each do |row|
60
- if row_has_expandable_hash?(row, headers)
61
- expanded_rows.concat(expand_row(row, headers))
62
- else
63
- expanded_rows << process_non_hash_row(row, headers)
64
- end
65
- end
66
-
67
- expanded_rows
68
- end
69
-
70
- def row_has_expandable_hash?(row, headers)
71
- has_type_column = headers.first == "Type"
72
- value_start_index = has_type_column ? 2 : 1
73
- row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
74
- end
75
-
76
- def process_non_hash_row(row, headers)
77
- has_type_column = headers.first == "Type"
78
- if has_type_column
79
- [row[0], row[1]] + row[2..].map { |value| ValueProcessor.process(value) }
80
- else
81
- [row[0]] + row[1..].map { |value| ValueProcessor.process(value) }
82
- end
83
- end
84
-
85
- def build_table(headers, rows)
86
- col_widths = calculate_column_widths(headers, rows)
87
-
88
- output = []
89
- output << build_row(headers, col_widths)
90
- output << build_separator(col_widths)
91
-
92
- rows.each do |row|
93
- output << build_row(row, col_widths)
94
- end
95
-
96
- output.join("\n")
97
- end
98
-
99
- def expand_row(row, headers)
100
- has_type_column = headers.first == "Type"
101
- row_data = extract_row_data(row, has_type_column)
102
-
103
- all_hash_keys = collect_hash_keys(row_data[:values])
104
- return [row] if all_hash_keys.empty?
105
-
106
- build_expanded_rows(row_data, all_hash_keys, has_type_column, row)
107
- end
108
-
109
- def extract_row_data(row, has_type_column)
110
- if has_type_column
111
- build_type_column_data(row)
112
- else
113
- build_standard_column_data(row)
114
- end
115
- end
116
-
117
- def build_type_column_data(row)
118
- {
119
- type_value: row[0],
120
- behavior_name: row[1],
121
- values: row[2..]
122
- }
123
- end
124
-
125
- def build_standard_column_data(row)
126
- {
127
- behavior_name: row[0],
128
- values: row[1..]
129
- }
130
- end
131
-
132
- def collect_hash_keys(values)
133
- all_hash_keys = Set.new
134
- values.each do |value|
135
- all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
136
- end
137
- all_hash_keys
138
- end
139
-
140
- def build_expanded_rows(row_data, all_hash_keys, has_type_column, original_row)
141
- expanded_rows = []
142
-
143
- # Add main row
144
- expanded_rows << build_main_expanded_row(row_data, has_type_column)
145
-
146
- # Add key rows
147
- all_hash_keys.to_a.sort.each do |key|
148
- expanded_rows << build_key_row(key, row_data, has_type_column, original_row)
149
- end
150
-
151
- expanded_rows
152
- end
153
-
154
- def build_main_expanded_row(row_data, has_type_column)
155
- processed_values = row_data[:values].map { |value| ValueProcessor.process(value) }
156
-
157
- if has_type_column
158
- [row_data[:type_value], row_data[:behavior_name]] + processed_values
159
- else
160
- [row_data[:behavior_name]] + processed_values
161
- end
162
- end
163
-
164
- def build_key_row(key, row_data, has_type_column, original_row)
165
- path_name = ".#{key}"
166
- key_values = process_key_values(key, row_data[:values], has_type_column, original_row)
167
-
168
- if has_type_column
169
- ["-", path_name] + key_values
170
- else
171
- [path_name] + key_values
172
- end
173
- end
174
-
175
- def process_key_values(key, values, has_type_column, original_row)
176
- values.map.with_index do |value, index|
177
- if value.is_a?(Hash)
178
- extract_hash_value(value, key)
179
- else
180
- handle_non_hash_value(original_row, index, has_type_column)
181
- end
182
- end
183
- end
184
-
185
- def extract_hash_value(hash, key)
186
- has_key = hash.key?(key.to_sym) || hash.key?(key.to_s)
187
- if has_key
188
- hash_value = hash[key.to_sym] || hash[key.to_s]
189
- ValueProcessor.process(hash_value)
190
- else
191
- "—" # Missing hash key
192
- end
193
- end
194
-
195
- def handle_non_hash_value(original_row, index, has_type_column)
196
- original_value = original_row[has_type_column ? (index + 2) : (index + 1)]
197
- if original_value.to_s.include?("🚫")
198
- "🚫 Not defined"
199
- else
200
- "—" # Non-hash values don't have this key
201
- end
202
- end
203
-
204
- def calculate_column_widths(headers, rows)
205
- col_count = headers.length
206
- widths = initialize_column_widths(col_count, headers)
207
-
208
- update_widths_from_rows(widths, rows, col_count)
209
- apply_minimum_widths(widths)
210
- end
211
-
212
- def initialize_column_widths(col_count, headers)
213
- widths = Array.new(col_count, 0)
214
- headers.each_with_index do |header, i|
215
- widths[i] = [widths[i], header.to_s.length].max
216
- end
217
- widths
218
- end
219
-
220
- def update_widths_from_rows(widths, rows, col_count)
221
- rows.each do |row|
222
- row.each_with_index do |cell, i|
223
- next if i >= col_count
224
-
225
- cell_width = calculate_cell_width(cell)
226
- widths[i] = [widths[i], cell_width].max
227
- end
228
- end
229
- end
230
-
231
- def calculate_cell_width(cell)
232
- cell_width = cell.to_s.length
233
- # Apply max width limit for readability
234
- @table_style == :compact ? [@max_column_width, cell_width].min : cell_width
235
- end
236
-
237
- def apply_minimum_widths(widths)
238
- widths.map { |w| [w, @min_column_width].max }
239
- end
240
-
241
- def build_row(cells, col_widths)
242
- formatted_cells = format_cells(cells, col_widths)
243
- "|#{formatted_cells.join("|")}|"
244
- end
245
-
246
- def format_cells(cells, col_widths)
247
- cells.each_with_index.map do |cell, i|
248
- width = col_widths[i] || 10
249
- cell_str = format_cell_content(cell)
250
- " #{cell_str.ljust(width)} "
251
- end
252
- end
253
-
254
- def format_cell_content(cell)
255
- cell_str = cell.to_s
256
- return cell_str unless @table_style == :compact && cell_str.length > @max_column_width
257
-
258
- "#{cell_str[0...(@max_column_width - 3)]}..."
259
- end
260
-
261
- def build_separator(col_widths)
262
- separators = col_widths.map { |width| "-" * (width + 2) }
263
- "|#{separators.join("|")}|"
43
+ rows = if @expand_hashes
44
+ @row_processor.process_expanded_rows(@data[:rows])
45
+ else
46
+ @row_processor.process_simple_rows(@data[:rows])
47
+ end
48
+
49
+ column_widths = @width_calculator.calculate_widths(headers, rows)
50
+ @renderer.render_table(headers, rows, column_widths)
264
51
  end
265
52
  end
266
53
  end
@@ -15,7 +15,7 @@ module ClassMetrix
15
15
  def format
16
16
  return "" if @data[:headers].empty? || @data[:rows].empty?
17
17
 
18
- output_lines = []
18
+ output_lines = [] # : Array[String]
19
19
 
20
20
  # Add CSV header comments
21
21
  if @options.fetch(:show_metadata, true)
@@ -65,7 +65,7 @@ module ClassMetrix
65
65
 
66
66
  return [] if headers.empty?
67
67
 
68
- output = []
68
+ output = [] # : Array[String]
69
69
  separator = @options.fetch(:separator, ",")
70
70
  quote_char = @options.fetch(:quote_char, '"')
71
71
 
@@ -75,7 +75,7 @@ module ClassMetrix
75
75
  csv_rows.each do |row|
76
76
  # Ensure all row values are strings and properly quoted
77
77
  formatted_row = row.map { |cell| format_csv_cell(cell) }
78
- csv_line = CSV.generate_line(formatted_row, col_sep: separator, quote_char: quote_char).chomp
78
+ csv_line = CSV.generate(col_sep: separator, quote_char: quote_char) { |csv| csv << formatted_row }.chomp
79
79
  output << csv_line
80
80
  end
81
81
 
@@ -16,7 +16,7 @@ module ClassMetrix
16
16
  def format
17
17
  return "" if @data[:headers].empty? || @data[:rows].empty?
18
18
 
19
- output = []
19
+ output = [] # : Array[String]
20
20
 
21
21
  # Add header sections (title, classes, extraction info)
22
22
  if @options.fetch(:show_metadata, true)
@@ -77,7 +77,7 @@ module ClassMetrix
77
77
 
78
78
  widths = calculate_column_widths(headers, rows)
79
79
 
80
- output = []
80
+ output = [] # : Array[String]
81
81
  output << build_header_row(headers, widths)
82
82
  output << build_separator_row(widths)
83
83
  output.concat(build_data_rows(rows, headers, widths))
@@ -170,8 +170,7 @@ module ClassMetrix
170
170
  if max_width >= 15
171
171
  # Look for the first complete key-value pair (handle :symbol=>value format)
172
172
  match = text.match(/\{(:[^,}]+=>[^,}]+)/)
173
- if match
174
- first_pair = match[1]
173
+ if match && (first_pair = match[1])
175
174
  needed_length = first_pair.length + 6 # For "{", ", ...}"
176
175
  return "{#{first_pair}, ...}" if needed_length <= max_width
177
176
  end
@@ -9,7 +9,7 @@ module ClassMetrix
9
9
  private
10
10
 
11
11
  def process_value(value)
12
- @value_processor.process_for_markdown(value)
12
+ @value_processor.process_for_markdown(value, debug_mode: @options.fetch(:debug_mode, false))
13
13
  end
14
14
 
15
15
  def get_null_value
@@ -19,7 +19,7 @@ module ClassMetrix
19
19
  # Create proper flat table structure for hash expansion
20
20
  def expand_row(row, _headers)
21
21
  behavior_name = row[behavior_column_index]
22
- values = row[value_start_index..]
22
+ values = row[value_start_index..] || []
23
23
 
24
24
  all_hash_keys = collect_unique_hash_keys(values)
25
25
  return [process_row(row)] if all_hash_keys.empty?
@@ -36,9 +36,14 @@ module ClassMetrix
36
36
  end
37
37
 
38
38
  def build_expanded_row_structure(row, behavior_name, values, all_hash_keys)
39
- expanded_rows = []
40
- expanded_rows << build_main_row(row, behavior_name, values)
41
- expanded_rows.concat(build_hash_key_rows(row, behavior_name, values, all_hash_keys))
39
+ expanded_rows = [] # : Array[Array[String]]
40
+
41
+ # Add main row if configured to show
42
+ expanded_rows << build_main_row(row, behavior_name, values) if should_show_main_row?
43
+
44
+ # Add key rows if configured to show
45
+ expanded_rows.concat(build_hash_key_rows(row, behavior_name, values, all_hash_keys)) if should_show_key_rows?
46
+
42
47
  expanded_rows
43
48
  end
44
49
 
@@ -84,8 +89,8 @@ module ClassMetrix
84
89
  end
85
90
 
86
91
  def extract_hash_key_value(hash, key)
87
- if @value_processor.has_hash_key?(hash, key)
88
- hash_value = @value_processor.safe_hash_lookup(hash, key)
92
+ if @value_processor.has_hash_key?(hash, key, debug_mode: @options.fetch(:debug_mode, false))
93
+ hash_value = @value_processor.safe_hash_lookup(hash, key, debug_mode: @options.fetch(:debug_mode, false))
89
94
  process_value(hash_value)
90
95
  else
91
96
  get_null_value