rhales 0.3.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 (116) 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 -2
  20. data/Gemfile +29 -0
  21. data/Gemfile.lock +189 -0
  22. data/README.md +706 -589
  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 +161 -1
  64. data/lib/rhales/core/context.rb +354 -0
  65. data/lib/rhales/{rue_document.rb → core/rue_document.rb} +59 -43
  66. data/lib/rhales/{template_engine.rb → core/template_engine.rb} +80 -33
  67. data/lib/rhales/core/view.rb +529 -0
  68. data/lib/rhales/{view_composition.rb → core/view_composition.rb} +81 -9
  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/hydration/earliest_injection_detector.rb +153 -0
  73. data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
  74. data/lib/rhales/hydration/hydration_endpoint.rb +215 -0
  75. data/lib/rhales/hydration/hydration_injector.rb +175 -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/hydration/link_based_injection_detector.rb +195 -0
  79. data/lib/rhales/hydration/mount_point_detector.rb +109 -0
  80. data/lib/rhales/hydration/safe_injection_validator.rb +103 -0
  81. data/lib/rhales/hydration.rb +13 -0
  82. data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +7 -13
  83. data/lib/rhales/{tilt.rb → integrations/tilt.rb} +26 -18
  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 +55 -36
  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 +5 -1
  98. data/lib/rhales.rb +47 -19
  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 +142 -18
  111. data/CLAUDE.locale.txt +0 -7
  112. data/lib/rhales/context.rb +0 -240
  113. data/lib/rhales/hydration_data_aggregator.rb +0 -220
  114. data/lib/rhales/hydrator.rb +0 -141
  115. data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
  116. data/lib/rhales/view.rb +0 -412
@@ -1,9 +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'
9
+ require_relative '../hydration/hydrator'
7
10
 
8
11
  module Rhales
9
12
  # Rhales - Ruby Handlebars-style template engine
@@ -27,6 +30,8 @@ module Rhales
27
30
  # - {{#each items}} ... {{/each}} - Iteration with context
28
31
  # - {{> partial_name}} - Partial inclusion
29
32
  class TemplateEngine
33
+ include Rhales::Utils::LoggingHelpers
34
+
30
35
  class RenderError < ::Rhales::RenderError; end
31
36
  class PartialNotFoundError < RenderError; end
32
37
  class UndefinedVariableError < RenderError; end
@@ -42,31 +47,46 @@ module Rhales
42
47
  end
43
48
 
44
49
  def render
45
- # Check if this is a simple template or a full .rue file
46
- if simple_template?
47
- # Use HandlebarsParser for simple templates
48
- parser = HandlebarsParser.new(@template_content)
49
- parser.parse!
50
- render_content_nodes(parser.ast.children)
51
- else
52
- # Use RueDocument for .rue files
53
- @parser = RueDocument.new(@template_content)
54
- @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!
55
65
 
56
- # Get template section via RueDocument
57
- template_content = @parser.section('template')
58
- 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
59
69
 
60
- # Render the template section as a simple template
61
- render_template_string(template_content)
70
+ # Render the template section as a simple template
71
+ render_template_string(template_content)
72
+ end
62
73
  end
63
74
  rescue ::Rhales::ParseError => ex
64
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
+ )
65
79
  raise RenderError, "Template parsing failed: #{ex.message}"
66
80
  rescue ::Rhales::ValidationError => ex
67
81
  # Validation errors from RueDocument
82
+ log_with_metadata(Rhales.logger, :error, 'Template validation error',
83
+ error: ex.message, template_type: :rue
84
+ )
68
85
  raise RenderError, "Template validation failed: #{ex.message}"
69
86
  rescue StandardError => ex
87
+ log_with_metadata(Rhales.logger, :error, 'Template render error',
88
+ error: ex.message, error_class: ex.class.name
89
+ )
70
90
  raise RenderError, "Template rendering failed: #{ex.message}"
71
91
  end
72
92
 
@@ -80,11 +100,6 @@ module Rhales
80
100
  @parser&.schema_path
81
101
  end
82
102
 
83
- # Access all data attributes from parsed .rue file
84
- def data_attributes
85
- @parser&.data_attributes || {}
86
- end
87
-
88
103
  # Get template variables used in the template
89
104
  def template_variables
90
105
  @parser&.template_variables || []
@@ -98,7 +113,7 @@ module Rhales
98
113
  private
99
114
 
100
115
  def simple_template?
101
- !@template_content.match?(/^<(data|template|logic)\b/)
116
+ !@template_content.match?(/^<(schema|template|logic)\b/)
102
117
  end
103
118
 
104
119
  def render_template_string(template_string)
@@ -130,7 +145,7 @@ module Rhales
130
145
  when :each_block
131
146
  result += render_each_block(node)
132
147
  when :handlebars_expression
133
- # Handle old format for data sections
148
+ # Handle handlebars expressions
134
149
  result += render_handlebars_expression(node)
135
150
  end
136
151
  end
@@ -143,7 +158,13 @@ module Rhales
143
158
  raw = node.value[:raw]
144
159
 
145
160
  value = get_variable_value(name)
146
- 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
147
168
  end
148
169
 
149
170
  def render_partial_expression(node)
@@ -204,7 +225,12 @@ module Rhales
204
225
  ''
205
226
  else # Variables
206
227
  value = get_variable_value(content)
207
- 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
208
234
  end
209
235
  end
210
236
 
@@ -214,9 +240,24 @@ module Rhales
214
240
  partial_content = @partial_resolver.call(partial_name)
215
241
  raise PartialNotFoundError, "Partial '#{partial_name}' not found" unless partial_content
216
242
 
217
- # Recursively render the partial content
218
- engine = self.class.new(partial_content, @context, partial_resolver: @partial_resolver)
219
- engine.render
243
+ # Check if this is a .rue document with sections
244
+ if partial_content.match?(/^<(schema|template|logic)\b/)
245
+ # Parse as RueDocument to handle schema sections properly
246
+ partial_doc = RueDocument.new(partial_content)
247
+ partial_doc.parse!
248
+
249
+ # Extract template section
250
+ template_content = partial_doc.section('template')
251
+ raise PartialNotFoundError, "Partial '#{partial_name}' missing template section" unless template_content
252
+
253
+ # Render template with current context
254
+ engine = self.class.new(template_content, @context, partial_resolver: @partial_resolver)
255
+ engine.render
256
+ else
257
+ # Simple template without sections - render as before
258
+ engine = self.class.new(partial_content, @context, partial_resolver: @partial_resolver)
259
+ engine.render
260
+ end
220
261
  end
221
262
 
222
263
  # Get variable value from context
@@ -234,11 +275,19 @@ module Rhales
234
275
  @context.get(variable_name)
235
276
  elsif @context.respond_to?(:[])
236
277
  @context[variable_name] || @context[variable_name.to_sym]
237
- else
238
- nil
239
278
  end
240
279
  end
241
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
+
242
291
  # Evaluate condition for if/unless blocks
243
292
  def evaluate_condition(condition)
244
293
  value = get_variable_value(condition)
@@ -343,8 +392,6 @@ module Rhales
343
392
  # Load and parse the partial .rue file
344
393
  document = RueDocument.parse_file(partial_path)
345
394
  document.section('template')
346
- else
347
- nil
348
395
  end
349
396
  end
350
397
  end