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
@@ -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,40 @@ 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
+ @width_calculator = ColumnWidthCalculator.new(
28
+ table_style: @table_style,
29
+ min_column_width: @min_column_width,
30
+ max_column_width: @max_column_width
31
+ )
32
+ @renderer = TableRenderer.new(
33
+ table_style: @table_style,
34
+ max_column_width: @max_column_width
35
+ )
16
36
  end
17
37
 
18
38
  def generate
19
39
  return "" if @data[:headers].empty? || @data[:rows].empty?
20
40
 
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
41
  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("|")}|"
42
+ rows = if @expand_hashes
43
+ @row_processor.process_expanded_rows(@data[:rows])
44
+ else
45
+ @row_processor.process_simple_rows(@data[:rows])
46
+ end
47
+
48
+ column_widths = @width_calculator.calculate_widths(headers, rows)
49
+ @renderer.render_table(headers, rows, column_widths)
264
50
  end
265
51
  end
266
52
  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
@@ -37,8 +37,13 @@ module ClassMetrix
37
37
 
38
38
  def build_expanded_row_structure(row, behavior_name, values, all_hash_keys)
39
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))
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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "value_processor"
4
+ require_relative "../../utils/debug_logger"
4
5
 
5
6
  module ClassMetrix
6
7
  module Formatters
@@ -13,6 +14,13 @@ module ClassMetrix
13
14
  @expand_hashes = expand_hashes
14
15
  @options = options
15
16
  @value_processor = ValueProcessor
17
+ @debug_mode = options.fetch(:debug_mode, false)
18
+ @debug_level = options.fetch(:debug_level, :basic)
19
+ @logger = Utils::DebugLogger.new("TableBuilder", @debug_mode, @debug_level)
20
+
21
+ @logger.log("TableBuilder initialized with expand_hashes: #{expand_hashes}")
22
+ @logger.log("Data headers: #{@data[:headers]}", :detailed)
23
+ @logger.log("Number of rows: #{@data[:rows]&.length || 0}")
16
24
  end
17
25
 
18
26
  def build_simple_table
@@ -25,24 +33,26 @@ module ClassMetrix
25
33
  def build_expanded_table
26
34
  return build_simple_table unless @expand_hashes
27
35
 
36
+ @logger.log("Building expanded table with hash expansion")
28
37
  headers = @data[:headers]
29
- expanded_rows = process_rows_for_expansion(headers)
38
+ rows = process_rows_for_expansion(headers)
30
39
 
31
40
  {
32
41
  headers: headers,
33
- rows: expanded_rows
42
+ rows: rows
34
43
  }
35
44
  end
36
45
 
37
46
  def build_flattened_table
38
47
  return build_simple_table unless @expand_hashes
39
48
 
49
+ @logger.log("Building flattened table with hash expansion")
40
50
  headers = @data[:headers]
41
- rows = @data[:rows]
51
+ all_hash_keys = collect_all_hash_keys(@data[:rows], headers)
52
+ @logger.log("Collected hash keys: #{all_hash_keys}")
42
53
 
43
- all_hash_keys = collect_all_hash_keys(rows, headers)
44
54
  flattened_headers = create_flattened_headers(headers, all_hash_keys)
45
- flattened_rows = create_flattened_rows(rows, headers, all_hash_keys)
55
+ flattened_rows = create_flattened_rows(@data[:rows], headers, all_hash_keys)
46
56
 
47
57
  {
48
58
  headers: flattened_headers,
@@ -54,20 +64,33 @@ module ClassMetrix
54
64
 
55
65
  def process_rows_for_expansion(headers)
56
66
  expanded_rows = []
67
+ expandable_count = 0
57
68
 
58
- @data[:rows].each do |row|
69
+ @data[:rows].each_with_index do |row, index|
59
70
  if row_has_expandable_hash?(row)
71
+ expandable_count += 1
72
+ @logger.log("Row #{index} has expandable hashes", :detailed)
60
73
  expanded_rows.concat(expand_row(row, headers))
61
74
  else
75
+ @logger.log("Row #{index} has no expandable hashes", :verbose)
62
76
  expanded_rows << process_row(row)
63
77
  end
64
78
  end
65
79
 
80
+ @logger.log("Processed #{@data[:rows].length} rows, #{expandable_count} had expandable hashes")
66
81
  expanded_rows
67
82
  end
68
83
 
69
84
  def row_has_expandable_hash?(row)
70
- row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
85
+ values = row[value_start_index..]
86
+
87
+ # Use summary logging instead of per-value logging
88
+ @logger.log_hash_detection_summary(values)
89
+
90
+ # Only consider real Hash objects as expandable
91
+ result = values.any? { |cell| cell.is_a?(Hash) && cell.instance_of?(Hash) }
92
+ @logger.log_decision("Row expandable", "#{result ? "Has" : "No"} real Hash objects", :detailed)
93
+ result
71
94
  end
72
95
 
73
96
  def create_flattened_rows(rows, headers, all_hash_keys)
@@ -109,6 +132,8 @@ module ClassMetrix
109
132
  behavior_name = row[behavior_column_index]
110
133
  values = row[value_start_index..]
111
134
 
135
+ @logger.log("Expanding row for behavior '#{behavior_name}'")
136
+
112
137
  all_hash_keys = collect_hash_keys_from_values(values)
113
138
  return [process_row(row)] if all_hash_keys.empty?
114
139
 
@@ -117,19 +142,44 @@ module ClassMetrix
117
142
 
118
143
  def collect_hash_keys_from_values(values)
119
144
  all_hash_keys = Set.new
120
- values.each do |value|
121
- all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
145
+ hash_count = 0
146
+
147
+ values.each_with_index do |value, index|
148
+ # Be more strict about what we consider a "hash"
149
+ if value.is_a?(Hash) && value.instance_of?(Hash) && value.respond_to?(:keys)
150
+ hash_count += 1
151
+ keys = @logger.safe_keys(value)
152
+ @logger.log("Value #{index} is a real Hash with keys: #{keys}", :detailed)
153
+ all_hash_keys.merge(keys.map(&:to_s))
154
+ elsif value.is_a?(Hash)
155
+ @logger.log_anomaly("Hash-like object at index #{index} (#{@logger.safe_class(value)}) skipped")
156
+ end
122
157
  end
158
+
159
+ @logger.log("Collected hash keys from #{hash_count} real hashes: #{all_hash_keys.to_a}")
123
160
  all_hash_keys
124
161
  end
125
162
 
126
163
  def build_expanded_row_set(row, behavior_name, values, all_hash_keys)
127
164
  expanded_rows = []
128
- expanded_rows << build_main_row(row, behavior_name, values)
129
- expanded_rows.concat(build_sub_rows(all_hash_keys, values))
165
+
166
+ # Add main row if configured to show
167
+ expanded_rows << build_main_row(row, behavior_name, values) if should_show_main_row?
168
+
169
+ # Add sub rows if configured to show
170
+ expanded_rows.concat(build_sub_rows(all_hash_keys, values)) if should_show_key_rows?
171
+
130
172
  expanded_rows
131
173
  end
132
174
 
175
+ def should_show_main_row?
176
+ !@options.fetch(:hide_main_row, false)
177
+ end
178
+
179
+ def should_show_key_rows?
180
+ !@options.fetch(:hide_key_rows, true) # Default: hide key rows
181
+ end
182
+
133
183
  def build_main_row(row, behavior_name, values)
134
184
  processed_values = values.map { |value| process_value(value) }
135
185
 
@@ -172,8 +222,8 @@ module ClassMetrix
172
222
  end
173
223
 
174
224
  def extract_hash_value_for_key(hash, key)
175
- if @value_processor.has_hash_key?(hash, key)
176
- hash_value = @value_processor.safe_hash_lookup(hash, key)
225
+ if @value_processor.has_hash_key?(hash, key, debug_mode: @debug_mode)
226
+ hash_value = @value_processor.safe_hash_lookup(hash, key, debug_mode: @debug_mode)
177
227
  process_value(hash_value)
178
228
  else
179
229
  get_null_value
@@ -183,24 +233,37 @@ module ClassMetrix
183
233
  def collect_all_hash_keys(rows, _headers)
184
234
  value_start_idx = value_start_index
185
235
  all_keys = {} # behavior_name => Set of keys
236
+ total_hash_count = 0
186
237
 
187
- rows.each do |row|
188
- collect_hash_keys_for_row(row, value_start_idx, all_keys)
238
+ rows.each_with_index do |row, index|
239
+ hash_count = collect_hash_keys_for_row(row, value_start_idx, all_keys)
240
+ total_hash_count += hash_count
241
+ @logger.log("Row #{index}: #{hash_count} hashes found", :detailed)
189
242
  end
190
243
 
244
+ @logger.log("Final hash key collection: #{total_hash_count} total hashes, #{all_keys.keys.length} behaviors with hashes")
191
245
  all_keys
192
246
  end
193
247
 
194
248
  def collect_hash_keys_for_row(row, value_start_idx, all_keys)
195
249
  behavior_name = row[behavior_column_index]
196
250
  values = row[value_start_idx..]
197
-
198
- values.each do |value|
199
- next unless value.is_a?(Hash)
200
-
201
- all_keys[behavior_name] ||= Set.new
202
- all_keys[behavior_name].merge(value.keys.map(&:to_s))
251
+ hash_count = 0
252
+
253
+ values.each_with_index do |value, index|
254
+ # Be more strict about what we consider a "hash"
255
+ if value.is_a?(Hash) && value.instance_of?(Hash) && value.respond_to?(:keys)
256
+ hash_count += 1
257
+ keys = @logger.safe_keys(value)
258
+ @logger.log("Behavior '#{behavior_name}' value #{index}: Hash with keys #{keys}", :verbose)
259
+ all_keys[behavior_name] ||= Set.new
260
+ all_keys[behavior_name].merge(keys.map(&:to_s))
261
+ elsif value.is_a?(Hash)
262
+ @logger.log_anomaly("Hash-like object in '#{behavior_name}' at index #{index} (#{@logger.safe_class(value)}) skipped")
263
+ end
203
264
  end
265
+
266
+ hash_count
204
267
  end
205
268
 
206
269
  def create_flattened_headers(headers, all_hash_keys)
@@ -1,14 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../utils/debug_logger"
4
+
3
5
  module ClassMetrix
4
6
  module Formatters
5
7
  module Shared
6
8
  class ValueProcessor
7
- def self.process_for_markdown(value)
9
+ def self.process_for_markdown(value, debug_mode: false, debug_level: :basic)
10
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
11
+ logger.log_value_details(value)
12
+
8
13
  case value
9
14
  when Hash
10
- value.inspect
15
+ logger.log("Processing Hash with keys: #{logger.safe_keys(value)}", :detailed)
16
+ logger.safe_inspect(value)
11
17
  when Array
18
+ logger.log("Processing Array with #{logger.safe_length(value)} elements", :detailed)
12
19
  value.join(", ")
13
20
  when true
14
21
  "✅"
@@ -17,21 +24,32 @@ module ClassMetrix
17
24
  when nil
18
25
  "❌"
19
26
  when String
27
+ logger.log("Processing String: #{logger.safe_truncate(value, 50)}", :verbose)
20
28
  value
21
29
  else
22
- value.to_s
30
+ logger.log("Processing other type (#{logger.safe_class(value)}): #{logger.safe_truncate(logger.safe_inspect(value), 100)}",
31
+ :detailed)
32
+ logger.safe_to_s(value)
23
33
  end
24
34
  end
25
35
 
26
- def self.process_for_csv(value, options = {})
27
- null_value = options.fetch(:null_value, "")
36
+ def self.process_for_csv(value, options = {}, debug_mode: false, debug_level: :basic)
37
+ debug_mode = options.fetch(:debug_mode, false) if options.is_a?(Hash)
38
+ debug_level = options.fetch(:debug_level, :basic) if options.is_a?(Hash)
39
+ null_value = options.fetch(:null_value, "") if options.is_a?(Hash)
40
+
41
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
42
+ logger.log_value_details(value)
43
+
44
+ process_csv_value_by_type(value, logger, null_value)
45
+ end
28
46
 
47
+ def self.process_csv_value_by_type(value, logger, null_value)
29
48
  case value
30
49
  when Hash
31
- # For non-flattened CSV, represent hash as JSON-like string
32
- value.inspect
50
+ process_hash_for_csv(value, logger)
33
51
  when Array
34
- value.join("; ") # Use semicolon to avoid CSV comma conflicts
52
+ process_array_for_csv(value, logger)
35
53
  when true
36
54
  "TRUE"
37
55
  when false
@@ -39,21 +57,59 @@ module ClassMetrix
39
57
  when nil
40
58
  null_value
41
59
  when String
42
- # Clean up emoji for CSV compatibility
43
- clean_value = value.gsub(/🚫|⚠️|✅|❌/, "").strip
44
- clean_value.empty? ? null_value : clean_value
60
+ process_string_for_csv(value, null_value)
45
61
  else
46
- value.to_s
62
+ process_other_for_csv(value, logger)
47
63
  end
48
64
  end
49
65
 
50
- def self.safe_hash_lookup(hash, key)
66
+ def self.process_hash_for_csv(value, logger)
67
+ logger.log("Processing Hash for CSV with keys: #{logger.safe_keys(value)}", :detailed)
68
+ logger.safe_inspect(value)
69
+ end
70
+
71
+ def self.process_array_for_csv(value, logger)
72
+ logger.log("Processing Array for CSV with #{logger.safe_length(value)} elements", :detailed)
73
+ value.join("; ") # Use semicolon to avoid CSV comma conflicts
74
+ end
75
+
76
+ def self.process_string_for_csv(value, null_value)
77
+ clean_value = value.gsub(/🚫|⚠️|✅|❌/, "").strip
78
+ clean_value.empty? ? null_value : clean_value
79
+ end
80
+
81
+ def self.process_other_for_csv(value, logger)
82
+ logger.log(
83
+ "Processing other type for CSV (#{logger.safe_class(value)}): #{logger.safe_truncate(logger.safe_inspect(value),
84
+ 100)}", :detailed
85
+ )
86
+ logger.safe_to_s(value)
87
+ end
88
+
89
+ def self.safe_hash_lookup(hash, key, debug_mode: false, debug_level: :basic)
90
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
91
+ logger.log("Hash lookup for key '#{key}' in hash with keys: #{logger.safe_keys(hash)}", :verbose)
92
+
51
93
  # Properly handle false values in hash lookup
52
- hash.key?(key.to_sym) ? hash[key.to_sym] : hash[key.to_s]
94
+ begin
95
+ hash.key?(key.to_sym) ? hash[key.to_sym] : hash[key.to_s]
96
+ rescue StandardError => e
97
+ logger.log("Error during hash lookup: #{e.class.name}: #{e.message}")
98
+ nil
99
+ end
53
100
  end
54
101
 
55
- def self.has_hash_key?(hash, key)
56
- hash.key?(key.to_sym) || hash.key?(key.to_s)
102
+ def self.has_hash_key?(hash, key, debug_mode: false, debug_level: :basic)
103
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
104
+
105
+ begin
106
+ result = hash.key?(key.to_sym) || hash.key?(key.to_s)
107
+ logger.log("Checking if hash has key '#{key}': #{result} (hash keys: #{logger.safe_keys(hash)})", :verbose)
108
+ result
109
+ rescue StandardError => e
110
+ logger.log("Error checking hash key: #{e.class.name}: #{e.message}")
111
+ false
112
+ end
57
113
  end
58
114
 
59
115
  # Error message generators