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,268 @@
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
+ def initialize(data, options = {})
10
+ @data = data
11
+ @options = options
12
+ @expand_hashes = options.fetch(:expand_hashes, false)
13
+ @table_style = options.fetch(:table_style, :standard) # :standard, :compact, :wide
14
+ @min_column_width = options.fetch(:min_column_width, 3)
15
+ @max_column_width = options.fetch(:max_column_width, 50)
16
+ end
17
+
18
+ def generate
19
+ return "" if @data[:headers].empty? || @data[:rows].empty?
20
+
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
+ headers = @data[:headers]
40
+ rows = @data[:rows]
41
+
42
+ expanded_rows = process_expanded_rows(rows, headers)
43
+ build_table(headers, expanded_rows)
44
+ end
45
+
46
+ def process_simple_rows(rows)
47
+ rows.map do |row|
48
+ processed_row = [row[0]] # Keep the behavior name as-is
49
+ row[1..].each do |value|
50
+ processed_row << ValueProcessor.process(value)
51
+ end
52
+ processed_row
53
+ end
54
+ end
55
+
56
+ def process_expanded_rows(rows, headers)
57
+ expanded_rows = []
58
+
59
+ rows.each do |row|
60
+ if row_has_expandable_hash?(row, headers)
61
+ expanded_rows.concat(expand_row(row, headers))
62
+ else
63
+ expanded_rows << process_non_hash_row(row, headers)
64
+ end
65
+ end
66
+
67
+ expanded_rows
68
+ end
69
+
70
+ def row_has_expandable_hash?(row, headers)
71
+ has_type_column = headers.first == "Type"
72
+ value_start_index = has_type_column ? 2 : 1
73
+ row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
74
+ end
75
+
76
+ def process_non_hash_row(row, headers)
77
+ has_type_column = headers.first == "Type"
78
+ if has_type_column
79
+ [row[0], row[1]] + row[2..].map { |value| ValueProcessor.process(value) }
80
+ else
81
+ [row[0]] + row[1..].map { |value| ValueProcessor.process(value) }
82
+ end
83
+ end
84
+
85
+ def build_table(headers, rows)
86
+ col_widths = calculate_column_widths(headers, rows)
87
+
88
+ output = []
89
+ output << build_row(headers, col_widths)
90
+ output << build_separator(col_widths)
91
+
92
+ rows.each do |row|
93
+ output << build_row(row, col_widths)
94
+ end
95
+
96
+ output.join("\n")
97
+ end
98
+
99
+ def expand_row(row, headers)
100
+ has_type_column = headers.first == "Type"
101
+ row_data = extract_row_data(row, has_type_column)
102
+
103
+ all_hash_keys = collect_hash_keys(row_data[:values])
104
+ return [row] if all_hash_keys.empty?
105
+
106
+ build_expanded_rows(row_data, all_hash_keys, has_type_column, row)
107
+ end
108
+
109
+ def extract_row_data(row, has_type_column)
110
+ if has_type_column
111
+ build_type_column_data(row)
112
+ else
113
+ build_standard_column_data(row)
114
+ end
115
+ end
116
+
117
+ def build_type_column_data(row)
118
+ {
119
+ type_value: row[0],
120
+ behavior_name: row[1],
121
+ values: row[2..]
122
+ }
123
+ end
124
+
125
+ def build_standard_column_data(row)
126
+ {
127
+ behavior_name: row[0],
128
+ values: row[1..]
129
+ }
130
+ end
131
+
132
+ def collect_hash_keys(values)
133
+ all_hash_keys = Set.new
134
+ values.each do |value|
135
+ all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
136
+ end
137
+ all_hash_keys
138
+ end
139
+
140
+ def build_expanded_rows(row_data, all_hash_keys, has_type_column, original_row)
141
+ expanded_rows = []
142
+
143
+ # Add main row
144
+ expanded_rows << build_main_expanded_row(row_data, has_type_column)
145
+
146
+ # Add key rows
147
+ all_hash_keys.to_a.sort.each do |key|
148
+ expanded_rows << build_key_row(key, row_data, has_type_column, original_row)
149
+ end
150
+
151
+ expanded_rows
152
+ end
153
+
154
+ def build_main_expanded_row(row_data, has_type_column)
155
+ processed_values = row_data[:values].map { |value| ValueProcessor.process(value) }
156
+
157
+ if has_type_column
158
+ [row_data[:type_value], row_data[:behavior_name]] + processed_values
159
+ else
160
+ [row_data[:behavior_name]] + processed_values
161
+ end
162
+ end
163
+
164
+ def build_key_row(key, row_data, has_type_column, original_row)
165
+ path_name = ".#{key}"
166
+ key_values = process_key_values(key, row_data[:values], has_type_column, original_row)
167
+
168
+ if has_type_column
169
+ ["-", path_name] + key_values
170
+ else
171
+ [path_name] + key_values
172
+ end
173
+ end
174
+
175
+ def process_key_values(key, values, has_type_column, original_row)
176
+ values.map.with_index do |value, index|
177
+ if value.is_a?(Hash)
178
+ extract_hash_value(value, key)
179
+ else
180
+ handle_non_hash_value(original_row, index, has_type_column)
181
+ end
182
+ end
183
+ end
184
+
185
+ def extract_hash_value(hash, key)
186
+ has_key = hash.key?(key.to_sym) || hash.key?(key.to_s)
187
+ if has_key
188
+ hash_value = hash[key.to_sym] || hash[key.to_s]
189
+ ValueProcessor.process(hash_value)
190
+ else
191
+ "—" # Missing hash key
192
+ end
193
+ end
194
+
195
+ def handle_non_hash_value(original_row, index, has_type_column)
196
+ original_value = original_row[has_type_column ? (index + 2) : (index + 1)]
197
+ if original_value.to_s.include?("🚫")
198
+ "🚫 Not defined"
199
+ else
200
+ "—" # Non-hash values don't have this key
201
+ end
202
+ end
203
+
204
+ def calculate_column_widths(headers, rows)
205
+ col_count = headers.length
206
+ widths = initialize_column_widths(col_count, headers)
207
+
208
+ update_widths_from_rows(widths, rows, col_count)
209
+ apply_minimum_widths(widths)
210
+ end
211
+
212
+ def initialize_column_widths(col_count, headers)
213
+ widths = Array.new(col_count, 0)
214
+ headers.each_with_index do |header, i|
215
+ widths[i] = [widths[i], header.to_s.length].max
216
+ end
217
+ widths
218
+ end
219
+
220
+ def update_widths_from_rows(widths, rows, col_count)
221
+ rows.each do |row|
222
+ row.each_with_index do |cell, i|
223
+ next if i >= col_count
224
+
225
+ cell_width = calculate_cell_width(cell)
226
+ widths[i] = [widths[i], cell_width].max
227
+ end
228
+ end
229
+ end
230
+
231
+ def calculate_cell_width(cell)
232
+ cell_width = cell.to_s.length
233
+ # Apply max width limit for readability
234
+ @table_style == :compact ? [@max_column_width, cell_width].min : cell_width
235
+ end
236
+
237
+ def apply_minimum_widths(widths)
238
+ widths.map { |w| [w, @min_column_width].max }
239
+ end
240
+
241
+ def build_row(cells, col_widths)
242
+ formatted_cells = format_cells(cells, col_widths)
243
+ "|#{formatted_cells.join("|")}|"
244
+ end
245
+
246
+ def format_cells(cells, col_widths)
247
+ cells.each_with_index.map do |cell, i|
248
+ width = col_widths[i] || 10
249
+ cell_str = format_cell_content(cell)
250
+ " #{cell_str.ljust(width)} "
251
+ end
252
+ end
253
+
254
+ def format_cell_content(cell)
255
+ cell_str = cell.to_s
256
+ return cell_str unless @table_style == :compact && cell_str.length > @max_column_width
257
+
258
+ "#{cell_str[0...(@max_column_width - 3)]}..."
259
+ end
260
+
261
+ def build_separator(col_widths)
262
+ separators = col_widths.map { |width| "-" * (width + 2) }
263
+ "|#{separators.join("|")}|"
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require_relative "base/base_formatter"
5
+ require_relative "shared/csv_table_builder"
6
+ require_relative "components/generic_header_component"
7
+
8
+ module ClassMetrix
9
+ class CsvFormatter < Formatters::Base::BaseFormatter
10
+ def initialize(data, expand_hashes = false, options = {})
11
+ super
12
+ @table_builder = Formatters::Shared::CsvTableBuilder.new(data, expand_hashes, @options)
13
+ end
14
+
15
+ def format
16
+ return "" if @data[:headers].empty? || @data[:rows].empty?
17
+
18
+ output_lines = []
19
+
20
+ # Add CSV header comments
21
+ if @options.fetch(:show_metadata, true)
22
+ header_component = Formatters::Components::GenericHeaderComponent.new(@data, @options.merge(format: :csv))
23
+ header_lines = header_component.generate
24
+ output_lines.concat(header_lines)
25
+ end
26
+
27
+ # Generate table data based on hash handling mode
28
+ table_data = determine_table_data
29
+
30
+ # Convert to CSV format
31
+ csv_output = render_csv_table(table_data)
32
+ output_lines.concat(csv_output)
33
+
34
+ output_lines.join("\n")
35
+ end
36
+
37
+ protected
38
+
39
+ def default_options
40
+ super.merge({
41
+ separator: ",",
42
+ quote_char: '"',
43
+ flatten_hashes: false,
44
+ null_value: "",
45
+ comment_char: "#",
46
+ show_metadata: true
47
+ })
48
+ end
49
+
50
+ private
51
+
52
+ def determine_table_data
53
+ if @options[:flatten_hashes]
54
+ @table_builder.build_flattened_table
55
+ elsif @expand_hashes
56
+ @table_builder.build_expanded_table
57
+ else
58
+ @table_builder.build_simple_table
59
+ end
60
+ end
61
+
62
+ def render_csv_table(table_data)
63
+ headers = table_data[:headers]
64
+ rows = table_data[:rows]
65
+
66
+ return [] if headers.empty?
67
+
68
+ output = []
69
+ separator = @options.fetch(:separator, ",")
70
+ quote_char = @options.fetch(:quote_char, '"')
71
+
72
+ # Generate CSV rows
73
+ csv_rows = [headers] + rows
74
+
75
+ csv_rows.each do |row|
76
+ # Ensure all row values are strings and properly quoted
77
+ formatted_row = row.map { |cell| format_csv_cell(cell) }
78
+ csv_line = CSV.generate_line(formatted_row, col_sep: separator, quote_char: quote_char).chomp
79
+ output << csv_line
80
+ end
81
+
82
+ output
83
+ end
84
+
85
+ def format_csv_cell(value)
86
+ case value
87
+ when nil
88
+ @options.fetch(:null_value, "")
89
+ when String
90
+ # Clean up emoji and special characters for CSV compatibility
91
+ clean_value = value.gsub(/[🚫⚠️✅❌]/, "").strip
92
+ clean_value.empty? ? @options.fetch(:null_value, "") : clean_value
93
+ else
94
+ value.to_s
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base/base_formatter"
4
+ require_relative "shared/markdown_table_builder"
5
+ require_relative "components/generic_header_component"
6
+ require_relative "components/missing_behaviors_component"
7
+ require_relative "components/footer_component"
8
+
9
+ module ClassMetrix
10
+ class MarkdownFormatter < Formatters::Base::BaseFormatter
11
+ def initialize(data, expand_hashes = false, options = {})
12
+ super
13
+ @table_builder = Formatters::Shared::MarkdownTableBuilder.new(data, expand_hashes, @options)
14
+ end
15
+
16
+ def format
17
+ return "" if @data[:headers].empty? || @data[:rows].empty?
18
+
19
+ output = []
20
+
21
+ # Add header sections (title, classes, extraction info)
22
+ if @options.fetch(:show_metadata, true)
23
+ header_component = Formatters::Components::GenericHeaderComponent.new(@data, @options.merge(format: :markdown))
24
+ header_output = header_component.generate
25
+ output.concat(header_output) unless header_output.empty?
26
+ end
27
+
28
+ # Add main table
29
+ table_data = @expand_hashes ? @table_builder.build_expanded_table : @table_builder.build_simple_table
30
+ table_output = render_markdown_table(table_data)
31
+ unless table_output.empty?
32
+ # Join table rows with single newlines, then add as one section
33
+ output << table_output.join("\n")
34
+ end
35
+
36
+ # Add missing behaviors summary
37
+ if @options.fetch(:show_missing_summary, false)
38
+ missing_component = Formatters::Components::MissingBehaviorsComponent.new(@data, @options)
39
+ missing_output = missing_component.generate
40
+ output.concat(missing_output) unless missing_output.empty?
41
+ end
42
+
43
+ # Add footer
44
+ if @options.fetch(:show_footer, true)
45
+ footer_component = Formatters::Components::FooterComponent.new(@options)
46
+ footer_output = footer_component.generate
47
+ output.concat(footer_output) unless footer_output.empty?
48
+ end
49
+
50
+ # Join with newlines, filtering out empty sections
51
+ output.reject { |section| section.nil? || section == "" }.join("\n\n")
52
+ end
53
+
54
+ protected
55
+
56
+ def default_options
57
+ super.merge({
58
+ show_footer: true,
59
+ footer_style: :default,
60
+ show_timestamp: false,
61
+ show_classes: true,
62
+ show_extraction_info: true,
63
+ table_style: :standard,
64
+ summary_style: :grouped,
65
+ min_column_width: 3,
66
+ max_column_width: 20
67
+ })
68
+ end
69
+
70
+ private
71
+
72
+ def render_markdown_table(table_data)
73
+ headers = table_data[:headers]
74
+ rows = table_data[:rows]
75
+
76
+ return [] if headers.empty? || rows.empty?
77
+
78
+ widths = calculate_column_widths(headers, rows)
79
+
80
+ output = []
81
+ output << build_header_row(headers, widths)
82
+ output << build_separator_row(widths)
83
+ output.concat(build_data_rows(rows, headers, widths))
84
+
85
+ output
86
+ end
87
+
88
+ def build_header_row(headers, widths)
89
+ "| " + headers.map.with_index { |header, i| header.ljust(widths[i]) }.join(" | ") + " |"
90
+ end
91
+
92
+ def build_separator_row(widths)
93
+ "|" + widths.map { |width| "-" * (width + 2) }.join("|") + "|"
94
+ end
95
+
96
+ def build_data_rows(rows, headers, widths)
97
+ rows.map do |row|
98
+ build_single_data_row(row, headers, widths)
99
+ end
100
+ end
101
+
102
+ def build_single_data_row(row, headers, widths)
103
+ padded_row = pad_row_to_header_length(row, headers.length)
104
+ formatted_cells = format_row_cells(padded_row, widths)
105
+ "| " + formatted_cells.join(" | ") + " |"
106
+ end
107
+
108
+ def pad_row_to_header_length(row, header_length)
109
+ padding_needed = [0, header_length - row.length].max
110
+ row + Array.new(padding_needed, "")
111
+ end
112
+
113
+ def format_row_cells(row, widths)
114
+ row.map.with_index do |cell, i|
115
+ cell_str = truncate_cell(cell.to_s, widths[i])
116
+ cell_str.ljust(widths[i])
117
+ end
118
+ end
119
+
120
+ def calculate_column_widths(headers, rows)
121
+ table_style = @options.fetch(:table_style, :standard)
122
+ min_width = @options.fetch(:min_column_width, 3)
123
+ max_width = @options.fetch(:max_column_width, 30)
124
+
125
+ # Calculate the maximum width needed for each column
126
+ widths = headers.map(&:length)
127
+
128
+ rows.each do |row|
129
+ row.each_with_index do |cell, i|
130
+ next if i >= widths.length
131
+
132
+ cell_length = cell.to_s.length
133
+
134
+ # Apply max width limit for readability (like the old component did)
135
+ cell_length = [max_width, cell_length].min if %i[compact standard].include?(table_style)
136
+
137
+ widths[i] = [widths[i], cell_length].max
138
+ end
139
+ end
140
+
141
+ # Apply minimum width and final constraints
142
+ case table_style
143
+ when :compact
144
+ widths.map { |w| [[w, min_width].max, max_width].min }
145
+ when :wide
146
+ widths.map { |w| [w, min_width].max } # No max limit for wide style
147
+ else # :standard
148
+ widths.map { |w| [[w, min_width].max, max_width].min } # Apply max limit for standard too
149
+ end
150
+ end
151
+
152
+ def truncate_cell(text, max_width)
153
+ return text if text.length <= max_width
154
+ return text if max_width < 4
155
+
156
+ # Smart truncation for hash-like structures
157
+ if text.start_with?("{") && text.include?("=>")
158
+ # For hash representations, create a valid truncated version
159
+ truncate_hash_representation(text, max_width)
160
+ else
161
+ # Standard truncation for other values
162
+ "#{text[0..max_width - 4]}..."
163
+ end
164
+ end
165
+
166
+ def truncate_hash_representation(text, max_width)
167
+ return text if max_width < 6 # Need space for "{...}"
168
+
169
+ # Try to fit at least the first key-value pair
170
+ if max_width >= 15
171
+ # Look for the first complete key-value pair (handle :symbol=>value format)
172
+ match = text.match(/\{(:[^,}]+=>[^,}]+)/)
173
+ if match
174
+ first_pair = match[1]
175
+ needed_length = first_pair.length + 6 # For "{", ", ...}"
176
+ return "{#{first_pair}, ...}" if needed_length <= max_width
177
+ end
178
+ end
179
+
180
+ # Fallback to simple hash indicator
181
+ "{...}"
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "table_builder"
4
+
5
+ module ClassMetrix
6
+ module Formatters
7
+ module Shared
8
+ class CsvTableBuilder < TableBuilder
9
+ private
10
+
11
+ def process_value(value)
12
+ @value_processor.process_for_csv(value, @options)
13
+ end
14
+
15
+ def get_null_value
16
+ @options.fetch(:null_value, "")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "table_builder"
4
+
5
+ module ClassMetrix
6
+ module Formatters
7
+ module Shared
8
+ class MarkdownTableBuilder < TableBuilder
9
+ private
10
+
11
+ def process_value(value)
12
+ @value_processor.process_for_markdown(value)
13
+ end
14
+
15
+ def get_null_value
16
+ @value_processor.missing_hash_key
17
+ end
18
+
19
+ # Create proper flat table structure for hash expansion
20
+ def expand_row(row, _headers)
21
+ behavior_name = row[behavior_column_index]
22
+ values = row[value_start_index..]
23
+
24
+ all_hash_keys = collect_unique_hash_keys(values)
25
+ return [process_row(row)] if all_hash_keys.empty?
26
+
27
+ build_expanded_row_structure(row, behavior_name, values, all_hash_keys)
28
+ end
29
+
30
+ def collect_unique_hash_keys(values)
31
+ all_hash_keys = Set.new
32
+ values.each do |value|
33
+ all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
34
+ end
35
+ all_hash_keys
36
+ end
37
+
38
+ def build_expanded_row_structure(row, behavior_name, values, all_hash_keys)
39
+ expanded_rows = []
40
+ expanded_rows << build_main_row(row, behavior_name, values)
41
+ expanded_rows.concat(build_hash_key_rows(row, behavior_name, values, all_hash_keys))
42
+ expanded_rows
43
+ end
44
+
45
+ def build_main_row(row, behavior_name, values)
46
+ processed_values = values.map { |value| process_value(value) }
47
+
48
+ if has_type_column?
49
+ [row[0], behavior_name] + processed_values
50
+ else
51
+ [behavior_name] + processed_values
52
+ end
53
+ end
54
+
55
+ def build_hash_key_rows(row, behavior_name, values, all_hash_keys)
56
+ all_hash_keys.to_a.sort.map do |key|
57
+ build_single_key_row(row, behavior_name, values, key)
58
+ end
59
+ end
60
+
61
+ def build_single_key_row(row, behavior_name, values, key)
62
+ path_name = "#{behavior_name}.#{key}"
63
+ key_values = extract_key_values_for_row(values, key)
64
+
65
+ if has_type_column?
66
+ [row[0], path_name] + key_values # Keep original type
67
+ else
68
+ [path_name] + key_values
69
+ end
70
+ end
71
+
72
+ def extract_key_values_for_row(values, key)
73
+ values.map do |value|
74
+ extract_single_key_value(value, key)
75
+ end
76
+ end
77
+
78
+ def extract_single_key_value(value, key)
79
+ if value.is_a?(Hash)
80
+ extract_hash_key_value(value, key)
81
+ else
82
+ get_null_value
83
+ end
84
+ end
85
+
86
+ 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)
89
+ process_value(hash_value)
90
+ else
91
+ get_null_value
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end