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.
- checksums.yaml +4 -4
- data/.editorconfig +48 -0
- data/.vscode/README.md +128 -0
- data/.vscode/extensions.json +31 -0
- data/.vscode/keybindings.json +26 -0
- data/.vscode/launch.json +32 -0
- data/.vscode/rbs.code-snippets +61 -0
- data/.vscode/settings.json +112 -0
- data/.vscode/tasks.json +240 -0
- data/CHANGELOG.md +73 -4
- data/README.md +86 -22
- data/Steepfile +26 -0
- data/docs/ARCHITECTURE.md +501 -0
- data/docs/CHANGELOG_EVOLUTION_EXAMPLE.md +95 -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 +8 -7
- data/lib/class_metrix/formatters/base/base_formatter.rb +3 -3
- data/lib/class_metrix/formatters/components/footer_component.rb +4 -4
- data/lib/class_metrix/formatters/components/generic_header_component.rb +2 -2
- data/lib/class_metrix/formatters/components/header_component.rb +4 -4
- data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +7 -7
- 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 +141 -0
- data/lib/class_metrix/formatters/components/table_component/table_data_extractor.rb +57 -0
- data/lib/class_metrix/formatters/components/table_component/table_renderer.rb +55 -0
- data/lib/class_metrix/formatters/components/table_component.rb +32 -245
- data/lib/class_metrix/formatters/csv_formatter.rb +3 -3
- data/lib/class_metrix/formatters/markdown_formatter.rb +3 -4
- data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +12 -7
- data/lib/class_metrix/formatters/shared/table_builder.rb +92 -27
- 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
- data/sig/class_metrix.rbs +8 -0
- data/sig/extractor.rbs +54 -0
- data/sig/extractors.rbs +84 -0
- data/sig/formatters_base.rbs +59 -0
- data/sig/formatters_components.rbs +133 -0
- data/sig/formatters_main.rbs +20 -0
- data/sig/formatters_shared.rbs +102 -0
- data/sig/manifest.yaml +32 -0
- data/sig/utils.rbs +57 -0
- data/sig/value_processor.rbs +11 -0
- data/sig/version.rbs +4 -0
- metadata +60 -10
- 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
- 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
|
-
|
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 = [] # : 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
|
-
|
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
|
@@ -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/
|
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
|