Class: Rhales::HydrationDataAggregator
- Inherits:
-
Object
- Object
- Rhales::HydrationDataAggregator
- Includes:
- Utils::LoggingHelpers
- Defined in:
- lib/rhales/hydration/hydration_data_aggregator.rb
Overview
HydrationDataAggregator traverses the ViewComposition and executes
all
This class implements the server-side data aggregation phase of the
two-pass rendering model, handling:
- Traversal of the template dependency tree
- Direct serialization of props for
The aggregator replaces the HydrationRegistry by performing all data merging in a single, coordinated pass.
Defined Under Namespace
Classes: JSONSerializationError
Instance Method Summary collapse
-
#aggregate(composition) ⇒ Object
Aggregate all hydration data from the view composition.
-
#build_template_path_for_schema(parser) ⇒ Object
private
-
#deep_merge(target, source) ⇒ Object
private
-
#empty_data?(data) ⇒ Boolean
private
Check if data is considered empty for collision detection.
-
#extract_expected_keys(template_name, schema_content) ⇒ Object
private
Extract expected keys using hybrid approach.
-
#extract_keys_from_json_schema(template_name) ⇒ Object
private
Extract keys from pre-generated JSON schema (preferred method).
-
#extract_keys_from_zod_regex(schema_content) ⇒ Object
private
Extract keys from Zod schema using regex (fallback method).
-
#initialize(context) ⇒ HydrationDataAggregator
constructor
A new instance of HydrationDataAggregator.
-
#load_schema_cached(template_name) ⇒ Object
private
Load and cache JSON schema from disk.
-
#merge_data(target, source, strategy, window_attr, template_path) ⇒ Object
private
-
#process_schema_section(template_name, parser) ⇒ Object
private
Process schema section: Direct JSON serialization.
-
#process_template(template_name, parser) ⇒ Object
private
-
#shallow_merge(target, source, window_attr, template_path) ⇒ Object
private
-
#strict_merge(target, source, window_attr, template_path) ⇒ Object
private
Methods included from Utils::LoggingHelpers
#format_value, #log_timed_operation, #log_with_metadata
Methods included from Utils
Constructor Details
#initialize(context) ⇒ HydrationDataAggregator
Returns a new instance of HydrationDataAggregator.
25 26 27 28 29 30 31 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 25 def initialize(context) @context = context @window_attributes = {} @merged_data = {} @schema_cache = {} @schemas_dir = File.join(Dir.pwd, 'public/schemas') end |
Instance Method Details
#aggregate(composition) ⇒ Object
Aggregate all hydration data from the view composition
34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 34 def aggregate(composition) log_timed_operation(Rhales.logger, :debug, 'Schema aggregation started', template_count: composition.template_names.size ) do composition.each_document_in_render_order do |template_name, parser| process_template(template_name, parser) end @merged_data end end |
#build_template_path_for_schema(parser) ⇒ Object (private)
193 194 195 196 197 198 199 200 201 202 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 193 def build_template_path_for_schema(parser) schema_node = parser.section_node('schema') line_number = schema_node ? schema_node.location.start_line : 1 if parser.file_path "#{parser.file_path}:#{line_number}" else "<inline>:#{line_number}" end end |
#deep_merge(target, source) ⇒ Object (private)
147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 147 def deep_merge(target, source) result = target.dup source.each do |key, value| result[key] = if result.key?(key) && result[key].is_a?(Hash) && value.is_a?(Hash) deep_merge(result[key], value) else value end end result end |
#empty_data?(data) ⇒ Boolean (private)
Check if data is considered empty for collision detection
205 206 207 208 209 210 211 212 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 205 def empty_data?(data) return true if data.nil? return true if data == {} return true if data == [] return true if data.respond_to?(:empty?) && data.empty? false end |
#extract_expected_keys(template_name, schema_content) ⇒ Object (private)
Extract expected keys using hybrid approach
Tries to load pre-generated JSON schema first (reliable, handles all Zod patterns). Falls back to regex parsing for development (before schemas are generated).
To generate JSON schemas, run: rake rhales:schema:generate
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 220 def extract_expected_keys(template_name, schema_content) # Try JSON schema first (reliable, comprehensive) keys = extract_keys_from_json_schema(template_name) if keys&.any? (Rhales.logger, :debug, 'Schema keys extracted from JSON schema', template: template_name, key_count: keys.size, method: 'json_schema' ) return keys end # Fall back to regex (development, before schemas generated) keys = extract_keys_from_zod_regex(schema_content) if keys.any? (Rhales.logger, :debug, 'Schema keys extracted from Zod regex', template: template_name, key_count: keys.size, method: 'regex_fallback', note: 'Run rake rhales:schema:generate for reliable validation' ) end keys end |
#extract_keys_from_json_schema(template_name) ⇒ Object (private)
Extract keys from pre-generated JSON schema (preferred method)
243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 243 def extract_keys_from_json_schema(template_name) schema = load_schema_cached(template_name) return nil unless schema # Extract all properties from JSON schema properties = schema.dig('properties') || {} properties.keys rescue StandardError => ex (Rhales.logger, :debug, 'JSON schema loading failed', template: template_name, error: ex. ) nil end |
#extract_keys_from_zod_regex(schema_content) ⇒ Object (private)
Extract keys from Zod schema using regex (fallback method)
NOTE: This is a basic implementation that only matches simple patterns like: fieldName: z.string()
It will miss: - Nested object literals: settings: { theme: z.enum([…]) } - Complex compositions and unions - Multiline definitions
For reliable validation, generate JSON schemas with: rake rhales:schema:generate
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 283 def extract_keys_from_zod_regex(schema_content) return [] unless schema_content keys = [] schema_content.scan(/(\w+):\s*z\./) do |match| keys << match[0] end keys rescue StandardError => ex (Rhales.logger, :debug, 'Regex key extraction failed', error: ex., schema_preview: schema_content[0..100] ) [] end |
#load_schema_cached(template_name) ⇒ Object (private)
Load and cache JSON schema from disk
258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 258 def load_schema_cached(template_name) @schema_cache[template_name] ||= begin schema_path = File.join(@schemas_dir, "#{template_name}.json") return nil unless File.exist?(schema_path) JSON.parse(File.read(schema_path)) rescue JSON::ParserError, Errno::ENOENT => ex (Rhales.logger, :debug, 'Schema file error', template: template_name, path: schema_path, error: ex.class.name ) nil end end |
#merge_data(target, source, strategy, window_attr, template_path) ⇒ Object (private)
134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 134 def merge_data(target, source, strategy, window_attr, template_path) case strategy when 'deep' deep_merge(target, source) when 'shallow' shallow_merge(target, source, window_attr, template_path) when 'strict' strict_merge(target, source, window_attr, template_path) else raise ArgumentError, "Unknown merge strategy: #{strategy}" end end |
#process_schema_section(template_name, parser) ⇒ Object (private)
Process schema section: Direct JSON serialization
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 61 def process_schema_section(template_name, parser) window_attr = parser.schema_window || 'data' merge_strategy = parser.schema_merge_strategy # Extract client data for validation client_data = @context.client || {} schema_content = parser.section('schema') expected_keys = extract_expected_keys(template_name, schema_content) if schema_content # Log schema validation details if expected_keys && expected_keys.any? actual_keys = client_data.keys.map(&:to_s) missing_keys = expected_keys - actual_keys extra_keys = actual_keys - expected_keys if missing_keys.any? || extra_keys.any? (Rhales.logger, :warn, 'Hydration schema mismatch', template: build_template_path_for_schema(parser), window_attribute: window_attr, expected_keys: expected_keys, actual_keys: actual_keys, missing_keys: missing_keys, extra_keys: extra_keys, client_data_size: client_data.size ) else (Rhales.logger, :debug, 'Schema validation passed', template: build_template_path_for_schema(parser), window_attribute: window_attr, key_count: expected_keys.size, client_data_size: client_data.size ) end end # Build template path for error reporting template_path = build_template_path_for_schema(parser) # Direct serialization of client data (no template interpolation) processed_data = @context.client # Check for collisions only if the data is not empty if @window_attributes.key?(window_attr) && merge_strategy.nil? && !empty_data?(processed_data) existing = @window_attributes[window_attr] existing_data = @merged_data[window_attr] # Only raise collision error if existing data is also not empty unless empty_data?(existing_data) raise ::Rhales::HydrationCollisionError.new(window_attr, existing[:path], template_path) end end # Merge or set the data @merged_data[window_attr] = if @merged_data.key?(window_attr) merge_data( @merged_data[window_attr], processed_data, merge_strategy || 'deep', window_attr, template_path, ) else processed_data end # Track the window attribute @window_attributes[window_attr] = { path: template_path, merge_strategy: merge_strategy, section_type: :schema, } end |
#process_template(template_name, parser) ⇒ Object (private)
48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 48 def process_template(template_name, parser) # Process schema section if parser.schema_lang log_timed_operation(Rhales.logger, :debug, 'Schema validation', template: template_name, schema_lang: parser.schema_lang ) do process_schema_section(template_name, parser) end end end |
#shallow_merge(target, source, window_attr, template_path) ⇒ Object (private)
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 161 def shallow_merge(target, source, window_attr, template_path) result = target.dup source.each do |key, value| if result.key?(key) raise ::Rhales::HydrationCollisionError.new( "#{window_attr}.#{key}", @window_attributes[window_attr][:path], template_path, ) end result[key] = value end result end |
#strict_merge(target, source, window_attr, template_path) ⇒ Object (private)
178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/rhales/hydration/hydration_data_aggregator.rb', line 178 def strict_merge(target, source, window_attr, template_path) # In strict mode, any collision is an error target.each_key do |key| next unless source.key?(key) raise ::Rhales::HydrationCollisionError.new( "#{window_attr}.#{key}", @window_attributes[window_attr][:path], template_path, ) end target.merge(source) end |