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
|
@@ -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
|
-
|
|
23
|
-
|
|
22
|
+
{ headers: headers, rows: rows }
|
|
23
|
+
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
methods = if inheritance_or_modules_enabled?
|
|
59
|
+
get_comprehensive_methods(klass)
|
|
60
|
+
else
|
|
61
|
+
klass.singleton_methods(false)
|
|
62
|
+
end
|
|
63
|
+
all_methods.merge(methods.map(&:to_s))
|
|
50
64
|
end
|
|
51
65
|
|
|
52
66
|
all_methods.to_a.sort
|
|
53
67
|
end
|
|
54
68
|
|
|
69
|
+
def inheritance_or_modules_enabled?
|
|
70
|
+
@options[:include_inherited] || @options[:include_modules]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_comprehensive_methods(klass)
|
|
74
|
+
methods = Set.new
|
|
75
|
+
methods.merge(klass.singleton_methods(false))
|
|
76
|
+
|
|
77
|
+
methods.merge(get_inherited_methods(klass)) if @options[:include_inherited]
|
|
78
|
+
|
|
79
|
+
methods.merge(get_module_methods(klass)) if @options[:include_modules]
|
|
80
|
+
|
|
81
|
+
methods.to_a
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def get_inherited_methods(klass)
|
|
85
|
+
methods = Set.new
|
|
86
|
+
parent = klass.superclass
|
|
87
|
+
|
|
88
|
+
while parent && !core_class?(parent)
|
|
89
|
+
methods.merge(parent.singleton_methods(false))
|
|
90
|
+
parent = parent.superclass
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
methods
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def get_module_methods(klass)
|
|
97
|
+
methods = Set.new
|
|
98
|
+
all_singleton_modules = get_all_singleton_modules(klass)
|
|
99
|
+
|
|
100
|
+
all_singleton_modules.each do |mod|
|
|
101
|
+
next if core_module?(mod)
|
|
102
|
+
|
|
103
|
+
module_methods = mod.instance_methods(false).map(&:to_s)
|
|
104
|
+
# Filter out methods that shouldn't be called directly
|
|
105
|
+
module_methods = module_methods.reject { |method| excluded_module_method?(method) }
|
|
106
|
+
methods.merge(module_methods)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
methods
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def excluded_module_method?(method_name)
|
|
113
|
+
# Methods that require arguments or shouldn't be called directly
|
|
114
|
+
%w[included extended prepended].include?(method_name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def get_all_singleton_modules(klass)
|
|
118
|
+
modules = []
|
|
119
|
+
modules.concat(klass.singleton_class.included_modules)
|
|
120
|
+
|
|
121
|
+
if @options[:include_inherited]
|
|
122
|
+
parent = klass.superclass
|
|
123
|
+
while parent && !core_class?(parent)
|
|
124
|
+
modules.concat(parent.singleton_class.included_modules)
|
|
125
|
+
parent = parent.superclass
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
modules
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def core_class?(klass)
|
|
133
|
+
[Object, BasicObject].include?(klass)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def core_module?(mod)
|
|
137
|
+
[Kernel, Module, Class].include?(mod)
|
|
138
|
+
end
|
|
139
|
+
|
|
55
140
|
def apply_filters(method_names)
|
|
56
141
|
return method_names if @filters.empty?
|
|
57
142
|
|
|
@@ -72,9 +157,10 @@ module ClassMetrix
|
|
|
72
157
|
end
|
|
73
158
|
|
|
74
159
|
def call_class_method(klass, method_name)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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,
|
|
8
|
+
def initialize(classes, types, filters, extraction_config = {})
|
|
9
9
|
@classes = classes
|
|
10
10
|
@types = types
|
|
11
11
|
@filters = filters
|
|
12
|
-
@modules = modules
|
|
13
|
-
@handle_errors = handle_errors
|
|
12
|
+
@modules = extraction_config[:modules] || []
|
|
13
|
+
@handle_errors = extraction_config[:handle_errors] || false
|
|
14
|
+
@options = extraction_config[:options] || {}
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def extract
|
|
@@ -42,9 +43,9 @@ module ClassMetrix
|
|
|
42
43
|
def extract_single_type(type)
|
|
43
44
|
case type
|
|
44
45
|
when :constants
|
|
45
|
-
ConstantsExtractor.new(@classes, @filters, @handle_errors).extract
|
|
46
|
+
ConstantsExtractor.new(@classes, @filters, @handle_errors, @options).extract
|
|
46
47
|
when :class_methods
|
|
47
|
-
MethodsExtractor.new(@classes, @filters, @handle_errors).extract
|
|
48
|
+
MethodsExtractor.new(@classes, @filters, @handle_errors, @options).extract
|
|
48
49
|
else
|
|
49
50
|
{ headers: [], rows: [] }
|
|
50
51
|
end
|
|
@@ -53,7 +53,7 @@ module ClassMetrix
|
|
|
53
53
|
|
|
54
54
|
output << "## Report Information"
|
|
55
55
|
output << ""
|
|
56
|
-
output << "- **Generated by**: [ClassMetrix gem](https://github.com/
|
|
56
|
+
output << "- **Generated by**: [ClassMetrix gem](https://github.com/patrick204nqh/class-metrix)"
|
|
57
57
|
output << "- **Generated at**: #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}"
|
|
58
58
|
output << "- **Ruby version**: #{RUBY_VERSION}"
|
|
59
59
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClassMetrix
|
|
4
|
+
module Formatters
|
|
5
|
+
module Components
|
|
6
|
+
class TableComponent
|
|
7
|
+
class ColumnWidthCalculator
|
|
8
|
+
def initialize(table_style: :standard, min_column_width: 3, max_column_width: 50)
|
|
9
|
+
@table_style = table_style
|
|
10
|
+
@min_column_width = min_column_width
|
|
11
|
+
@max_column_width = max_column_width
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def calculate_widths(headers, rows)
|
|
15
|
+
col_count = headers.length
|
|
16
|
+
widths = initialize_column_widths(col_count, headers)
|
|
17
|
+
|
|
18
|
+
update_widths_from_rows(widths, rows, col_count)
|
|
19
|
+
apply_minimum_widths(widths)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def initialize_column_widths(col_count, headers)
|
|
25
|
+
widths = Array.new(col_count, 0)
|
|
26
|
+
headers.each_with_index do |header, i|
|
|
27
|
+
widths[i] = [widths[i], header.to_s.length].max
|
|
28
|
+
end
|
|
29
|
+
widths
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update_widths_from_rows(widths, rows, col_count)
|
|
33
|
+
rows.each do |row|
|
|
34
|
+
row.each_with_index do |cell, i|
|
|
35
|
+
next if i >= col_count
|
|
36
|
+
|
|
37
|
+
cell_width = calculate_cell_width(cell)
|
|
38
|
+
widths[i] = [widths[i], cell_width].max
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def calculate_cell_width(cell)
|
|
44
|
+
cell_width = cell.to_s.length
|
|
45
|
+
# Apply max width limit for readability
|
|
46
|
+
@table_style == :compact ? [@max_column_width, cell_width].min : cell_width
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def apply_minimum_widths(widths)
|
|
50
|
+
widths.map { |w| [w, @min_column_width].max }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../processors/value_processor"
|
|
4
|
+
|
|
5
|
+
module ClassMetrix
|
|
6
|
+
module Formatters
|
|
7
|
+
module Components
|
|
8
|
+
class TableComponent
|
|
9
|
+
class RowProcessor
|
|
10
|
+
def initialize(data_extractor, options = {})
|
|
11
|
+
@data_extractor = data_extractor
|
|
12
|
+
@hide_main_row = options.fetch(:hide_main_row, false)
|
|
13
|
+
@hide_key_rows = options.fetch(:hide_key_rows, true) # Default: show only main rows
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process_simple_rows(rows)
|
|
17
|
+
rows.map do |row|
|
|
18
|
+
processed_row = [row[0]] # Keep the behavior name as-is
|
|
19
|
+
row[1..].each do |value|
|
|
20
|
+
processed_row << ValueProcessor.process(value)
|
|
21
|
+
end
|
|
22
|
+
processed_row
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def process_expanded_rows(rows)
|
|
27
|
+
expanded_rows = []
|
|
28
|
+
|
|
29
|
+
rows.each do |row|
|
|
30
|
+
if @data_extractor.row_has_expandable_hash?(row)
|
|
31
|
+
expanded_rows.concat(expand_row(row))
|
|
32
|
+
else
|
|
33
|
+
expanded_rows << process_non_hash_row(row)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
expanded_rows
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def process_non_hash_row(row)
|
|
43
|
+
if @data_extractor.has_type_column?
|
|
44
|
+
[row[0], row[1]] + row[2..].map { |value| ValueProcessor.process(value) }
|
|
45
|
+
else
|
|
46
|
+
[row[0]] + row[1..].map { |value| ValueProcessor.process(value) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def expand_row(row)
|
|
51
|
+
row_data = @data_extractor.extract_row_data(row)
|
|
52
|
+
all_hash_keys = @data_extractor.collect_hash_keys(row_data[:values])
|
|
53
|
+
|
|
54
|
+
return [row] if all_hash_keys.empty?
|
|
55
|
+
|
|
56
|
+
build_expanded_rows(row_data, all_hash_keys, row)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_expanded_rows(row_data, all_hash_keys, original_row)
|
|
60
|
+
expanded_rows = []
|
|
61
|
+
|
|
62
|
+
# Add main row if configured to show
|
|
63
|
+
expanded_rows << build_main_expanded_row(row_data) if should_show_main_row?
|
|
64
|
+
|
|
65
|
+
# Add key rows if configured to show
|
|
66
|
+
expanded_rows.concat(build_key_rows(all_hash_keys, row_data, original_row)) if should_show_key_rows?
|
|
67
|
+
|
|
68
|
+
expanded_rows
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def should_show_main_row?
|
|
72
|
+
!@hide_main_row
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def should_show_key_rows?
|
|
76
|
+
!@hide_key_rows
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_key_rows(all_hash_keys, row_data, original_row)
|
|
80
|
+
all_hash_keys.to_a.sort.map do |key|
|
|
81
|
+
build_key_row(key, row_data, original_row)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_main_expanded_row(row_data)
|
|
86
|
+
processed_values = row_data[:values].map { |value| ValueProcessor.process(value) }
|
|
87
|
+
|
|
88
|
+
if @data_extractor.has_type_column?
|
|
89
|
+
[row_data[:type_value], row_data[:behavior_name]] + processed_values
|
|
90
|
+
else
|
|
91
|
+
[row_data[:behavior_name]] + processed_values
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_key_row(key, row_data, original_row)
|
|
96
|
+
path_name = ".#{key}"
|
|
97
|
+
key_values = process_key_values(key, row_data[:values], original_row)
|
|
98
|
+
|
|
99
|
+
if @data_extractor.has_type_column?
|
|
100
|
+
["-", path_name] + key_values
|
|
101
|
+
else
|
|
102
|
+
[path_name] + key_values
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def process_key_values(key, values, original_row)
|
|
107
|
+
values.map.with_index do |value, index|
|
|
108
|
+
if value.is_a?(Hash)
|
|
109
|
+
extract_hash_value(value, key)
|
|
110
|
+
else
|
|
111
|
+
handle_non_hash_value(original_row, index)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def extract_hash_value(hash, key)
|
|
117
|
+
has_key = hash.key?(key.to_sym) || hash.key?(key.to_s)
|
|
118
|
+
if has_key
|
|
119
|
+
hash_value = hash[key.to_sym] || hash[key.to_s]
|
|
120
|
+
ValueProcessor.process(hash_value)
|
|
121
|
+
else
|
|
122
|
+
"—" # Missing hash key
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_non_hash_value(original_row, index)
|
|
127
|
+
original_value = original_row[@data_extractor.has_type_column? ? (index + 2) : (index + 1)]
|
|
128
|
+
if original_value.to_s.include?("🚫")
|
|
129
|
+
"🚫 Not defined"
|
|
130
|
+
else
|
|
131
|
+
"—" # Non-hash values don't have this key
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClassMetrix
|
|
4
|
+
module Formatters
|
|
5
|
+
module Components
|
|
6
|
+
class TableComponent
|
|
7
|
+
class TableDataExtractor
|
|
8
|
+
def initialize(headers)
|
|
9
|
+
@headers = headers
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def has_type_column?
|
|
13
|
+
@headers.first == "Type"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def value_start_index
|
|
17
|
+
has_type_column? ? 2 : 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def behavior_column_index
|
|
21
|
+
has_type_column? ? 1 : 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def extract_row_data(row)
|
|
25
|
+
if has_type_column?
|
|
26
|
+
{
|
|
27
|
+
type_value: row[0],
|
|
28
|
+
behavior_name: row[1],
|
|
29
|
+
values: row[2..]
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
{
|
|
33
|
+
behavior_name: row[0],
|
|
34
|
+
values: row[1..]
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def row_has_expandable_hash?(row)
|
|
40
|
+
row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def collect_hash_keys(values)
|
|
44
|
+
all_hash_keys = Set.new
|
|
45
|
+
values.each do |value|
|
|
46
|
+
all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
|
|
47
|
+
end
|
|
48
|
+
all_hash_keys
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClassMetrix
|
|
4
|
+
module Formatters
|
|
5
|
+
module Components
|
|
6
|
+
class TableComponent
|
|
7
|
+
class TableRenderer
|
|
8
|
+
def initialize(table_style: :standard, max_column_width: 50)
|
|
9
|
+
@table_style = table_style
|
|
10
|
+
@max_column_width = max_column_width
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render_table(headers, rows, column_widths)
|
|
14
|
+
output = []
|
|
15
|
+
output << build_row(headers, column_widths)
|
|
16
|
+
output << build_separator(column_widths)
|
|
17
|
+
|
|
18
|
+
rows.each do |row|
|
|
19
|
+
output << build_row(row, column_widths)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
output.join("\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def build_row(cells, col_widths)
|
|
28
|
+
formatted_cells = format_cells(cells, col_widths)
|
|
29
|
+
"|#{formatted_cells.join("|")}|"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_cells(cells, col_widths)
|
|
33
|
+
cells.each_with_index.map do |cell, i|
|
|
34
|
+
width = col_widths[i] || 10
|
|
35
|
+
cell_str = format_cell_content(cell)
|
|
36
|
+
" #{cell_str.ljust(width)} "
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_cell_content(cell)
|
|
41
|
+
cell_str = cell.to_s
|
|
42
|
+
return cell_str unless @table_style == :compact && cell_str.length > @max_column_width
|
|
43
|
+
|
|
44
|
+
"#{cell_str[0...(@max_column_width - 3)]}..."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_separator(col_widths)
|
|
48
|
+
separators = col_widths.map { |width| "-" * (width + 2) }
|
|
49
|
+
"|#{separators.join("|")}|"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|