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
metadata CHANGED
@@ -1,14 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhales
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - delano
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json_schemer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: logger
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: tilt
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2'
12
54
  description: |
13
55
  Rhales is a framework for building server-rendered components with
14
56
  client-side data hydration using .rue files called RSFCs (Ruby
@@ -23,39 +65,115 @@ executables: []
23
65
  extensions: []
24
66
  extra_rdoc_files: []
25
67
  files:
26
- - CLAUDE.locale.txt
68
+ - ".github/renovate.json5"
69
+ - ".github/workflows/ci.yml"
70
+ - ".github/workflows/claude-code-review.yml"
71
+ - ".github/workflows/claude.yml"
72
+ - ".github/workflows/code-smells.yml"
73
+ - ".github/workflows/ruby-lint.yml"
74
+ - ".github/workflows/yardoc.yml"
75
+ - ".gitignore"
76
+ - ".pr_agent.toml"
77
+ - ".pre-commit-config.yaml"
78
+ - ".prettierignore"
79
+ - ".prettierrc"
80
+ - ".reek.yml"
81
+ - ".rubocop.yml"
82
+ - ".serena/.gitignore"
83
+ - ".yardopts"
84
+ - CHANGELOG.md
27
85
  - CLAUDE.md
86
+ - Gemfile
87
+ - Gemfile.lock
28
88
  - LICENSE.txt
29
89
  - README.md
90
+ - Rakefile
91
+ - debug_context.rb
92
+ - demo/rhales-roda-demo/.gitignore
93
+ - demo/rhales-roda-demo/Gemfile
94
+ - demo/rhales-roda-demo/Gemfile.lock
95
+ - demo/rhales-roda-demo/MAIL.md
96
+ - demo/rhales-roda-demo/README.md
97
+ - demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md
98
+ - demo/rhales-roda-demo/Rakefile
99
+ - demo/rhales-roda-demo/app.rb
100
+ - demo/rhales-roda-demo/bin/rackup
101
+ - demo/rhales-roda-demo/config.ru
102
+ - demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb
103
+ - demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb
104
+ - demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb
105
+ - demo/rhales-roda-demo/templates/change_login.rue
106
+ - demo/rhales-roda-demo/templates/change_password.rue
107
+ - demo/rhales-roda-demo/templates/close_account.rue
108
+ - demo/rhales-roda-demo/templates/create_account.rue
109
+ - demo/rhales-roda-demo/templates/dashboard.rue
110
+ - demo/rhales-roda-demo/templates/home.rue
111
+ - demo/rhales-roda-demo/templates/layouts/main.rue
112
+ - demo/rhales-roda-demo/templates/login.rue
113
+ - demo/rhales-roda-demo/templates/logout.rue
114
+ - demo/rhales-roda-demo/templates/reset_password.rue
115
+ - demo/rhales-roda-demo/templates/verify_account.rue
116
+ - demo/rhales-roda-demo/test_full_output.rb
117
+ - demo/rhales-roda-demo/test_simple.rb
118
+ - docs/.gitignore
119
+ - docs/architecture/data-flow.md
120
+ - examples/dashboard-with-charts.rue
121
+ - examples/form-with-validation.rue
122
+ - examples/simple-page.rue
123
+ - examples/vue.rue
124
+ - generate-json-schemas.ts
125
+ - json_schemer_migration_summary.md
30
126
  - lib/rhales.rb
127
+ - lib/rhales/adapters.rb
31
128
  - lib/rhales/adapters/base_auth.rb
32
129
  - lib/rhales/adapters/base_request.rb
33
130
  - lib/rhales/adapters/base_session.rb
34
131
  - lib/rhales/configuration.rb
35
- - lib/rhales/context.rb
36
- - lib/rhales/csp.rb
37
- - lib/rhales/earliest_injection_detector.rb
132
+ - lib/rhales/core.rb
133
+ - lib/rhales/core/context.rb
134
+ - lib/rhales/core/rue_document.rb
135
+ - lib/rhales/core/template_engine.rb
136
+ - lib/rhales/core/view.rb
137
+ - lib/rhales/core/view_composition.rb
38
138
  - lib/rhales/errors.rb
39
139
  - lib/rhales/errors/hydration_collision_error.rb
40
- - lib/rhales/hydration_data_aggregator.rb
41
- - lib/rhales/hydration_endpoint.rb
42
- - lib/rhales/hydration_injector.rb
43
- - lib/rhales/hydration_registry.rb
44
- - lib/rhales/hydrator.rb
45
- - lib/rhales/link_based_injection_detector.rb
46
- - lib/rhales/mount_point_detector.rb
47
- - lib/rhales/parsers/handlebars-grammar-review.txt
140
+ - lib/rhales/hydration.rb
141
+ - lib/rhales/hydration/earliest_injection_detector.rb
142
+ - lib/rhales/hydration/hydration_data_aggregator.rb
143
+ - lib/rhales/hydration/hydration_endpoint.rb
144
+ - lib/rhales/hydration/hydration_injector.rb
145
+ - lib/rhales/hydration/hydration_registry.rb
146
+ - lib/rhales/hydration/hydrator.rb
147
+ - lib/rhales/hydration/link_based_injection_detector.rb
148
+ - lib/rhales/hydration/mount_point_detector.rb
149
+ - lib/rhales/hydration/safe_injection_validator.rb
150
+ - lib/rhales/integrations.rb
151
+ - lib/rhales/integrations/refinements/require_refinements.rb
152
+ - lib/rhales/integrations/tilt.rb
153
+ - lib/rhales/middleware.rb
154
+ - lib/rhales/middleware/json_responder.rb
155
+ - lib/rhales/middleware/schema_validator.rb
156
+ - lib/rhales/parsers.rb
48
157
  - lib/rhales/parsers/handlebars_parser.rb
49
158
  - lib/rhales/parsers/rue_format_parser.rb
50
- - lib/rhales/refinements/require_refinements.rb
51
- - lib/rhales/rue_document.rb
52
- - lib/rhales/safe_injection_validator.rb
53
- - lib/rhales/template_engine.rb
54
- - lib/rhales/tilt.rb
159
+ - lib/rhales/security/csp.rb
160
+ - lib/rhales/utils.rb
161
+ - lib/rhales/utils/json_serializer.rb
162
+ - lib/rhales/utils/logging_helpers.rb
163
+ - lib/rhales/utils/schema_extractor.rb
164
+ - lib/rhales/utils/schema_generator.rb
55
165
  - lib/rhales/version.rb
56
- - lib/rhales/view.rb
57
- - lib/rhales/view_composition.rb
166
+ - lib/tasks/rhales_schema.rake
167
+ - package.json
168
+ - pnpm-lock.yaml
169
+ - pnpm-workspace.yaml
170
+ - proofs/error_handling.rb
171
+ - proofs/expanded_object_inheritance.rb
172
+ - proofs/partial_context_scoping_fix.rb
173
+ - proofs/ui_context_partial_inheritance.rb
58
174
  - rhales.gemspec
175
+ - schema_vs_data_comparison.md
176
+ - test_direct_access.rb
59
177
  homepage: https://github.com/onetimesecret/rhales
60
178
  licenses:
61
179
  - MIT
@@ -71,14 +189,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
189
  requirements:
72
190
  - - ">="
73
191
  - !ruby/object:Gem::Version
74
- version: 3.4.0
192
+ version: 3.3.4
75
193
  required_rubygems_version: !ruby/object:Gem::Requirement
76
194
  requirements:
77
195
  - - ">="
78
196
  - !ruby/object:Gem::Version
79
197
  version: '0'
80
198
  requirements: []
81
- rubygems_version: 3.6.9
199
+ rubygems_version: 3.7.2
82
200
  specification_version: 4
83
201
  summary: Rhales - Server-rendered components with client-side hydration (RSFCs)
84
202
  test_files: []
data/CLAUDE.locale.txt DELETED
@@ -1,7 +0,0 @@
1
- Our own private local claude notes. Not read by claude or checked in.
2
-
3
- https://claudelog.com/mechanics/plan-mode
4
- https://handlebars-lang.github.io/spec/#sec-handlebars-specification-statements
5
- https://github.com/jeremyevans/rodauth/tree/588b865cf70c7f327f0f24a4e277dfe787a6674f/doc
6
- https://docs.anthropic.com/en/docs/claude-code/hooks#subagentstop
7
- https://www.anthropic.com/engineering/claude-code-best-practices
@@ -1,239 +0,0 @@
1
- # lib/rhales/context.rb
2
-
3
- require_relative 'configuration'
4
- require_relative 'adapters/base_auth'
5
- require_relative 'adapters/base_session'
6
- require_relative 'adapters/base_request'
7
- require_relative 'csp'
8
-
9
- module Rhales
10
- # RSFCContext provides a clean interface for RSFC templates to access
11
- # server-side data. Follows the established pattern from InitScriptContext
12
- # and EnvironmentContext for focused, single-responsibility context objects.
13
- #
14
- # The context provides two layers of data:
15
- # 1. App: Framework-provided data (CSRF tokens, authentication, config)
16
- # 2. Props: Application data passed to the view (user, content, features)
17
- #
18
- # App data is accessible as both direct variables and through the app.* namespace.
19
- # Props take precedence over app data for variable resolution.
20
- #
21
- # One RSFCContext instance is created per page render and shared across
22
- # the main template and all partials to maintain security boundaries.
23
- class Context
24
- attr_reader :req, :sess, :cust, :locale, :props, :config, :app_data
25
-
26
- def initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil)
27
- @req = req
28
- @sess = sess || default_session
29
- @cust = cust || default_customer
30
- @config = config || Rhales.configuration
31
- @locale = locale_override || @config.default_locale
32
-
33
- # Normalize props keys to strings for consistent access
34
- @props = normalize_keys(props).freeze
35
-
36
- # Build context layers (two-layer model: app + props)
37
- @app_data = build_app_data.freeze
38
-
39
- # Pre-compute all_data before freezing
40
- # Props take precedence over app data, and add app namespace
41
- @all_data = @app_data.merge(@props).merge({ 'app' => @app_data }).freeze
42
-
43
- # Make context immutable after creation
44
- freeze
45
- end
46
-
47
- # Get variable value with dot notation support (e.g., "user.id", "features.account_creation")
48
- def get(variable_path)
49
- path_parts = variable_path.split('.')
50
- current_value = all_data
51
-
52
- path_parts.each do |part|
53
- case current_value
54
- when Hash
55
- if current_value.key?(part)
56
- current_value = current_value[part]
57
- elsif current_value.key?(part.to_sym)
58
- current_value = current_value[part.to_sym]
59
- else
60
- return nil
61
- end
62
- when Object
63
- if current_value.respond_to?(part)
64
- current_value = current_value.public_send(part)
65
- elsif current_value.respond_to?("#{part}?")
66
- current_value = current_value.public_send("#{part}?")
67
- else
68
- return nil
69
- end
70
- else
71
- return nil
72
- end
73
-
74
- return nil if current_value.nil?
75
- end
76
-
77
- current_value
78
- end
79
-
80
- # Get all available data (runtime + business + computed)
81
- attr_reader :all_data
82
-
83
- # Check if variable exists
84
- def variable?(variable_path)
85
- !get(variable_path).nil?
86
- end
87
-
88
- # Get list of all available variable paths (for validation)
89
- def available_variables
90
- collect_variable_paths(all_data)
91
- end
92
-
93
- # Resolve variable (alias for get method for hydrator compatibility)
94
- def resolve_variable(variable_path)
95
- get(variable_path)
96
- end
97
-
98
- private
99
-
100
- # Build consolidated app data (replaces runtime_data + computed_data)
101
- def build_app_data
102
- app = {}
103
-
104
- # Request context (from current runtime_data)
105
- if req && req.respond_to?(:env) && req.env
106
- app['csrf_token'] = req.env.fetch(@config.csrf_token_name, nil)
107
- app['nonce'] = get_or_generate_nonce
108
- app['request_id'] = req.env.fetch('request_id', nil)
109
- app['domain_strategy'] = req.env.fetch('domain_strategy', :default)
110
- app['display_domain'] = req.env.fetch('display_domain', nil)
111
- else
112
- # Generate nonce even without request if CSP is enabled
113
- app['nonce'] = get_or_generate_nonce
114
- end
115
-
116
- # Configuration (from both layers)
117
- app['environment'] = @config.app_environment
118
- app['api_base_url'] = @config.api_base_url
119
- app['features'] = @config.features
120
- app['development'] = @config.development?
121
-
122
- # Authentication & UI (from current computed_data)
123
- app['authenticated'] = authenticated?
124
- app['theme_class'] = determine_theme_class
125
-
126
- app
127
- end
128
-
129
- # Build API base URL from configuration (deprecated - moved to config)
130
- def build_api_base_url
131
- @config.api_base_url
132
- end
133
-
134
- # Determine theme class for CSS
135
- def determine_theme_class
136
- # Default theme logic - can be overridden by business data
137
- if props['theme']
138
- "theme-#{props['theme']}"
139
- elsif cust && cust.respond_to?(:theme_preference)
140
- "theme-#{cust.theme_preference}"
141
- else
142
- 'theme-light'
143
- end
144
- end
145
-
146
- # Check if user is authenticated
147
- def authenticated?
148
- sess && sess.authenticated? && cust && !cust.anonymous?
149
- end
150
-
151
- # Get default session instance
152
- def default_session
153
- Rhales::Adapters::AnonymousSession.new
154
- end
155
-
156
- # Get default customer instance
157
- def default_customer
158
- Rhales::Adapters::AnonymousAuth.new
159
- end
160
-
161
- # Normalize hash keys to strings recursively
162
- def normalize_keys(data)
163
- case data
164
- when Hash
165
- data.each_with_object({}) do |(key, value), result|
166
- result[key.to_s] = normalize_keys(value)
167
- end
168
- when Array
169
- data.map { |item| normalize_keys(item) }
170
- else
171
- data
172
- end
173
- end
174
-
175
- # Recursively collect all variable paths from nested data
176
- def collect_variable_paths(data, prefix = '')
177
- paths = []
178
-
179
- case data
180
- when Hash
181
- data.each do |key, value|
182
- current_path = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
183
- paths << current_path
184
-
185
- if value.is_a?(Hash) || value.is_a?(Object)
186
- paths.concat(collect_variable_paths(value, current_path))
187
- end
188
- end
189
- when Object
190
- # For objects, collect method names that look like attributes
191
- data.public_methods(false).each do |method|
192
- method_name = method.to_s
193
- next if method_name.end_with?('=') # Skip setters
194
- next if method_name.start_with?('_') # Skip private-ish methods
195
-
196
- current_path = prefix.empty? ? method_name : "#{prefix}.#{method_name}"
197
- paths << current_path
198
- end
199
- end
200
-
201
- paths
202
- end
203
-
204
- # Get or generate nonce for CSP
205
- def get_or_generate_nonce
206
- # Try to get existing nonce from request env
207
- if req && req.respond_to?(:env) && req.env
208
- existing_nonce = req.env.fetch(@config.nonce_header_name, nil)
209
- return existing_nonce if existing_nonce
210
- end
211
-
212
- # Generate new nonce if auto_nonce is enabled or CSP is enabled
213
- return CSP.generate_nonce if @config.auto_nonce || (@config.csp_enabled && csp_nonce_required?)
214
-
215
- # Return nil if nonce is not needed
216
- nil
217
- end
218
-
219
- # Check if CSP policy requires nonce
220
- def csp_nonce_required?
221
- return false unless @config.csp_enabled
222
-
223
- csp = CSP.new(@config)
224
- csp.nonce_required?
225
- end
226
-
227
- class << self
228
- # Create context with business data for a specific view
229
- def for_view(req, sess, cust, locale, config: nil, **props)
230
- new(req, sess, cust, locale, props: props, config: config)
231
- end
232
-
233
- # Create minimal context for testing
234
- def minimal(props: {}, config: nil)
235
- new(nil, nil, nil, 'en', props: props, config: config)
236
- end
237
- end
238
- end
239
- end
@@ -1,221 +0,0 @@
1
- # lib/rhales/hydration_data_aggregator.rb
2
-
3
- require 'json'
4
- require_relative 'template_engine'
5
- require_relative 'errors'
6
-
7
- module Rhales
8
- # HydrationDataAggregator traverses the ViewComposition and executes
9
- # all <data> sections to produce a single, merged JSON structure.
10
- #
11
- # This class implements the server-side data aggregation phase of the
12
- # two-pass rendering model, handling:
13
- # - Traversal of the template dependency tree
14
- # - Execution of <data> sections with full server context
15
- # - Merge strategies (deep, shallow, strict)
16
- # - Collision detection and error reporting
17
- #
18
- # The aggregator replaces the HydrationRegistry by performing all
19
- # data merging in a single, coordinated pass.
20
- class HydrationDataAggregator
21
- class JSONSerializationError < StandardError; end
22
-
23
- def initialize(context)
24
- @context = context
25
- @window_attributes = {}
26
- @merged_data = {}
27
- end
28
-
29
- # Aggregate all hydration data from the view composition
30
- def aggregate(composition)
31
- composition.each_document_in_render_order do |template_name, parser|
32
- process_template(template_name, parser)
33
- end
34
-
35
- @merged_data
36
- end
37
-
38
- private
39
-
40
- def process_template(_template_name, parser)
41
- data_content = parser.section('data')
42
- return unless data_content
43
-
44
- window_attr = parser.window_attribute || 'data'
45
- merge_strategy = parser.merge_strategy
46
-
47
- # Build template path for error reporting
48
- template_path = build_template_path(parser)
49
-
50
- # Process the data section first to check if it's empty
51
- processed_data = process_data_section(data_content, parser)
52
-
53
- # Check for collisions only if the data is not empty
54
- if @window_attributes.key?(window_attr) && merge_strategy.nil? && !empty_data?(processed_data)
55
- existing = @window_attributes[window_attr]
56
- existing_data = @merged_data[window_attr]
57
-
58
- # Only raise collision error if existing data is also not empty
59
- unless empty_data?(existing_data)
60
- raise ::Rhales::HydrationCollisionError.new(window_attr, existing[:path], template_path)
61
- end
62
- end
63
-
64
- # Merge or set the data
65
- @merged_data[window_attr] = if @merged_data.key?(window_attr)
66
- merge_data(
67
- @merged_data[window_attr],
68
- processed_data,
69
- merge_strategy || 'deep',
70
- window_attr,
71
- template_path,
72
- )
73
- else
74
- processed_data
75
- end
76
-
77
- # Track the window attribute
78
- @window_attributes[window_attr] = {
79
- path: template_path,
80
- merge_strategy: merge_strategy,
81
- }
82
- end
83
-
84
- def process_data_section(data_content, parser)
85
- # Create a JSON-aware context wrapper for data sections
86
- json_context = JsonAwareContext.new(@context)
87
-
88
- # Process template variables in the data section
89
- processed_content = TemplateEngine.render(data_content, json_context)
90
-
91
- # Parse as JSON
92
- begin
93
- JSON.parse(processed_content)
94
- rescue JSON::ParserError => ex
95
- template_path = build_template_path(parser)
96
- raise JSONSerializationError,
97
- "Invalid JSON in data section at #{template_path}: #{ex.message}\n" \
98
- "Processed content: #{processed_content[0..200]}..."
99
- end
100
- end
101
-
102
- def merge_data(target, source, strategy, window_attr, template_path)
103
- case strategy
104
- when 'deep'
105
- deep_merge(target, source)
106
- when 'shallow'
107
- shallow_merge(target, source, window_attr, template_path)
108
- when 'strict'
109
- strict_merge(target, source, window_attr, template_path)
110
- else
111
- raise ArgumentError, "Unknown merge strategy: #{strategy}"
112
- end
113
- end
114
-
115
- def deep_merge(target, source)
116
- result = target.dup
117
-
118
- source.each do |key, value|
119
- result[key] = if result.key?(key) && result[key].is_a?(Hash) && value.is_a?(Hash)
120
- deep_merge(result[key], value)
121
- else
122
- value
123
- end
124
- end
125
-
126
- result
127
- end
128
-
129
- def shallow_merge(target, source, window_attr, template_path)
130
- result = target.dup
131
-
132
- source.each do |key, value|
133
- if result.key?(key)
134
- raise ::Rhales::HydrationCollisionError.new(
135
- "#{window_attr}.#{key}",
136
- @window_attributes[window_attr][:path],
137
- template_path,
138
- )
139
- end
140
- result[key] = value
141
- end
142
-
143
- result
144
- end
145
-
146
- def strict_merge(target, source, window_attr, template_path)
147
- # In strict mode, any collision is an error
148
- target.each_key do |key|
149
- next unless source.key?(key)
150
-
151
- raise ::Rhales::HydrationCollisionError.new(
152
- "#{window_attr}.#{key}",
153
- @window_attributes[window_attr][:path],
154
- template_path,
155
- )
156
- end
157
-
158
- target.merge(source)
159
- end
160
-
161
- def build_template_path(parser)
162
- data_node = parser.section_node('data')
163
- line_number = data_node ? data_node.location.start_line : 1
164
-
165
- if parser.file_path
166
- "#{parser.file_path}:#{line_number}"
167
- else
168
- "<inline>:#{line_number}"
169
- end
170
- end
171
-
172
- # Check if data is considered empty for collision detection
173
- def empty_data?(data)
174
- return true if data.nil?
175
- return true if data == {}
176
- return true if data == []
177
- return true if data.respond_to?(:empty?) && data.empty?
178
-
179
- false
180
- end
181
- end
182
-
183
- # Context wrapper that automatically converts Ruby objects to JSON in data sections
184
- class JsonAwareContext
185
- def initialize(context)
186
- @context = context
187
- end
188
-
189
- # Delegate all methods to the wrapped context
190
- def method_missing(method, *, &)
191
- @context.send(method, *, &)
192
- end
193
-
194
- def respond_to_missing?(method, include_private = false)
195
- @context.respond_to?(method, include_private)
196
- end
197
-
198
- # Override get method to return JSON-serialized objects
199
- def get(variable_path)
200
- value = @context.get(variable_path)
201
-
202
- # Convert Ruby objects to JSON for data sections
203
- case value
204
- when Hash, Array
205
- begin
206
- value.to_json
207
- rescue JSON::GeneratorError, SystemStackError => ex
208
- # Handle serialization errors (circular references, unsupported types, etc.)
209
- raise JSONSerializationError,
210
- "Failed to serialize Ruby object to JSON: #{ex.message}. " \
211
- "Object type: #{value.class}, var path: #{variable_path}..."
212
- end
213
- else
214
- value
215
- end
216
- end
217
-
218
- # Alias for compatibility with template engine
219
- alias resolve_variable get
220
- end
221
- end