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.
- checksums.yaml +4 -4
- data/.editorconfig +48 -0
- data/.vscode/README.md +72 -0
- data/.vscode/extensions.json +28 -0
- data/.vscode/launch.json +32 -0
- data/.vscode/settings.json +88 -0
- data/.vscode/tasks.json +99 -0
- data/CHANGELOG.md +71 -4
- data/README.md +41 -7
- data/docs/ARCHITECTURE.md +501 -0
- data/examples/README.md +161 -114
- data/examples/basic_usage.rb +88 -0
- data/examples/debug_levels_demo.rb +65 -0
- data/examples/debug_mode_demo.rb +75 -0
- data/examples/inheritance_and_modules.rb +155 -0
- data/lib/class_metrix/extractor.rb +106 -11
- data/lib/class_metrix/extractors/constants_extractor.rb +155 -21
- data/lib/class_metrix/extractors/methods_extractor.rb +186 -21
- data/lib/class_metrix/extractors/multi_type_extractor.rb +6 -5
- data/lib/class_metrix/formatters/components/footer_component.rb +1 -1
- data/lib/class_metrix/formatters/components/table_component/column_width_calculator.rb +56 -0
- data/lib/class_metrix/formatters/components/table_component/row_processor.rb +138 -0
- data/lib/class_metrix/formatters/components/table_component/table_data_extractor.rb +54 -0
- data/lib/class_metrix/formatters/components/table_component/table_renderer.rb +55 -0
- data/lib/class_metrix/formatters/components/table_component.rb +30 -244
- data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +10 -5
- data/lib/class_metrix/formatters/shared/table_builder.rb +84 -21
- data/lib/class_metrix/formatters/shared/value_processor.rb +72 -16
- data/lib/class_metrix/utils/debug_logger.rb +159 -0
- data/lib/class_metrix/version.rb +1 -1
- metadata +17 -9
- data/examples/advanced/error_handling.rb +0 -199
- data/examples/advanced/hash_expansion.rb +0 -180
- data/examples/basic/01_simple_constants.rb +0 -56
- data/examples/basic/02_simple_methods.rb +0 -99
- data/examples/basic/03_multi_type_extraction.rb +0 -116
- data/examples/components/configurable_reports.rb +0 -201
- data/examples/csv_output_demo.rb +0 -237
- data/examples/real_world/microservices_audit.rb +0 -312
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
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)
|
|
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 = @
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
38
|
+
rows = process_rows_for_expansion(headers)
|
|
30
39
|
|
|
31
40
|
{
|
|
32
41
|
headers: headers,
|
|
33
|
-
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
|
-
|
|
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].
|
|
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..]
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
value.inspect
|
|
50
|
+
process_hash_for_csv(value, logger)
|
|
33
51
|
when Array
|
|
34
|
-
value
|
|
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
|
-
|
|
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
|
|
62
|
+
process_other_for_csv(value, logger)
|
|
47
63
|
end
|
|
48
64
|
end
|
|
49
65
|
|
|
50
|
-
def self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|