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.
- checksums.yaml +4 -4
- data/.github/renovate.json5 +52 -0
- data/.github/workflows/ci.yml +123 -0
- data/.github/workflows/claude-code-review.yml +69 -0
- data/.github/workflows/claude.yml +49 -0
- data/.github/workflows/code-smells.yml +146 -0
- data/.github/workflows/ruby-lint.yml +78 -0
- data/.github/workflows/yardoc.yml +126 -0
- data/.gitignore +55 -0
- data/.pr_agent.toml +63 -0
- data/.pre-commit-config.yaml +89 -0
- data/.prettierignore +8 -0
- data/.prettierrc +38 -0
- data/.reek.yml +98 -0
- data/.rubocop.yml +428 -0
- data/.serena/.gitignore +3 -0
- data/.yardopts +56 -0
- data/CHANGELOG.md +44 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +29 -0
- data/Gemfile.lock +189 -0
- data/README.md +686 -868
- data/Rakefile +46 -0
- data/debug_context.rb +25 -0
- data/demo/rhales-roda-demo/.gitignore +7 -0
- data/demo/rhales-roda-demo/Gemfile +32 -0
- data/demo/rhales-roda-demo/Gemfile.lock +151 -0
- data/demo/rhales-roda-demo/MAIL.md +405 -0
- data/demo/rhales-roda-demo/README.md +376 -0
- data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
- data/demo/rhales-roda-demo/Rakefile +49 -0
- data/demo/rhales-roda-demo/app.rb +325 -0
- data/demo/rhales-roda-demo/bin/rackup +26 -0
- data/demo/rhales-roda-demo/config.ru +13 -0
- data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
- data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
- data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
- data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
- data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
- data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
- data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
- data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
- data/demo/rhales-roda-demo/templates/home.rue +78 -0
- data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
- data/demo/rhales-roda-demo/templates/login.rue +65 -0
- data/demo/rhales-roda-demo/templates/logout.rue +25 -0
- data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
- data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
- data/demo/rhales-roda-demo/test_full_output.rb +27 -0
- data/demo/rhales-roda-demo/test_simple.rb +24 -0
- data/docs/.gitignore +9 -0
- data/docs/architecture/data-flow.md +499 -0
- data/examples/dashboard-with-charts.rue +271 -0
- data/examples/form-with-validation.rue +180 -0
- data/examples/simple-page.rue +61 -0
- data/examples/vue.rue +136 -0
- data/generate-json-schemas.ts +158 -0
- data/json_schemer_migration_summary.md +172 -0
- data/lib/rhales/adapters/base_auth.rb +2 -0
- data/lib/rhales/adapters/base_request.rb +2 -0
- data/lib/rhales/adapters/base_session.rb +2 -0
- data/lib/rhales/adapters.rb +7 -0
- data/lib/rhales/configuration.rb +47 -0
- data/lib/rhales/core/context.rb +354 -0
- data/lib/rhales/{rue_document.rb → core/rue_document.rb} +56 -38
- data/lib/rhales/{template_engine.rb → core/template_engine.rb} +66 -59
- data/lib/rhales/{view.rb → core/view.rb} +112 -135
- data/lib/rhales/{view_composition.rb → core/view_composition.rb} +78 -8
- data/lib/rhales/core.rb +9 -0
- data/lib/rhales/errors/hydration_collision_error.rb +2 -0
- data/lib/rhales/errors.rb +2 -0
- data/lib/rhales/{earliest_injection_detector.rb → hydration/earliest_injection_detector.rb} +4 -0
- data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
- data/lib/rhales/{hydration_endpoint.rb → hydration/hydration_endpoint.rb} +16 -12
- data/lib/rhales/{hydration_injector.rb → hydration/hydration_injector.rb} +4 -0
- data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
- data/lib/rhales/hydration/hydrator.rb +102 -0
- data/lib/rhales/{link_based_injection_detector.rb → hydration/link_based_injection_detector.rb} +4 -0
- data/lib/rhales/{mount_point_detector.rb → hydration/mount_point_detector.rb} +4 -0
- data/lib/rhales/{safe_injection_validator.rb → hydration/safe_injection_validator.rb} +4 -0
- data/lib/rhales/hydration.rb +13 -0
- data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +3 -1
- data/lib/rhales/{tilt.rb → integrations/tilt.rb} +22 -15
- data/lib/rhales/integrations.rb +6 -0
- data/lib/rhales/middleware/json_responder.rb +191 -0
- data/lib/rhales/middleware/schema_validator.rb +300 -0
- data/lib/rhales/middleware.rb +6 -0
- data/lib/rhales/parsers/handlebars_parser.rb +2 -0
- data/lib/rhales/parsers/rue_format_parser.rb +9 -7
- data/lib/rhales/parsers.rb +9 -0
- data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
- data/lib/rhales/utils/json_serializer.rb +114 -0
- data/lib/rhales/utils/logging_helpers.rb +75 -0
- data/lib/rhales/utils/schema_extractor.rb +132 -0
- data/lib/rhales/utils/schema_generator.rb +194 -0
- data/lib/rhales/utils.rb +40 -0
- data/lib/rhales/version.rb +3 -1
- data/lib/rhales.rb +41 -24
- data/lib/tasks/rhales_schema.rake +197 -0
- data/package.json +10 -0
- data/pnpm-lock.yaml +345 -0
- data/pnpm-workspace.yaml +2 -0
- data/proofs/error_handling.rb +79 -0
- data/proofs/expanded_object_inheritance.rb +82 -0
- data/proofs/partial_context_scoping_fix.rb +168 -0
- data/proofs/ui_context_partial_inheritance.rb +236 -0
- data/rhales.gemspec +14 -6
- data/schema_vs_data_comparison.md +254 -0
- data/test_direct_access.rb +36 -0
- metadata +141 -23
- data/CLAUDE.locale.txt +0 -7
- data/lib/rhales/context.rb +0 -239
- data/lib/rhales/hydration_data_aggregator.rb +0 -221
- data/lib/rhales/hydrator.rb +0 -141
- 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
|
+
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
|
-
-
|
|
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/
|
|
36
|
-
- lib/rhales/
|
|
37
|
-
- lib/rhales/
|
|
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/
|
|
41
|
-
- lib/rhales/
|
|
42
|
-
- lib/rhales/
|
|
43
|
-
- lib/rhales/
|
|
44
|
-
- lib/rhales/
|
|
45
|
-
- lib/rhales/
|
|
46
|
-
- lib/rhales/
|
|
47
|
-
- lib/rhales/
|
|
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/
|
|
51
|
-
- lib/rhales/
|
|
52
|
-
- lib/rhales/
|
|
53
|
-
- lib/rhales/
|
|
54
|
-
- lib/rhales/
|
|
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/
|
|
57
|
-
-
|
|
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
|
|
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.
|
|
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
|
data/lib/rhales/context.rb
DELETED
|
@@ -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
|