rhales 0.4.0 → 0.5.3

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/renovate.json5 +52 -0
  3. data/.github/workflows/ci.yml +123 -0
  4. data/.github/workflows/claude-code-review.yml +69 -0
  5. data/.github/workflows/claude.yml +49 -0
  6. data/.github/workflows/code-smells.yml +146 -0
  7. data/.github/workflows/ruby-lint.yml +78 -0
  8. data/.github/workflows/yardoc.yml +126 -0
  9. data/.gitignore +55 -0
  10. data/.pr_agent.toml +63 -0
  11. data/.pre-commit-config.yaml +89 -0
  12. data/.prettierignore +8 -0
  13. data/.prettierrc +38 -0
  14. data/.reek.yml +98 -0
  15. data/.rubocop.yml +428 -0
  16. data/.serena/.gitignore +3 -0
  17. data/.yardopts +56 -0
  18. data/CHANGELOG.md +44 -0
  19. data/CLAUDE.md +1 -1
  20. data/Gemfile +29 -0
  21. data/Gemfile.lock +189 -0
  22. data/README.md +686 -868
  23. data/Rakefile +46 -0
  24. data/debug_context.rb +25 -0
  25. data/demo/rhales-roda-demo/.gitignore +7 -0
  26. data/demo/rhales-roda-demo/Gemfile +32 -0
  27. data/demo/rhales-roda-demo/Gemfile.lock +151 -0
  28. data/demo/rhales-roda-demo/MAIL.md +405 -0
  29. data/demo/rhales-roda-demo/README.md +376 -0
  30. data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
  31. data/demo/rhales-roda-demo/Rakefile +49 -0
  32. data/demo/rhales-roda-demo/app.rb +325 -0
  33. data/demo/rhales-roda-demo/bin/rackup +26 -0
  34. data/demo/rhales-roda-demo/config.ru +13 -0
  35. data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
  36. data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
  37. data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
  38. data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
  39. data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
  40. data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
  41. data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
  42. data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
  43. data/demo/rhales-roda-demo/templates/home.rue +78 -0
  44. data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
  45. data/demo/rhales-roda-demo/templates/login.rue +65 -0
  46. data/demo/rhales-roda-demo/templates/logout.rue +25 -0
  47. data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
  48. data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
  49. data/demo/rhales-roda-demo/test_full_output.rb +27 -0
  50. data/demo/rhales-roda-demo/test_simple.rb +24 -0
  51. data/docs/.gitignore +9 -0
  52. data/docs/architecture/data-flow.md +499 -0
  53. data/examples/dashboard-with-charts.rue +271 -0
  54. data/examples/form-with-validation.rue +180 -0
  55. data/examples/simple-page.rue +61 -0
  56. data/examples/vue.rue +136 -0
  57. data/generate-json-schemas.ts +158 -0
  58. data/json_schemer_migration_summary.md +172 -0
  59. data/lib/rhales/adapters/base_auth.rb +2 -0
  60. data/lib/rhales/adapters/base_request.rb +2 -0
  61. data/lib/rhales/adapters/base_session.rb +2 -0
  62. data/lib/rhales/adapters.rb +7 -0
  63. data/lib/rhales/configuration.rb +47 -0
  64. data/lib/rhales/core/context.rb +354 -0
  65. data/lib/rhales/{rue_document.rb → core/rue_document.rb} +56 -38
  66. data/lib/rhales/{template_engine.rb → core/template_engine.rb} +66 -59
  67. data/lib/rhales/{view.rb → core/view.rb} +112 -135
  68. data/lib/rhales/{view_composition.rb → core/view_composition.rb} +78 -8
  69. data/lib/rhales/core.rb +9 -0
  70. data/lib/rhales/errors/hydration_collision_error.rb +2 -0
  71. data/lib/rhales/errors.rb +2 -0
  72. data/lib/rhales/{earliest_injection_detector.rb → hydration/earliest_injection_detector.rb} +4 -0
  73. data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
  74. data/lib/rhales/{hydration_endpoint.rb → hydration/hydration_endpoint.rb} +16 -12
  75. data/lib/rhales/{hydration_injector.rb → hydration/hydration_injector.rb} +4 -0
  76. data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
  77. data/lib/rhales/hydration/hydrator.rb +102 -0
  78. data/lib/rhales/{link_based_injection_detector.rb → hydration/link_based_injection_detector.rb} +4 -0
  79. data/lib/rhales/{mount_point_detector.rb → hydration/mount_point_detector.rb} +4 -0
  80. data/lib/rhales/{safe_injection_validator.rb → hydration/safe_injection_validator.rb} +4 -0
  81. data/lib/rhales/hydration.rb +13 -0
  82. data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +3 -1
  83. data/lib/rhales/{tilt.rb → integrations/tilt.rb} +22 -15
  84. data/lib/rhales/integrations.rb +6 -0
  85. data/lib/rhales/middleware/json_responder.rb +191 -0
  86. data/lib/rhales/middleware/schema_validator.rb +300 -0
  87. data/lib/rhales/middleware.rb +6 -0
  88. data/lib/rhales/parsers/handlebars_parser.rb +2 -0
  89. data/lib/rhales/parsers/rue_format_parser.rb +9 -7
  90. data/lib/rhales/parsers.rb +9 -0
  91. data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
  92. data/lib/rhales/utils/json_serializer.rb +114 -0
  93. data/lib/rhales/utils/logging_helpers.rb +75 -0
  94. data/lib/rhales/utils/schema_extractor.rb +132 -0
  95. data/lib/rhales/utils/schema_generator.rb +194 -0
  96. data/lib/rhales/utils.rb +40 -0
  97. data/lib/rhales/version.rb +3 -1
  98. data/lib/rhales.rb +41 -24
  99. data/lib/tasks/rhales_schema.rake +197 -0
  100. data/package.json +10 -0
  101. data/pnpm-lock.yaml +345 -0
  102. data/pnpm-workspace.yaml +2 -0
  103. data/proofs/error_handling.rb +79 -0
  104. data/proofs/expanded_object_inheritance.rb +82 -0
  105. data/proofs/partial_context_scoping_fix.rb +168 -0
  106. data/proofs/ui_context_partial_inheritance.rb +236 -0
  107. data/rhales.gemspec +14 -6
  108. data/schema_vs_data_comparison.md +254 -0
  109. data/test_direct_access.rb +36 -0
  110. metadata +141 -23
  111. data/CLAUDE.locale.txt +0 -7
  112. data/lib/rhales/context.rb +0 -239
  113. data/lib/rhales/hydration_data_aggregator.rb +0 -221
  114. data/lib/rhales/hydrator.rb +0 -141
  115. data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
@@ -1,10 +1,12 @@
1
1
  # lib/rhales/template_engine.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require 'erb'
4
- require_relative 'parsers/rue_format_parser'
5
- require_relative 'parsers/handlebars_parser'
6
+ require_relative '../parsers/rue_format_parser'
7
+ require_relative '../parsers/handlebars_parser'
6
8
  require_relative 'rue_document'
7
- require_relative 'hydrator'
9
+ require_relative '../hydration/hydrator'
8
10
 
9
11
  module Rhales
10
12
  # Rhales - Ruby Handlebars-style template engine
@@ -28,6 +30,8 @@ module Rhales
28
30
  # - {{#each items}} ... {{/each}} - Iteration with context
29
31
  # - {{> partial_name}} - Partial inclusion
30
32
  class TemplateEngine
33
+ include Rhales::Utils::LoggingHelpers
34
+
31
35
  class RenderError < ::Rhales::RenderError; end
32
36
  class PartialNotFoundError < RenderError; end
33
37
  class UndefinedVariableError < RenderError; end
@@ -43,31 +47,46 @@ module Rhales
43
47
  end
44
48
 
45
49
  def render
46
- # Check if this is a simple template or a full .rue file
47
- if simple_template?
48
- # Use HandlebarsParser for simple templates
49
- parser = HandlebarsParser.new(@template_content)
50
- parser.parse!
51
- render_content_nodes(parser.ast.children)
52
- else
53
- # Use RueDocument for .rue files
54
- @parser = RueDocument.new(@template_content)
55
- @parser.parse!
50
+ template_type = simple_template? ? :handlebars : :rue
51
+
52
+ log_timed_operation(Rhales.logger, :debug, 'Template compiled',
53
+ template_type: template_type, cached: false
54
+ ) do
55
+ # Check if this is a simple template or a full .rue file
56
+ if simple_template?
57
+ # Use HandlebarsParser for simple templates
58
+ parser = HandlebarsParser.new(@template_content)
59
+ parser.parse!
60
+ render_content_nodes(parser.ast.children)
61
+ else
62
+ # Use RueDocument for .rue files
63
+ @parser = RueDocument.new(@template_content)
64
+ @parser.parse!
56
65
 
57
- # Get template section via RueDocument
58
- template_content = @parser.section('template')
59
- raise RenderError, 'Missing template section' unless template_content
66
+ # Get template section via RueDocument
67
+ template_content = @parser.section('template')
68
+ raise RenderError, 'Missing template section' unless template_content
60
69
 
61
- # Render the template section as a simple template
62
- render_template_string(template_content)
70
+ # Render the template section as a simple template
71
+ render_template_string(template_content)
72
+ end
63
73
  end
64
74
  rescue ::Rhales::ParseError => ex
65
75
  # Parse errors already have good error messages with location
76
+ log_with_metadata(Rhales.logger, :error, 'Template parse error',
77
+ error: ex.message, line: ex.line, column: ex.column, section: ex.source_type
78
+ )
66
79
  raise RenderError, "Template parsing failed: #{ex.message}"
67
80
  rescue ::Rhales::ValidationError => ex
68
81
  # Validation errors from RueDocument
82
+ log_with_metadata(Rhales.logger, :error, 'Template validation error',
83
+ error: ex.message, template_type: :rue
84
+ )
69
85
  raise RenderError, "Template validation failed: #{ex.message}"
70
86
  rescue StandardError => ex
87
+ log_with_metadata(Rhales.logger, :error, 'Template render error',
88
+ error: ex.message, error_class: ex.class.name
89
+ )
71
90
  raise RenderError, "Template rendering failed: #{ex.message}"
72
91
  end
73
92
 
@@ -81,11 +100,6 @@ module Rhales
81
100
  @parser&.schema_path
82
101
  end
83
102
 
84
- # Access all data attributes from parsed .rue file
85
- def data_attributes
86
- @parser&.data_attributes || {}
87
- end
88
-
89
103
  # Get template variables used in the template
90
104
  def template_variables
91
105
  @parser&.template_variables || []
@@ -99,7 +113,7 @@ module Rhales
99
113
  private
100
114
 
101
115
  def simple_template?
102
- !@template_content.match?(/^<(data|template|logic)\b/)
116
+ !@template_content.match?(/^<(schema|template|logic)\b/)
103
117
  end
104
118
 
105
119
  def render_template_string(template_string)
@@ -131,7 +145,7 @@ module Rhales
131
145
  when :each_block
132
146
  result += render_each_block(node)
133
147
  when :handlebars_expression
134
- # Handle old format for data sections
148
+ # Handle handlebars expressions
135
149
  result += render_handlebars_expression(node)
136
150
  end
137
151
  end
@@ -144,7 +158,13 @@ module Rhales
144
158
  raw = node.value[:raw]
145
159
 
146
160
  value = get_variable_value(name)
147
- raw ? value.to_s : escape_html(value.to_s)
161
+
162
+ if raw
163
+ warn_if_unescaped(name, value, 'variable_expression')
164
+ value.to_s
165
+ else
166
+ escape_html(value.to_s)
167
+ end
148
168
  end
149
169
 
150
170
  def render_partial_expression(node)
@@ -205,7 +225,12 @@ module Rhales
205
225
  ''
206
226
  else # Variables
207
227
  value = get_variable_value(content)
208
- raw ? value.to_s : escape_html(value.to_s)
228
+ if raw
229
+ warn_if_unescaped(content, value, 'handlebars_expression')
230
+ value.to_s
231
+ else
232
+ escape_html(value.to_s)
233
+ end
209
234
  end
210
235
  end
211
236
 
@@ -216,8 +241,8 @@ module Rhales
216
241
  raise PartialNotFoundError, "Partial '#{partial_name}' not found" unless partial_content
217
242
 
218
243
  # Check if this is a .rue document with sections
219
- if partial_content.match?(/^<(data|template|logic)\b/)
220
- # Parse as RueDocument to handle data sections properly
244
+ if partial_content.match?(/^<(schema|template|logic)\b/)
245
+ # Parse as RueDocument to handle schema sections properly
221
246
  partial_doc = RueDocument.new(partial_content)
222
247
  partial_doc.parse!
223
248
 
@@ -225,19 +250,8 @@ module Rhales
225
250
  template_content = partial_doc.section('template')
226
251
  raise PartialNotFoundError, "Partial '#{partial_name}' missing template section" unless template_content
227
252
 
228
- # Process data section if present and merge with parent context
229
- merged_context = @context
230
- if partial_doc.section('data')
231
- # Create hydrator with parent context to process interpolations
232
- hydrator = Hydrator.new(partial_doc, @context)
233
- local_data = hydrator.processed_data_hash
234
-
235
- # Create merged context (local data takes precedence)
236
- merged_context = create_merged_context(@context, local_data)
237
- end
238
-
239
- # Render template with merged context
240
- engine = self.class.new(template_content, merged_context, partial_resolver: @partial_resolver)
253
+ # Render template with current context
254
+ engine = self.class.new(template_content, @context, partial_resolver: @partial_resolver)
241
255
  engine.render
242
256
  else
243
257
  # Simple template without sections - render as before
@@ -264,6 +278,16 @@ module Rhales
264
278
  end
265
279
  end
266
280
 
281
+ # Log warning for unescaped variable usage unless whitelisted
282
+ def warn_if_unescaped(variable_name, value, template_context)
283
+ return if Rhales.config.allowed_unescaped_variables.include?(variable_name.to_s)
284
+
285
+ log_with_metadata(
286
+ Rhales.logger, :warn, 'Unescaped variable usage',
287
+ variable: variable_name, value_type: value.class.name, template_context: template_context
288
+ )
289
+ end
290
+
267
291
  # Evaluate condition for if/unless blocks
268
292
  def evaluate_condition(condition)
269
293
  value = get_variable_value(condition)
@@ -297,23 +321,6 @@ module Rhales
297
321
  ERB::Util.html_escape(string)
298
322
  end
299
323
 
300
- # Create a new context with merged data
301
- def create_merged_context(parent_context, local_data)
302
- # Extract all props from parent context and merge with local data
303
- # Local data takes precedence over parent props
304
- merged_props = parent_context.props.merge(local_data)
305
-
306
- # Create new context with merged props, preserving other context attributes
307
- Context.for_view(
308
- parent_context.req,
309
- parent_context.sess,
310
- parent_context.cust,
311
- parent_context.locale,
312
- config: parent_context.config,
313
- **merged_props,
314
- )
315
- end
316
-
317
324
  # Context wrapper for {{#each}} iterations
318
325
  class EachContext
319
326
  attr_reader :parent_context, :current_item, :current_index, :items_var
@@ -1,14 +1,18 @@
1
1
  # lib/rhales/view.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require 'securerandom'
6
+ require 'forwardable'
4
7
  require_relative 'context'
5
8
  require_relative 'rue_document'
6
9
  require_relative 'template_engine'
7
- require_relative 'hydrator'
10
+ require_relative '../hydration/hydrator'
8
11
  require_relative 'view_composition'
9
- require_relative 'hydration_data_aggregator'
10
- require_relative 'csp'
11
- require_relative 'refinements/require_refinements'
12
+ require_relative '../hydration/hydration_data_aggregator'
13
+ require_relative '../security/csp'
14
+ require_relative '../utils/json_serializer'
15
+ require_relative '../integrations/refinements/require_refinements'
12
16
 
13
17
  using Rhales::Ruequire
14
18
 
@@ -27,14 +31,15 @@ module Rhales
27
31
  #
28
32
  # ### Server Templates: Full Context Access
29
33
  # Templates have complete access to all server-side data:
30
- # - All props passed to View.new
31
- # - Data from .rue file's <data> section (processed server-side)
34
+ # - All client data passed to View.new
35
+ # - All server data passed to View.new
36
+
32
37
  # - Runtime data (CSRF tokens, nonces, request metadata)
33
38
  # - Computed data (authentication status, theme classes)
34
39
  # - User objects, configuration, internal APIs
35
40
  #
36
41
  # ### Client Data: Explicit Allowlist
37
- # Only data declared in <data> sections reahas_role?ches the browser:
42
+ # Only client data declared in <schema> sections reaches the browser:
38
43
  # - Creates a REST API-like boundary
39
44
  # - Server-side variable interpolation processes secrets safely
40
45
  # - JSON serialization validates data structure
@@ -44,53 +49,83 @@ module Rhales
44
49
  # # Server template has full access:
45
50
  # {{user.admin?}} {{csrf_token}} {{internal_config}}
46
51
  #
47
- # # Client only gets declared data:
48
- # { "user_name": "{{user.name}}", "theme": "{{user.theme}}" }
52
+ # # Client only gets declared data from schema:
53
+ # <schema lang="js-zod" window="data">
54
+ # const schema = z.object({ userName: z.string() });
55
+ # </schema>
49
56
  #
50
57
  # See docs/CONTEXT_AND_DATA_BOUNDARIES.md for complete details.
51
58
  #
52
59
  # Subclasses can override context_class to use different context implementations.
53
60
  class View
61
+ extend Forwardable
62
+ include Rhales::Utils::LoggingHelpers
63
+
54
64
  class RenderError < StandardError; end
55
65
  class TemplateNotFoundError < RenderError; end
56
66
 
57
- attr_reader :req, :sess, :cust, :locale, :rsfc_context, :props, :config
67
+ attr_reader :req, :rsfc_context
58
68
 
59
- def initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil)
60
- @req = req
61
- @sess = sess
62
- @cust = cust
63
- @locale = locale_override
64
- @props = props
65
- @config = config || Rhales.configuration
69
+ # Delegate context accessors to rsfc_context
70
+ def_delegators :@rsfc_context, :sess, :user, :client, :server, :config, :locale
66
71
 
67
- # Create context using the specified context class
68
- @rsfc_context = create_context
72
+ def initialize(req, client: {}, server: {}, config: nil)
73
+ @req = req
74
+ @rsfc_context = context_class.for_view(req, client: client, server: server, config: config || Rhales.configuration)
69
75
  end
70
76
 
71
77
  # Render RSFC template with hydration using two-pass architecture
72
78
  def render(template_name = nil)
79
+ start_time = now_in_μs
73
80
  template_name ||= self.class.default_template_name
74
81
 
75
- # Phase 1: Build view composition and aggregate data
76
- composition = build_view_composition(template_name)
77
- aggregator = HydrationDataAggregator.new(@rsfc_context)
78
- merged_hydration_data = aggregator.aggregate(composition)
82
+ # Store template name in request env for middleware validation
83
+ @req.env['rhales.template_name'] = template_name if @req && @req.respond_to?(:env)
79
84
 
80
- # Phase 2: Render HTML with pre-computed data
81
- # Render template content
82
- template_html = render_template_with_composition(composition, template_name)
85
+ begin
86
+ # Phase 1: Build view composition and aggregate data
87
+ composition = build_view_composition(template_name)
88
+ aggregator = HydrationDataAggregator.new(@rsfc_context)
89
+ merged_hydration_data = aggregator.aggregate(composition)
83
90
 
84
- # Generate hydration HTML with merged data
85
- hydration_html = generate_hydration_from_merged_data(merged_hydration_data)
91
+ # Phase 2: Render HTML with pre-computed data
92
+ # Render template content
93
+ template_html = render_template_with_composition(composition, template_name)
86
94
 
87
- # Set CSP header if enabled
88
- set_csp_header_if_enabled
95
+ # Generate hydration HTML with merged data
96
+ hydration_html = generate_hydration_from_merged_data(merged_hydration_data)
89
97
 
90
- # Smart hydration injection with mount point detection
91
- inject_hydration_with_mount_points(composition, template_name, template_html, hydration_html)
92
- rescue StandardError => ex
93
- raise RenderError, "Failed to render template '#{template_name}': #{ex.message}"
98
+ # Set CSP header if enabled
99
+ set_csp_header_if_enabled
100
+
101
+ # Smart hydration injection with mount point detection
102
+ result = inject_hydration_with_mount_points(composition, template_name, template_html, hydration_html)
103
+
104
+ # Log successful render
105
+ duration = now_in_μs - start_time
106
+ hydration_size = merged_hydration_data.to_json.bytesize if merged_hydration_data
107
+
108
+ log_with_metadata(Rhales.logger, :debug, 'View rendered',
109
+ template: template_name,
110
+ layout: composition.layout,
111
+ partials: composition.dependencies.values.flatten.uniq,
112
+ duration: duration,
113
+ hydration_size_bytes: hydration_size
114
+ )
115
+
116
+ result
117
+ rescue StandardError => ex
118
+ duration = now_in_μs - start_time
119
+
120
+ log_with_metadata(Rhales.logger, :error, 'View render failed',
121
+ template: template_name,
122
+ duration: duration,
123
+ error: ex.message,
124
+ error_class: ex.class.name
125
+ )
126
+
127
+ raise RenderError, "Failed to render template '#{template_name}': #{ex.message}"
128
+ end
94
129
  end
95
130
 
96
131
  # Render only the template section (without data hydration)
@@ -104,46 +139,46 @@ module Rhales
104
139
 
105
140
  # Render JSON response for API endpoints (link-based strategies)
106
141
  def render_json_only(template_name = nil, additional_context = {})
107
- require_relative 'hydration_endpoint'
142
+ require_relative '../hydration/hydration_endpoint'
108
143
 
109
144
  template_name ||= self.class.default_template_name
110
- endpoint = HydrationEndpoint.new(@config, @rsfc_context)
145
+ endpoint = HydrationEndpoint.new(config, @rsfc_context)
111
146
  endpoint.render_json(template_name, additional_context)
112
147
  end
113
148
 
114
149
  # Render ES module response for modulepreload strategy
115
150
  def render_module_only(template_name = nil, additional_context = {})
116
- require_relative 'hydration_endpoint'
151
+ require_relative '../hydration/hydration_endpoint'
117
152
 
118
153
  template_name ||= self.class.default_template_name
119
- endpoint = HydrationEndpoint.new(@config, @rsfc_context)
154
+ endpoint = HydrationEndpoint.new(config, @rsfc_context)
120
155
  endpoint.render_module(template_name, additional_context)
121
156
  end
122
157
 
123
158
  # Render JSONP response with callback
124
159
  def render_jsonp_only(template_name = nil, callback_name = 'callback', additional_context = {})
125
- require_relative 'hydration_endpoint'
160
+ require_relative '../hydration/hydration_endpoint'
126
161
 
127
162
  template_name ||= self.class.default_template_name
128
- endpoint = HydrationEndpoint.new(@config, @rsfc_context)
163
+ endpoint = HydrationEndpoint.new(config, @rsfc_context)
129
164
  endpoint.render_jsonp(template_name, callback_name, additional_context)
130
165
  end
131
166
 
132
167
  # Check if template data has changed for caching
133
168
  def data_changed?(template_name = nil, etag = nil, additional_context = {})
134
- require_relative 'hydration_endpoint'
169
+ require_relative '../hydration/hydration_endpoint'
135
170
 
136
171
  template_name ||= self.class.default_template_name
137
- endpoint = HydrationEndpoint.new(@config, @rsfc_context)
172
+ endpoint = HydrationEndpoint.new(config, @rsfc_context)
138
173
  endpoint.data_changed?(template_name, etag, additional_context)
139
174
  end
140
175
 
141
176
  # Calculate ETag for current template data
142
177
  def calculate_etag(template_name = nil, additional_context = {})
143
- require_relative 'hydration_endpoint'
178
+ require_relative '../hydration/hydration_endpoint'
144
179
 
145
180
  template_name ||= self.class.default_template_name
146
- endpoint = HydrationEndpoint.new(@config, @rsfc_context)
181
+ endpoint = HydrationEndpoint.new(config, @rsfc_context)
147
182
  endpoint.calculate_etag(template_name, additional_context)
148
183
  end
149
184
 
@@ -172,12 +207,6 @@ module Rhales
172
207
 
173
208
  protected
174
209
 
175
- # Create the appropriate context for this view
176
- # Subclasses can override this to use different context types
177
- def create_context
178
- context_class.for_view(@req, @sess, @cust, @locale, config: @config, **@props)
179
- end
180
-
181
210
  # Return the context class to use
182
211
  # Subclasses can override this to use different context implementations
183
212
  def context_class
@@ -201,8 +230,8 @@ module Rhales
201
230
  # Resolve template path
202
231
  def resolve_template_path(template_name)
203
232
  # Check configured template paths first
204
- if @config && @config.template_paths && !@config.template_paths.empty?
205
- @config.template_paths.each do |path|
233
+ if config && config.template_paths && !config.template_paths.empty?
234
+ config.template_paths.each do |path|
206
235
  template_path = File.join(path, "#{template_name}.rue")
207
236
  return template_path if File.exist?(template_path)
208
237
  end
@@ -218,8 +247,8 @@ module Rhales
218
247
  return templates_path if File.exist?(templates_path)
219
248
 
220
249
  # Return first configured path or web path for error message
221
- if @config && @config.template_paths && !@config.template_paths.empty?
222
- File.join(@config.template_paths.first, "#{template_name}.rue")
250
+ if config && config.template_paths && !config.template_paths.empty?
251
+ File.join(config.template_paths.first, "#{template_name}.rue")
223
252
  else
224
253
  web_path
225
254
  end
@@ -235,10 +264,9 @@ module Rhales
235
264
  #
236
265
  # RSFC Security Model: Templates have full server context access
237
266
  # - Templates can access all business data, user objects, methods, etc.
238
- # - Templates can access data from .rue file's <data> section (processed server-side)
239
267
  # - This is like any server-side template (ERB, HAML, etc.)
240
268
  # - Security boundary is at server-to-client handoff, not within server rendering
241
- # - Only data declared in <data> section reaches the client (after processing)
269
+ # - Only data declared in <schema> section reaches the client (after validation)
242
270
  def render_template_section(parser)
243
271
  template_content = parser.section('template')
244
272
  return '' unless template_content
@@ -246,11 +274,8 @@ module Rhales
246
274
  # Create partial resolver
247
275
  partial_resolver = create_partial_resolver
248
276
 
249
- # Merge .rue file data with existing context
250
- context_with_rue_data = create_context_with_rue_data(parser)
251
-
252
- # Render with full server context (props + computed context + rue data)
253
- TemplateEngine.render(template_content, context_with_rue_data, partial_resolver: partial_resolver)
277
+ # Render with full server context
278
+ TemplateEngine.render(template_content, @rsfc_context, partial_resolver: partial_resolver)
254
279
  end
255
280
 
256
281
  # Create partial resolver for {{> partial}} inclusions
@@ -268,44 +293,12 @@ module Rhales
268
293
  end
269
294
  end
270
295
 
271
- # Generate data hydration HTML
272
- def generate_hydration(parser)
273
- Hydrator.generate(parser, @rsfc_context)
274
- end
275
-
276
- # Create context that includes data from .rue file's data section
277
- def create_context_with_rue_data(parser)
278
- # Get data from .rue file's data section
279
- rue_data = extract_rue_data(parser)
280
-
281
- # Merge rue data with existing props (rue data takes precedence)
282
- merged_props = @props.merge(rue_data)
283
-
284
- # Create new context with merged data
285
- context_class.for_view(@req, @sess, @cust, @locale, config: @config, **merged_props)
286
- end
287
-
288
- # Extract and process data from .rue file's data section
289
- def extract_rue_data(parser)
290
- data_content = parser.section('data')
291
- return {} unless data_content
292
-
293
- # Process the data section as JSON and parse it
294
- hydrator = Hydrator.new(parser, @rsfc_context)
295
- hydrator.processed_data_hash
296
- rescue JSON::ParserError, Hydrator::JSONSerializationError => ex
297
- puts "Error processing data section: #{ex.message}"
298
- # If data section isn't valid JSON, return empty hash
299
- # This allows templates to work even with malformed data sections
300
- {}
301
- end
302
-
303
296
  # Smart hydration injection with mount point detection on rendered HTML
304
297
  def inject_hydration_with_mount_points(composition, template_name, template_html, hydration_html)
305
- injector = HydrationInjector.new(@config.hydration, template_name)
298
+ injector = HydrationInjector.new(config.hydration, template_name)
306
299
 
307
300
  # Check if using link-based strategy
308
- if @config.hydration.link_based_strategy?
301
+ if config.hydration.link_based_strategy?
309
302
  # For link-based strategies, we need the merged data context
310
303
  aggregator = HydrationDataAggregator.new(@rsfc_context)
311
304
  merged_data = aggregator.aggregate(composition)
@@ -319,22 +312,11 @@ module Rhales
319
312
  end
320
313
  end
321
314
 
322
- # Legacy injection method (kept for backwards compatibility)
323
- def inject_hydration_into_template(template_html, hydration_html)
324
- # Try to inject before closing </body> tag
325
- if template_html.include?('</body>')
326
- template_html.sub('</body>', "#{hydration_html}\n</body>")
327
- # Otherwise append to end
328
- else
329
- "#{template_html}\n#{hydration_html}"
330
- end
331
- end
332
-
333
315
  # Detect mount points in fully rendered HTML
334
316
  def detect_mount_point_in_rendered_html(template_html)
335
- return nil unless @config&.hydration
317
+ return nil unless config&.hydration
336
318
 
337
- custom_selectors = @config.hydration.mount_point_selectors || []
319
+ custom_selectors = config.hydration.mount_point_selectors || []
338
320
  detector = MountPointDetector.new
339
321
  detector.detect(template_html, custom_selectors)
340
322
  end
@@ -342,7 +324,7 @@ module Rhales
342
324
  # Build view composition for the given template
343
325
  def build_view_composition(template_name)
344
326
  loader = method(:load_template_for_composition)
345
- composition = ViewComposition.new(template_name, loader: loader, config: @config)
327
+ composition = ViewComposition.new(template_name, loader: loader, config: config)
346
328
  composition.resolve!
347
329
  end
348
330
 
@@ -365,8 +347,8 @@ module Rhales
365
347
  # Create partial resolver that uses the composition
366
348
  partial_resolver = create_partial_resolver_from_composition(composition)
367
349
 
368
- # Merge .rue file data with existing context
369
- context_with_rue_data = create_context_with_rue_data(root_parser)
350
+ # Use existing context for rendering
351
+ context_with_rue_data = @rsfc_context
370
352
 
371
353
  # Check if template has a layout
372
354
  if root_parser.layout && composition.template(root_parser.layout)
@@ -378,16 +360,8 @@ module Rhales
378
360
  layout_content = layout_parser.section('template')
379
361
  return '' unless layout_content
380
362
 
381
- # Create new context with content for layout rendering
382
- layout_props = context_with_rue_data.props.merge('content' => content_html)
383
- layout_context = Context.new(
384
- context_with_rue_data.req,
385
- context_with_rue_data.sess,
386
- context_with_rue_data.cust,
387
- context_with_rue_data.locale,
388
- props: layout_props,
389
- config: context_with_rue_data.config,
390
- )
363
+ # Use builder pattern to create new context with content for layout rendering
364
+ layout_context = context_with_rue_data.merge_client('content' => content_html)
391
365
 
392
366
  TemplateEngine.render(layout_content, layout_context, partial_resolver: partial_resolver)
393
367
  else
@@ -411,16 +385,16 @@ module Rhales
411
385
  merged_data.each do |window_attr, data|
412
386
  # Generate unique ID for this data block
413
387
  unique_id = "rsfc-data-#{SecureRandom.hex(8)}"
388
+ nonce_attr = nonce_attribute
414
389
 
415
390
  # Create JSON script tag with optional reflection attributes
416
- json_attrs = reflection_enabled? ? " data-window=\"#{window_attr}\"" : ""
391
+ json_attrs = reflection_enabled? ? " data-window=\"#{window_attr}\"" : ''
417
392
  json_script = <<~HTML.strip
418
- <script id="#{unique_id}" type="application/json"#{json_attrs}>#{JSON.generate(data)}</script>
393
+ <script#{nonce_attr} id="#{unique_id}" type="application/json"#{json_attrs}>#{JSONSerializer.dump(data)}</script>
419
394
  HTML
420
395
 
421
396
  # Create hydration script with optional reflection attributes
422
- nonce_attr = nonce_attribute
423
- hydration_attrs = reflection_enabled? ? " data-hydration-target=\"#{window_attr}\"" : ""
397
+ hydration_attrs = reflection_enabled? ? " data-hydration-target=\"#{window_attr}\"" : ''
424
398
  hydration_script = if reflection_enabled?
425
399
  <<~HTML.strip
426
400
  <script#{nonce_attr}#{hydration_attrs}>
@@ -446,12 +420,15 @@ module Rhales
446
420
  hydration_parts << generate_reflection_utilities
447
421
  end
448
422
 
449
- hydration_parts.join("\n")
423
+ return '' if hydration_parts.empty?
424
+
425
+ hydration_content = hydration_parts.join("\n")
426
+ "\n\n<!-- Rhales Hydration Start -->\n#{hydration_content}\n<!-- Rhales Hydration End -->"
450
427
  end
451
428
 
452
429
  # Check if reflection system is enabled
453
430
  def reflection_enabled?
454
- @config.hydration.reflection_enabled
431
+ config.hydration.reflection_enabled
455
432
  end
456
433
 
457
434
  # Generate JavaScript utilities for hydration reflection
@@ -512,14 +489,14 @@ module Rhales
512
489
 
513
490
  # Set CSP header if enabled
514
491
  def set_csp_header_if_enabled
515
- return unless @config.csp_enabled
492
+ return unless config.csp_enabled
516
493
  return unless @req && @req.respond_to?(:env)
517
494
 
518
495
  # Get nonce from context
519
496
  nonce = @rsfc_context.get('nonce')
520
497
 
521
498
  # Create CSP instance and build header
522
- csp = CSP.new(@config, nonce: nonce)
499
+ csp = CSP.new(config, nonce: nonce)
523
500
  header_value = csp.build_header
524
501
 
525
502
  # Set header in request environment for framework to use
@@ -537,15 +514,15 @@ module Rhales
537
514
  .sub(/_view$/, '')
538
515
  end
539
516
 
540
- # Render template with props
541
- def render_with_data(req, sess, cust, locale, template_name: nil, config: nil, **props)
542
- view = new(req, sess, cust, locale, props: props, config: config)
517
+ # Render template with client data
518
+ def render_with_data(req, template_name: nil, config: nil, **client_data)
519
+ view = new(req, client: client_data, config: config)
543
520
  view.render(template_name)
544
521
  end
545
522
 
546
- # Create view instance with props
547
- def with_data(req, sess, cust, locale, config: nil, **props)
548
- new(req, sess, cust, locale, props: props, config: config)
523
+ # Create view instance with client data
524
+ def with_data(req, config: nil, **client_data)
525
+ new(req, client: client_data, config: config)
549
526
  end
550
527
  end
551
528
  end