class-metrix 0.1.2

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +88 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +41 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +417 -0
  8. data/RELEASE_GUIDE.md +158 -0
  9. data/Rakefile +12 -0
  10. data/examples/README.md +155 -0
  11. data/examples/advanced/error_handling.rb +199 -0
  12. data/examples/advanced/hash_expansion.rb +180 -0
  13. data/examples/basic/01_simple_constants.rb +56 -0
  14. data/examples/basic/02_simple_methods.rb +99 -0
  15. data/examples/basic/03_multi_type_extraction.rb +116 -0
  16. data/examples/components/configurable_reports.rb +201 -0
  17. data/examples/csv_output_demo.rb +237 -0
  18. data/examples/real_world/microservices_audit.rb +312 -0
  19. data/lib/class_metrix/extractor.rb +121 -0
  20. data/lib/class_metrix/extractors/constants_extractor.rb +87 -0
  21. data/lib/class_metrix/extractors/methods_extractor.rb +87 -0
  22. data/lib/class_metrix/extractors/multi_type_extractor.rb +66 -0
  23. data/lib/class_metrix/formatters/base/base_component.rb +62 -0
  24. data/lib/class_metrix/formatters/base/base_formatter.rb +93 -0
  25. data/lib/class_metrix/formatters/components/footer_component.rb +67 -0
  26. data/lib/class_metrix/formatters/components/generic_header_component.rb +87 -0
  27. data/lib/class_metrix/formatters/components/header_component.rb +92 -0
  28. data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +140 -0
  29. data/lib/class_metrix/formatters/components/table_component.rb +268 -0
  30. data/lib/class_metrix/formatters/csv_formatter.rb +98 -0
  31. data/lib/class_metrix/formatters/markdown_formatter.rb +184 -0
  32. data/lib/class_metrix/formatters/shared/csv_table_builder.rb +21 -0
  33. data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +97 -0
  34. data/lib/class_metrix/formatters/shared/table_builder.rb +267 -0
  35. data/lib/class_metrix/formatters/shared/value_processor.rb +78 -0
  36. data/lib/class_metrix/processors/value_processor.rb +40 -0
  37. data/lib/class_metrix/utils/class_resolver.rb +20 -0
  38. data/lib/class_metrix/version.rb +5 -0
  39. data/lib/class_metrix.rb +12 -0
  40. data/sig/class/metrix.rbs +6 -0
  41. metadata +118 -0
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "value_processor"
4
+
5
+ module ClassMetrix
6
+ module Formatters
7
+ module Shared
8
+ class TableBuilder
9
+ attr_reader :data, :expand_hashes, :options
10
+
11
+ def initialize(data, expand_hashes = false, options = {})
12
+ @data = data
13
+ @expand_hashes = expand_hashes
14
+ @options = options
15
+ @value_processor = ValueProcessor
16
+ end
17
+
18
+ def build_simple_table
19
+ {
20
+ headers: @data[:headers],
21
+ rows: @data[:rows].map { |row| process_row(row) }
22
+ }
23
+ end
24
+
25
+ def build_expanded_table
26
+ return build_simple_table unless @expand_hashes
27
+
28
+ headers = @data[:headers]
29
+ expanded_rows = process_rows_for_expansion(headers)
30
+
31
+ {
32
+ headers: headers,
33
+ rows: expanded_rows
34
+ }
35
+ end
36
+
37
+ def build_flattened_table
38
+ return build_simple_table unless @expand_hashes
39
+
40
+ headers = @data[:headers]
41
+ rows = @data[:rows]
42
+
43
+ all_hash_keys = collect_all_hash_keys(rows, headers)
44
+ flattened_headers = create_flattened_headers(headers, all_hash_keys)
45
+ flattened_rows = create_flattened_rows(rows, headers, all_hash_keys)
46
+
47
+ {
48
+ headers: flattened_headers,
49
+ rows: flattened_rows
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def process_rows_for_expansion(headers)
56
+ expanded_rows = []
57
+
58
+ @data[:rows].each do |row|
59
+ if row_has_expandable_hash?(row)
60
+ expanded_rows.concat(expand_row(row, headers))
61
+ else
62
+ expanded_rows << process_row(row)
63
+ end
64
+ end
65
+
66
+ expanded_rows
67
+ end
68
+
69
+ def row_has_expandable_hash?(row)
70
+ row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
71
+ end
72
+
73
+ def create_flattened_rows(rows, headers, all_hash_keys)
74
+ rows.map do |row|
75
+ flatten_row(row, headers, all_hash_keys)
76
+ end
77
+ end
78
+
79
+ def has_type_column?
80
+ @data[:headers].first == "Type"
81
+ end
82
+
83
+ def behavior_column_index
84
+ has_type_column? ? 1 : 0
85
+ end
86
+
87
+ def value_start_index
88
+ has_type_column? ? 2 : 1
89
+ end
90
+
91
+ def class_headers
92
+ if has_type_column?
93
+ @data[:headers][2..] # Skip "Type" and "Behavior"
94
+ else
95
+ @data[:headers][1..] # Skip first column (behavior name)
96
+ end
97
+ end
98
+
99
+ def process_row(row)
100
+ row.map { |value| process_value(value) }
101
+ end
102
+
103
+ def process_value(value)
104
+ # This will be overridden by subclasses
105
+ value
106
+ end
107
+
108
+ def expand_row(row, _headers)
109
+ behavior_name = row[behavior_column_index]
110
+ values = row[value_start_index..]
111
+
112
+ all_hash_keys = collect_hash_keys_from_values(values)
113
+ return [process_row(row)] if all_hash_keys.empty?
114
+
115
+ build_expanded_row_set(row, behavior_name, values, all_hash_keys)
116
+ end
117
+
118
+ def collect_hash_keys_from_values(values)
119
+ 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)
122
+ end
123
+ all_hash_keys
124
+ end
125
+
126
+ def build_expanded_row_set(row, behavior_name, values, all_hash_keys)
127
+ expanded_rows = []
128
+ expanded_rows << build_main_row(row, behavior_name, values)
129
+ expanded_rows.concat(build_sub_rows(all_hash_keys, values))
130
+ expanded_rows
131
+ end
132
+
133
+ def build_main_row(row, behavior_name, values)
134
+ processed_values = values.map { |value| process_value(value) }
135
+
136
+ if has_type_column?
137
+ [row[0], behavior_name] + processed_values
138
+ else
139
+ [behavior_name] + processed_values
140
+ end
141
+ end
142
+
143
+ def build_sub_rows(all_hash_keys, values)
144
+ all_hash_keys.to_a.sort.map do |key|
145
+ build_single_sub_row(key, values)
146
+ end
147
+ end
148
+
149
+ def build_single_sub_row(key, values)
150
+ path_name = ".#{key}"
151
+ key_values = extract_key_values(values, key)
152
+
153
+ if has_type_column?
154
+ ["", path_name] + key_values # Empty type for sub-rows
155
+ else
156
+ [path_name] + key_values
157
+ end
158
+ end
159
+
160
+ def extract_key_values(values, key)
161
+ values.map do |value|
162
+ extract_single_key_value(value, key)
163
+ end
164
+ end
165
+
166
+ def extract_single_key_value(value, key)
167
+ if value.is_a?(Hash)
168
+ extract_hash_value_for_key(value, key)
169
+ else
170
+ get_null_value
171
+ end
172
+ end
173
+
174
+ 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)
177
+ process_value(hash_value)
178
+ else
179
+ get_null_value
180
+ end
181
+ end
182
+
183
+ def collect_all_hash_keys(rows, _headers)
184
+ value_start_idx = value_start_index
185
+ all_keys = {} # behavior_name => Set of keys
186
+
187
+ rows.each do |row|
188
+ collect_hash_keys_for_row(row, value_start_idx, all_keys)
189
+ end
190
+
191
+ all_keys
192
+ end
193
+
194
+ def collect_hash_keys_for_row(row, value_start_idx, all_keys)
195
+ behavior_name = row[behavior_column_index]
196
+ 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))
203
+ end
204
+ end
205
+
206
+ def create_flattened_headers(headers, all_hash_keys)
207
+ flattened = headers.dup
208
+ class_hdrs = class_headers
209
+
210
+ add_hash_key_headers(flattened, all_hash_keys, class_hdrs)
211
+ flattened
212
+ end
213
+
214
+ def add_hash_key_headers(flattened, all_hash_keys, class_hdrs)
215
+ all_hash_keys.each do |behavior_name, keys|
216
+ add_behavior_key_headers(flattened, behavior_name, keys, class_hdrs)
217
+ end
218
+ end
219
+
220
+ def add_behavior_key_headers(flattened, behavior_name, keys, class_hdrs)
221
+ keys.to_a.sort.each do |key|
222
+ class_hdrs.each do |class_name|
223
+ flattened << "#{behavior_name}.#{key}.#{class_name}"
224
+ end
225
+ end
226
+ end
227
+
228
+ def flatten_row(row, _headers, all_hash_keys)
229
+ behavior_name = row[behavior_column_index]
230
+ flattened = process_row(row)
231
+
232
+ add_flattened_hash_values(flattened, row, behavior_name, all_hash_keys)
233
+ flattened
234
+ end
235
+
236
+ def add_flattened_hash_values(flattened, row, behavior_name, all_hash_keys)
237
+ return unless all_hash_keys[behavior_name]
238
+
239
+ values = row[value_start_index..]
240
+
241
+ all_hash_keys[behavior_name].to_a.sort.each do |key|
242
+ add_flattened_values_for_key(flattened, values, key)
243
+ end
244
+ end
245
+
246
+ def add_flattened_values_for_key(flattened, values, key)
247
+ values.each do |value|
248
+ flattened << extract_flattened_value(value, key)
249
+ end
250
+ end
251
+
252
+ def extract_flattened_value(value, key)
253
+ if value.is_a?(Hash)
254
+ extract_hash_value_for_key(value, key)
255
+ else
256
+ get_null_value
257
+ end
258
+ end
259
+
260
+ def get_null_value
261
+ # Override in subclasses
262
+ ""
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Formatters
5
+ module Shared
6
+ class ValueProcessor
7
+ def self.process_for_markdown(value)
8
+ case value
9
+ when Hash
10
+ value.inspect
11
+ when Array
12
+ value.join(", ")
13
+ when true
14
+ "✅"
15
+ when false
16
+ "❌"
17
+ when nil
18
+ "❌"
19
+ when String
20
+ value
21
+ else
22
+ value.to_s
23
+ end
24
+ end
25
+
26
+ def self.process_for_csv(value, options = {})
27
+ null_value = options.fetch(:null_value, "")
28
+
29
+ case value
30
+ when Hash
31
+ # For non-flattened CSV, represent hash as JSON-like string
32
+ value.inspect
33
+ when Array
34
+ value.join("; ") # Use semicolon to avoid CSV comma conflicts
35
+ when true
36
+ "TRUE"
37
+ when false
38
+ "FALSE"
39
+ when nil
40
+ null_value
41
+ when String
42
+ # Clean up emoji for CSV compatibility
43
+ clean_value = value.gsub(/🚫|⚠️|✅|❌/, "").strip
44
+ clean_value.empty? ? null_value : clean_value
45
+ else
46
+ value.to_s
47
+ end
48
+ end
49
+
50
+ def self.safe_hash_lookup(hash, key)
51
+ # Properly handle false values in hash lookup
52
+ hash.key?(key.to_sym) ? hash[key.to_sym] : hash[key.to_s]
53
+ end
54
+
55
+ def self.has_hash_key?(hash, key)
56
+ hash.key?(key.to_sym) || hash.key?(key.to_s)
57
+ end
58
+
59
+ # Error message generators
60
+ def self.missing_constant
61
+ "🚫 Not defined"
62
+ end
63
+
64
+ def self.missing_method
65
+ "🚫 No method"
66
+ end
67
+
68
+ def self.handle_extraction_error(error)
69
+ "⚠️ Error: #{error.message}"
70
+ end
71
+
72
+ def self.missing_hash_key
73
+ "—"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../formatters/shared/value_processor"
4
+
5
+ module ClassMetrix
6
+ # Legacy value processor - now delegates to the new shared processor
7
+ # Maintained for backward compatibility with existing code
8
+ class ValueProcessor
9
+ def self.process(value, expand_hashes: false)
10
+ if expand_hashes && value.is_a?(Hash)
11
+ expand_hash(value)
12
+ else
13
+ Formatters::Shared::ValueProcessor.process_for_markdown(value)
14
+ end
15
+ rescue StandardError => e
16
+ "⚠️ #{e.class.name}"
17
+ end
18
+
19
+ def self.format_hash(hash)
20
+ Formatters::Shared::ValueProcessor.process_for_markdown(hash)
21
+ end
22
+
23
+ def self.expand_hash(hash)
24
+ # Return an array of key-value pairs for expansion
25
+ hash.map { |k, v| { key: k.to_s, value: process(v, expand_hashes: false) } }
26
+ end
27
+
28
+ def self.handle_extraction_error(error)
29
+ Formatters::Shared::ValueProcessor.handle_extraction_error(error)
30
+ end
31
+
32
+ def self.missing_constant
33
+ Formatters::Shared::ValueProcessor.missing_constant
34
+ end
35
+
36
+ def self.missing_method
37
+ Formatters::Shared::ValueProcessor.missing_method
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ class ClassResolver
5
+ def self.normalize_classes(classes)
6
+ classes.map do |klass|
7
+ case klass
8
+ when String
9
+ Object.const_get(klass)
10
+ when Class
11
+ klass
12
+ else
13
+ raise ArgumentError, "Invalid class: #{klass.inspect}"
14
+ end
15
+ end
16
+ rescue NameError => e
17
+ raise ArgumentError, "Class not found: #{e.message}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ VERSION = "0.1.2"
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "class_metrix/version"
4
+ require_relative "class_metrix/extractor"
5
+
6
+ module ClassMetrix
7
+ class Error < StandardError; end
8
+
9
+ def self.extract(*types)
10
+ Extractor.new(*types)
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ module Class
2
+ module Metrix
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: class-metrix
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Huy Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-06-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description: ClassMetrix allows you to easily extract and compare constants and class
42
+ methods across multiple Ruby classes, generating clean markdown tables for analysis
43
+ and documentation.
44
+ email:
45
+ - patrick204nqh@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".rspec"
51
+ - ".rubocop.yml"
52
+ - ".tool-versions"
53
+ - CHANGELOG.md
54
+ - LICENSE.txt
55
+ - README.md
56
+ - RELEASE_GUIDE.md
57
+ - Rakefile
58
+ - examples/README.md
59
+ - examples/advanced/error_handling.rb
60
+ - examples/advanced/hash_expansion.rb
61
+ - examples/basic/01_simple_constants.rb
62
+ - examples/basic/02_simple_methods.rb
63
+ - examples/basic/03_multi_type_extraction.rb
64
+ - examples/components/configurable_reports.rb
65
+ - examples/csv_output_demo.rb
66
+ - examples/real_world/microservices_audit.rb
67
+ - lib/class_metrix.rb
68
+ - lib/class_metrix/extractor.rb
69
+ - lib/class_metrix/extractors/constants_extractor.rb
70
+ - lib/class_metrix/extractors/methods_extractor.rb
71
+ - lib/class_metrix/extractors/multi_type_extractor.rb
72
+ - lib/class_metrix/formatters/base/base_component.rb
73
+ - lib/class_metrix/formatters/base/base_formatter.rb
74
+ - lib/class_metrix/formatters/components/footer_component.rb
75
+ - lib/class_metrix/formatters/components/generic_header_component.rb
76
+ - lib/class_metrix/formatters/components/header_component.rb
77
+ - lib/class_metrix/formatters/components/missing_behaviors_component.rb
78
+ - lib/class_metrix/formatters/components/table_component.rb
79
+ - lib/class_metrix/formatters/csv_formatter.rb
80
+ - lib/class_metrix/formatters/markdown_formatter.rb
81
+ - lib/class_metrix/formatters/shared/csv_table_builder.rb
82
+ - lib/class_metrix/formatters/shared/markdown_table_builder.rb
83
+ - lib/class_metrix/formatters/shared/table_builder.rb
84
+ - lib/class_metrix/formatters/shared/value_processor.rb
85
+ - lib/class_metrix/processors/value_processor.rb
86
+ - lib/class_metrix/utils/class_resolver.rb
87
+ - lib/class_metrix/version.rb
88
+ - sig/class/metrix.rbs
89
+ homepage: https://github.com/patrick204nqh/class-metrix
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ allowed_push_host: https://rubygems.org
94
+ homepage_uri: https://github.com/patrick204nqh/class-metrix
95
+ source_code_uri: https://github.com/patrick204nqh/class-metrix
96
+ changelog_uri: https://github.com/patrick204nqh/class-metrix/blob/master/CHANGELOG.md
97
+ rubygems_mfa_required: 'true'
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 3.1.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.5.22
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Simple extraction and comparison of Ruby class behaviors with clean markdown
117
+ output
118
+ test_files: []