rhales 0.4.0 → 0.5.4

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} +69 -7
  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} +49 -2
  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,7 +1,9 @@
1
1
  # lib/rhales/view_composition.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'rue_document'
4
- require_relative 'refinements/require_refinements'
6
+ require_relative '../integrations/refinements/require_refinements'
5
7
 
6
8
  using Rhales::Ruequire
7
9
 
@@ -23,6 +25,8 @@ module Rhales
23
25
  # - Immutable: Once created, the composition is read-only
24
26
  # - Cacheable: Can be cached in production for performance
25
27
  class ViewComposition
28
+ include Rhales::Utils::LoggingHelpers
29
+
26
30
  class TemplateNotFoundError < StandardError; end
27
31
  class CircularDependencyError < StandardError; end
28
32
 
@@ -39,9 +43,23 @@ module Rhales
39
43
 
40
44
  # Resolve all template dependencies
41
45
  def resolve!
42
- load_template_recursive(@root_template_name)
43
- freeze_composition
44
- self
46
+ log_timed_operation(Rhales.logger, :debug, 'Template dependency resolution',
47
+ root_template: @root_template_name
48
+ ) do
49
+ load_template_recursive(@root_template_name)
50
+ freeze_composition
51
+
52
+ # Log resolution results
53
+ log_with_metadata(Rhales.logger, :debug, 'Template composition resolved',
54
+ root_template: @root_template_name,
55
+ total_templates: @templates.size,
56
+ total_dependencies: @dependencies.values.sum(&:size),
57
+ partials: @dependencies.values.flatten.uniq,
58
+ layout: layout
59
+ )
60
+
61
+ self
62
+ end
45
63
  end
46
64
 
47
65
  # Iterate through all documents in render order
@@ -84,25 +102,50 @@ module Rhales
84
102
  @dependencies[template_name] || []
85
103
  end
86
104
 
105
+ # Get the layout for the root template (if any)
106
+ def layout
107
+ root_template = @templates[@root_template_name]
108
+ return nil unless root_template
109
+
110
+ root_template.layout
111
+ end
87
112
 
88
113
  private
89
114
 
90
- def load_template_recursive(template_name, _parent_path = nil)
115
+ def load_template_recursive(template_name, parent_path = nil)
116
+ depth = @loading.size
117
+
91
118
  # Check for circular dependencies
92
119
  if @loading.include?(template_name)
120
+ log_with_metadata(Rhales.logger, :error, 'Circular dependency detected',
121
+ template: template_name,
122
+ dependency_chain: @loading.to_a,
123
+ depth: depth
124
+ )
93
125
  raise CircularDependencyError, "Circular dependency detected: #{template_name} -> #{@loading.to_a.join(' -> ')}"
94
126
  end
95
127
 
96
- # Skip if already loaded
97
- return if @templates.key?(template_name)
128
+ # Skip if already loaded (cache hit)
129
+ if @templates.key?(template_name)
130
+ Rhales.logger.debug("Template cache hit: template=#{template_name}")
131
+ return
132
+ end
98
133
 
99
134
  @loading.add(template_name)
100
135
 
101
136
  begin
102
137
  # Load template using the provided loader
138
+ start_time = now_in_μs
103
139
  parser = @loader.call(template_name)
140
+ load_duration = now_in_μs - start_time
104
141
 
105
142
  unless parser
143
+ log_with_metadata(Rhales.logger, :error, 'Template not found',
144
+ template: template_name,
145
+ parent: parent_path,
146
+ depth: depth,
147
+ search_duration: load_duration
148
+ )
106
149
  raise TemplateNotFoundError, "Template not found: #{template_name}"
107
150
  end
108
151
 
@@ -111,15 +154,42 @@ module Rhales
111
154
  @dependencies[template_name] = []
112
155
 
113
156
  # Extract and load partials
114
- extract_partials(parser).each do |partial_name|
157
+ partials = extract_partials(parser)
158
+
159
+ if partials.any?
160
+ log_with_metadata(Rhales.logger, :debug, 'Resolved partial',
161
+ template: template_name,
162
+ partials_found: partials,
163
+ partial_count: partials.size,
164
+ depth: depth,
165
+ load_duration: load_duration
166
+ )
167
+ end
168
+
169
+ partials.each do |partial_name|
115
170
  @dependencies[template_name] << partial_name
116
171
  load_template_recursive(partial_name, template_name)
117
172
  end
118
173
 
119
174
  # Load layout if specified and not already loaded
120
175
  if parser.layout && !@templates.key?(parser.layout)
176
+ log_with_metadata(Rhales.logger, :debug, 'Layout resolution',
177
+ template: template_name,
178
+ layout: parser.layout,
179
+ depth: depth
180
+ )
121
181
  load_template_recursive(parser.layout, template_name)
122
182
  end
183
+
184
+ # Log successful template load
185
+ log_with_metadata(Rhales.logger, :debug, 'Template loaded',
186
+ template: template_name,
187
+ parent: parent_path,
188
+ depth: depth,
189
+ has_partials: partials.any?,
190
+ has_layout: !parser.layout.nil?,
191
+ load_duration: load_duration
192
+ )
123
193
  ensure
124
194
  @loading.delete(template_name)
125
195
  end
@@ -0,0 +1,9 @@
1
+ # lib/rhales/core.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'core/context'
6
+ require_relative 'core/rue_document'
7
+ require_relative 'core/template_engine'
8
+ require_relative 'core/view_composition'
9
+ require_relative 'core/view'
@@ -1,4 +1,6 @@
1
1
  # lib/rhales/errors/hydration_collision_error.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Rhales
4
6
  class HydrationCollisionError < Error
data/lib/rhales/errors.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # lib/rhales/errors.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Rhales
4
6
  class Error < StandardError; end
@@ -1,3 +1,7 @@
1
+ # lib/rhales/hydration/earliest_injection_detector.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  require 'strscan'
2
6
  require_relative 'safe_injection_validator'
3
7
 
@@ -22,6 +26,8 @@ module Rhales
22
26
  def detect(template_html)
23
27
  scanner = StringScanner.new(template_html)
24
28
  validator = SafeInjectionValidator.new(template_html)
29
+ # Build byte-to-char map once for the entire template
30
+ @byte_to_char_map = build_byte_to_char_map(template_html)
25
31
 
26
32
  # Try head section injection points first
27
33
  head_injection_point = detect_head_injection_point(scanner, validator, template_html)
@@ -64,7 +70,9 @@ module Rhales
64
70
 
65
71
  # Find opening <body> tag
66
72
  if scanner.scan_until(/<body\b[^>]*>/i)
67
- body_start = scanner.pos - scanner.matched.length
73
+ # Convert byte position to character position using pre-built map
74
+ byte_body_start = scanner.pos - scanner.matched.length
75
+ body_start = @byte_to_char_map[byte_body_start]
68
76
  safe_position = find_safe_injection_position(validator, body_start)
69
77
  return safe_position if safe_position
70
78
  end
@@ -77,11 +85,15 @@ module Rhales
77
85
 
78
86
  # Find opening <head> tag
79
87
  return nil unless scanner.scan_until(/<head\b[^>]*>/i)
80
- head_start = scanner.pos
88
+ # Convert byte position to character position using pre-built map
89
+ byte_head_start = scanner.pos
90
+ head_start = @byte_to_char_map[byte_head_start]
81
91
 
82
92
  # Find closing </head> tag
83
93
  return nil unless scanner.scan_until(/<\/head>/i)
84
- head_end = scanner.pos - scanner.matched.length
94
+ # Convert byte position to character position using pre-built map
95
+ byte_head_end = scanner.pos - scanner.matched.length
96
+ head_end = @byte_to_char_map[byte_head_end]
85
97
 
86
98
  [head_start, head_end]
87
99
  end
@@ -90,9 +102,13 @@ module Rhales
90
102
  head_content = template_html[head_start...head_end]
91
103
  scanner = StringScanner.new(head_content)
92
104
  last_link_end = nil
105
+ byte_to_char_map = build_byte_to_char_map(head_content)
93
106
 
94
107
  while scanner.scan_until(/<link\b[^>]*\/?>/i)
95
- last_link_end = scanner.pos
108
+ # scanner.pos is byte position within head_content
109
+ byte_pos = scanner.pos
110
+ # Convert to character position using pre-built map
111
+ last_link_end = byte_to_char_map[byte_pos]
96
112
  end
97
113
 
98
114
  last_link_end ? head_start + last_link_end : nil
@@ -102,9 +118,13 @@ module Rhales
102
118
  head_content = template_html[head_start...head_end]
103
119
  scanner = StringScanner.new(head_content)
104
120
  last_meta_end = nil
121
+ byte_to_char_map = build_byte_to_char_map(head_content)
105
122
 
106
123
  while scanner.scan_until(/<meta\b[^>]*\/?>/i)
107
- last_meta_end = scanner.pos
124
+ # scanner.pos is byte position within head_content
125
+ byte_pos = scanner.pos
126
+ # Convert to character position using pre-built map
127
+ last_meta_end = byte_to_char_map[byte_pos]
108
128
  end
109
129
 
110
130
  last_meta_end ? head_start + last_meta_end : nil
@@ -113,14 +133,18 @@ module Rhales
113
133
  def find_after_first_script(template_html, head_start, head_end)
114
134
  head_content = template_html[head_start...head_end]
115
135
  scanner = StringScanner.new(head_content)
136
+ byte_to_char_map = build_byte_to_char_map(head_content)
116
137
 
117
138
  # Find first script opening tag
118
139
  if scanner.scan_until(/<script\b[^>]*>/i)
119
- script_start = scanner.pos - scanner.matched.length
140
+ # Only the script end position is needed for this method, not the start position
120
141
 
121
142
  # Find corresponding closing tag
122
143
  if scanner.scan_until(/<\/script>/i)
123
- first_script_end = scanner.pos
144
+ # scanner.pos is byte position within head_content
145
+ byte_script_end = scanner.pos
146
+ # Convert to character position using pre-built map
147
+ first_script_end = byte_to_char_map[byte_script_end]
124
148
  return head_start + first_script_end
125
149
  end
126
150
  end
@@ -145,5 +169,43 @@ module Rhales
145
169
  # No safe position found
146
170
  nil
147
171
  end
172
+
173
+ # Builds a mapping from byte positions to character positions for efficient
174
+ # conversion when processing UTF-8 strings with StringScanner.
175
+ #
176
+ # This method creates a hash where keys are byte positions and values are
177
+ # the corresponding character positions. For multibyte UTF-8 characters,
178
+ # only the starting byte position has an entry in the map.
179
+ #
180
+ # @param str [String] The UTF-8 encoded string to map
181
+ # @return [Hash<Integer, Integer>] A hash mapping byte positions to character positions
182
+ #
183
+ # @example ASCII string
184
+ # build_byte_to_char_map("Hello")
185
+ # # => {0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5}
186
+ #
187
+ # @example UTF-8 with multibyte characters
188
+ # build_byte_to_char_map("café") # é is 2 bytes
189
+ # # => {0=>0, 1=>1, 2=>2, 3=>3, 5=>4} # Note: byte 4 is continuation byte
190
+ #
191
+ def build_byte_to_char_map(str)
192
+ map = {}
193
+ char_pos = 0
194
+ byte_pos = 0
195
+
196
+ # Iterate through each character (not byte) in the string
197
+ str.each_char do |char|
198
+ # Map the starting byte position of this character
199
+ map[byte_pos] = char_pos
200
+
201
+ # Advance byte position by the byte size of this character
202
+ byte_pos += char.bytesize
203
+ char_pos += 1
204
+ end
205
+
206
+ # Add final mapping for the end of the string
207
+ map[byte_pos] = char_pos
208
+ map
209
+ end
148
210
  end
149
211
  end