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
@@ -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 = [] # : Array[Module]
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
@@ -19,7 +20,7 @@ module ClassMetrix
19
20
  # Build headers: ["Type", "Behavior", "Class1", "Class2", ...]
20
21
  headers = %w[Type Behavior] + @classes.map(&:name)
21
22
 
22
- all_rows = []
23
+ all_rows = [] # : Array[Array[untyped]]
23
24
 
24
25
  @types.each do |type|
25
26
  type_data = extract_single_type(type)
@@ -27,7 +28,7 @@ module ClassMetrix
27
28
  # Add rows with type prefix
28
29
  type_data[:rows].each do |row|
29
30
  behavior_name = row[0]
30
- values = row[1..]
31
+ values = row[1..] || []
31
32
 
32
33
  new_row = [type_label(type), behavior_name] + values
33
34
  all_rows << new_row
@@ -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
@@ -26,7 +26,7 @@ module ClassMetrix
26
26
  # Common options for all formatters
27
27
  title: nil,
28
28
  show_metadata: true,
29
- extraction_types: []
29
+ extraction_types: [] # : Array[Symbol]
30
30
  }
31
31
  end
32
32
 
@@ -52,11 +52,11 @@ module ClassMetrix
52
52
 
53
53
  def collect_all_hash_keys(rows, _headers)
54
54
  value_start_idx = value_start_index
55
- all_keys = {} # behavior_name => Set of keys
55
+ all_keys = {} # : Hash[String, Set[String]] # behavior_name => Set of keys
56
56
 
57
57
  rows.each do |row|
58
58
  behavior_name = row[behavior_column_index]
59
- values = row[value_start_idx..]
59
+ values = row[value_start_idx..] || []
60
60
 
61
61
  values.each do |value|
62
62
  if value.is_a?(Hash)
@@ -16,7 +16,7 @@ module ClassMetrix
16
16
  def generate
17
17
  return [] unless @show_footer
18
18
 
19
- output = []
19
+ output = [] # : Array[String]
20
20
 
21
21
  # Add separator line
22
22
  output << "---" if @show_separator
@@ -36,7 +36,7 @@ module ClassMetrix
36
36
  private
37
37
 
38
38
  def generate_default_footer
39
- output = []
39
+ output = [] # : Array[String]
40
40
 
41
41
  output << (@custom_footer || "*Report generated by ClassMetrix gem*")
42
42
 
@@ -49,11 +49,11 @@ module ClassMetrix
49
49
  end
50
50
 
51
51
  def generate_detailed_footer
52
- output = []
52
+ output = [] # : Array[String]
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
 
@@ -30,7 +30,7 @@ module ClassMetrix
30
30
  private
31
31
 
32
32
  def generate_markdown_header
33
- output = []
33
+ output = [] # : Array[String]
34
34
 
35
35
  # Add title
36
36
  if @title
@@ -58,7 +58,7 @@ module ClassMetrix
58
58
  end
59
59
 
60
60
  def generate_csv_header
61
- output = []
61
+ output = [] # : Array[String]
62
62
  comment_char = @options.fetch(:comment_char, "#")
63
63
 
64
64
  # Add title as comment
@@ -15,7 +15,7 @@ module ClassMetrix
15
15
  end
16
16
 
17
17
  def generate
18
- output = []
18
+ output = [] # : Array[String]
19
19
 
20
20
  # Add title
21
21
  output.concat(generate_title) if @title || @show_metadata
@@ -32,7 +32,7 @@ module ClassMetrix
32
32
  private
33
33
 
34
34
  def generate_title
35
- output = []
35
+ output = [] # : Array[String]
36
36
 
37
37
  if @title
38
38
  output << "# #{@title}"
@@ -46,7 +46,7 @@ module ClassMetrix
46
46
  end
47
47
 
48
48
  def generate_classes_section
49
- output = []
49
+ output = [] # : Array[String]
50
50
 
51
51
  has_type_column = @data[:headers].first == "Type"
52
52
  class_headers = if has_type_column
@@ -66,7 +66,7 @@ module ClassMetrix
66
66
  end
67
67
 
68
68
  def generate_extraction_info
69
- output = []
69
+ output = [] # : Array[String]
70
70
 
71
71
  output << "## Extraction Types"
72
72
  output << ""
@@ -9,7 +9,7 @@ module ClassMetrix
9
9
  @options = options
10
10
  @show_missing_summary = options.fetch(:show_missing_summary, false)
11
11
  @summary_style = options.fetch(:summary_style, :grouped) # :grouped, :flat, :detailed
12
- @missing_behaviors = {}
12
+ @missing_behaviors = {} # : Hash[String, Hash[String, String]]
13
13
  end
14
14
 
15
15
  def generate
@@ -30,7 +30,7 @@ module ClassMetrix
30
30
  end
31
31
 
32
32
  # Initialize missing behaviors tracking with hash to store behavior name and error message
33
- class_headers.each { |class_name| @missing_behaviors[class_name] = {} }
33
+ class_headers.each { |class_name| @missing_behaviors[class_name] = {} } # : Hash[String, Hash[String, String]]
34
34
 
35
35
  @data[:rows].each do |row|
36
36
  behavior_name = has_type_column ? row[1] : row[0]
@@ -63,7 +63,7 @@ module ClassMetrix
63
63
  end
64
64
 
65
65
  def generate_grouped_summary
66
- output = []
66
+ output = [] # : Array[String]
67
67
 
68
68
  output << "## Missing Behaviors Summary"
69
69
  output << ""
@@ -84,12 +84,12 @@ module ClassMetrix
84
84
  end
85
85
 
86
86
  def generate_flat_summary
87
- output = []
87
+ output = [] # : Array[String]
88
88
 
89
89
  output << "## Missing Behaviors"
90
90
  output << ""
91
91
 
92
- all_missing = []
92
+ all_missing = [] # : Array[String]
93
93
  @missing_behaviors.each do |class_name, behaviors_hash|
94
94
  behaviors_hash.each do |behavior_name, error_message|
95
95
  all_missing << "- **#{class_name}**: `#{behavior_name}` - #{error_message}"
@@ -103,7 +103,7 @@ module ClassMetrix
103
103
  end
104
104
 
105
105
  def generate_detailed_summary
106
- output = []
106
+ output = [] # : Array[String]
107
107
 
108
108
  total_missing = @missing_behaviors.values.map(&:size).sum
109
109
  total_classes = @missing_behaviors.keys.size
@@ -114,7 +114,7 @@ module ClassMetrix
114
114
  output << ""
115
115
 
116
116
  # Group by error type
117
- by_error_type = {}
117
+ by_error_type = {} # : Hash[String, Array[Hash[Symbol, String]]]
118
118
  @missing_behaviors.each do |class_name, behaviors_hash|
119
119
  behaviors_hash.each do |behavior_name, error_message|
120
120
  error_type = error_message.split.first(2).join(" ") # e.g., "🚫 Not", "⚠️ Error:"
@@ -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,141 @@
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
+ rest_values = row[1..] || []
20
+ rest_values.each do |value|
21
+ processed_row << ValueProcessor.process(value)
22
+ end
23
+ processed_row
24
+ end
25
+ end
26
+
27
+ def process_expanded_rows(rows)
28
+ expanded_rows = [] # : Array[Array[String]]
29
+
30
+ rows.each do |row|
31
+ if @data_extractor.row_has_expandable_hash?(row)
32
+ expanded_rows.concat(expand_row(row))
33
+ else
34
+ expanded_rows << process_non_hash_row(row)
35
+ end
36
+ end
37
+
38
+ expanded_rows
39
+ end
40
+
41
+ private
42
+
43
+ def process_non_hash_row(row)
44
+ if @data_extractor.has_type_column?
45
+ rest_values = row[2..] || []
46
+ [row[0], row[1]] + rest_values.map { |value| ValueProcessor.process(value) }
47
+ else
48
+ rest_values = row[1..] || []
49
+ [row[0]] + rest_values.map { |value| ValueProcessor.process(value) }
50
+ end
51
+ end
52
+
53
+ def expand_row(row)
54
+ row_data = @data_extractor.extract_row_data(row)
55
+ all_hash_keys = @data_extractor.collect_hash_keys(row_data[:values])
56
+
57
+ return [row] if all_hash_keys.empty?
58
+
59
+ build_expanded_rows(row_data, all_hash_keys, row)
60
+ end
61
+
62
+ def build_expanded_rows(row_data, all_hash_keys, original_row)
63
+ expanded_rows = [] # : Array[Array[String]]
64
+
65
+ # Add main row if configured to show
66
+ expanded_rows << build_main_expanded_row(row_data) if should_show_main_row?
67
+
68
+ # Add key rows if configured to show
69
+ expanded_rows.concat(build_key_rows(all_hash_keys, row_data, original_row)) if should_show_key_rows?
70
+
71
+ expanded_rows
72
+ end
73
+
74
+ def should_show_main_row?
75
+ !@hide_main_row
76
+ end
77
+
78
+ def should_show_key_rows?
79
+ !@hide_key_rows
80
+ end
81
+
82
+ def build_key_rows(all_hash_keys, row_data, original_row)
83
+ all_hash_keys.to_a.sort.map do |key|
84
+ build_key_row(key, row_data, original_row)
85
+ end
86
+ end
87
+
88
+ def build_main_expanded_row(row_data)
89
+ processed_values = row_data[:values].map { |value| ValueProcessor.process(value) }
90
+
91
+ if @data_extractor.has_type_column?
92
+ [row_data[:type_value], row_data[:behavior_name]] + processed_values
93
+ else
94
+ [row_data[:behavior_name]] + processed_values
95
+ end
96
+ end
97
+
98
+ def build_key_row(key, row_data, original_row)
99
+ path_name = ".#{key}"
100
+ key_values = process_key_values(key, row_data[:values], original_row)
101
+
102
+ if @data_extractor.has_type_column?
103
+ ["-", path_name] + key_values
104
+ else
105
+ [path_name] + key_values
106
+ end
107
+ end
108
+
109
+ def process_key_values(key, values, original_row)
110
+ values.map.with_index do |value, index|
111
+ if value.is_a?(Hash)
112
+ extract_hash_value(value, key)
113
+ else
114
+ handle_non_hash_value(original_row, index)
115
+ end
116
+ end
117
+ end
118
+
119
+ def extract_hash_value(hash, key)
120
+ has_key = hash.key?(key.to_sym) || hash.key?(key.to_s)
121
+ if has_key
122
+ hash_value = hash[key.to_sym] || hash[key.to_s]
123
+ ValueProcessor.process(hash_value)
124
+ else
125
+ "—" # Missing hash key
126
+ end
127
+ end
128
+
129
+ def handle_non_hash_value(original_row, index)
130
+ original_value = original_row[@data_extractor.has_type_column? ? (index + 2) : (index + 1)]
131
+ if original_value.to_s.include?("🚫")
132
+ "🚫 Not defined"
133
+ else
134
+ "—" # Non-hash values don't have this key
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end