Class: Rhales::HydrationDataAggregator

Inherits:
Object
  • Object
show all
Defined in:
lib/rhales/hydration_data_aggregator.rb

Overview

HydrationDataAggregator traverses the ViewComposition and executes all sections to produce a single, merged JSON structure.

This class implements the server-side data aggregation phase of the two-pass rendering model, handling: - Traversal of the template dependency tree - Execution of sections with full server context - Merge strategies (deep, shallow, strict) - Collision detection and error reporting

The aggregator replaces the HydrationRegistry by performing all data merging in a single, coordinated pass.

Defined Under Namespace

Classes: JSONSerializationError

Instance Method Summary collapse

Constructor Details

#initialize(context) ⇒ HydrationDataAggregator

Returns a new instance of HydrationDataAggregator.



23
24
25
26
27
# File 'lib/rhales/hydration_data_aggregator.rb', line 23

def initialize(context)
  @context = context
  @window_attributes = {}
  @merged_data = {}
end

Instance Method Details

#aggregate(composition) ⇒ Object

Aggregate all hydration data from the view composition



30
31
32
33
34
35
36
# File 'lib/rhales/hydration_data_aggregator.rb', line 30

def aggregate(composition)
  composition.each_document_in_render_order do |template_name, parser|
    process_template(template_name, parser)
  end

  @merged_data
end

#build_template_path(parser) ⇒ Object (private)



161
162
163
164
165
166
167
168
169
170
# File 'lib/rhales/hydration_data_aggregator.rb', line 161

def build_template_path(parser)
  data_node = parser.section_node('data')
  line_number = data_node ? data_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)



115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/rhales/hydration_data_aggregator.rb', line 115

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

Returns:

  • (Boolean)


173
174
175
176
177
178
179
180
# File 'lib/rhales/hydration_data_aggregator.rb', line 173

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

#merge_data(target, source, strategy, window_attr, template_path) ⇒ Object (private)



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/rhales/hydration_data_aggregator.rb', line 102

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_data_section(data_content, parser) ⇒ Object (private)



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/rhales/hydration_data_aggregator.rb', line 84

def process_data_section(data_content, parser)
  # Create a JSON-aware context wrapper for data sections
  json_context = JsonAwareContext.new(@context)

  # Process template variables in the data section
  processed_content = TemplateEngine.render(data_content, json_context)

  # Parse as JSON
  begin
    JSON.parse(processed_content)
  rescue JSON::ParserError => ex
    template_path = build_template_path(parser)
    raise JSONSerializationError,
      "Invalid JSON in data section at #{template_path}: #{ex.message}\n" \
      "Processed content: #{processed_content[0..200]}..."
  end
end

#process_template(_template_name, parser) ⇒ Object (private)



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/rhales/hydration_data_aggregator.rb', line 40

def process_template(_template_name, parser)
  data_content = parser.section('data')
  return unless data_content

  window_attr = parser.window_attribute || 'data'
  merge_strategy = parser.merge_strategy

  # Build template path for error reporting
  template_path = build_template_path(parser)

  # Process the data section first to check if it's empty
  processed_data = process_data_section(data_content, parser)

  # 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,
  }
end

#shallow_merge(target, source, window_attr, template_path) ⇒ Object (private)



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/rhales/hydration_data_aggregator.rb', line 129

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)



146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/rhales/hydration_data_aggregator.rb', line 146

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