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
data/README.md CHANGED
@@ -1,100 +1,90 @@
1
1
  # Rhales - Ruby Single File Components
2
2
 
3
3
  > [!CAUTION]
4
- > **Early Development Release** - Rhales is in active development (v0.1.0). The API may change between versions. While functional and tested, it's recommended for experimental use and contributions rather than production applications. Please report issues and provide feedback through GitHub.
4
+ > **Early Development Release** - Rhales is in active development (v0.5). The API underwent breaking changes from v0.4. While functional and tested, it's recommended for experimental use and contributions. Please report issues and provide feedback through GitHub.
5
5
 
6
- Rhales is a framework for building server-rendered components with client-side data hydration using `.rue` files called RSFCs (Ruby Single File Components). Similar to Vue.js single file components but designed for Ruby applications.
6
+ Rhales is a **type-safe contract enforcement framework** for server-rendered pages with client-side data hydration. It uses `.rue` files (Ruby Single File Components) that combine Zod v4 schemas, Handlebars templates, and documentation into a single contract-first format.
7
7
 
8
- About the name:
9
- It all started with a simple mustache template many years ago. The successor to mustache, "Handlebars" is a visual analog for a mustache and successor to the format. "Two Whales Kissing" is another visual analog for a mustache and since we're working with Ruby we could call that, "Two Whales Kissing for Ruby", which is very long. Rhales combines Ruby and Whales into a one-word name for our library. It's a perfect name with absolutely no ambiguity or risk of confusion with other gems.
8
+ **About the name:** It all started with a simple mustache template many years ago. Mustache's successor, "Handlebars," is a visual analog for a mustache. "Two Whales Kissing" is another visual analog for a mustache, and since we're working with Ruby, we call it "Rhales" (Ruby + Whales). It's a perfect name with absolutely no ambiguity or risk of confusion.
9
+
10
+ ## What's New in v0.5
11
+
12
+ - ✅ **Schema-First Design**: Replaced `<data>` sections with Zod v4 `<schema>` sections
13
+ - ✅ **Type Safety**: Contract enforcement between backend and frontend
14
+ - ✅ **Simplified API**: Removed deprecated parameters (`sess`, `cust`, `props:`, `app_data:`)
15
+ - ✅ **Clear Context Layers**: Renamed `app` → `request` for clarity
16
+ - ✅ **Schema Tooling**: Rake tasks for schema generation and validation
17
+ - ✅ **100% Migration**: All demo templates use schemas
18
+
19
+ **Breaking changes from v0.4:** See [Migration Guide](#migration-from-v04-to-v05) below.
10
20
 
11
21
  ## Features
12
22
 
13
- - **Server-side template rendering** with Handlebars-style syntax
14
- - **Enhanced hydration strategies** for optimal client-side performance
15
- - **API endpoint generation** for link-based hydration strategies
16
- - **Window collision detection** prevents silent data overwrites
17
- - **Explicit merge strategies** for controlled data sharing (shallow, deep, strict)
18
- - **Clear security boundaries** between server context and client data
19
- - **Partial support** for component composition
20
- - **Pluggable authentication adapters** for any auth system
21
- - **Security-first design** with XSS protection and automatic CSP generation
23
+ - **Schema-based hydration** with Zod v4 for type-safe client data
24
+ - **Server-side rendering** with Handlebars-style template syntax
25
+ - **Three-layer context** for request, server, and client data separation
26
+ - **Security-first design** with explicit server-to-client boundaries
27
+ - **Layout & partial composition** for component reuse
28
+ - **CSP support** with automatic nonce generation
29
+ - **Framework agnostic** - works with Rails, Roda, Sinatra, Grape, Padrino
22
30
  - **Dependency injection** for testability and flexibility
23
- - **Resource hint optimization** with browser preload/prefetch support
24
31
 
25
32
  ## Installation
26
33
 
27
- Add this line to your application's Gemfile:
34
+ Add to your Gemfile:
28
35
 
29
36
  ```ruby
30
37
  gem 'rhales'
31
38
  ```
32
39
 
33
- And then execute:
40
+ Then execute:
34
41
 
35
42
  ```bash
36
43
  bundle install
37
44
  ```
38
45
 
39
- Or install it yourself as:
40
-
41
- ```bash
42
- gem install rhales
43
- ```
44
-
45
46
  ## Quick Start
46
47
 
47
48
  ### 1. Configure Rhales
48
49
 
49
50
  ```ruby
50
- # Configure Rhales in your application initialization
51
+ # config/initializers/rhales.rb or similar
51
52
  Rhales.configure do |config|
52
53
  config.default_locale = 'en'
53
- config.template_paths = ['templates'] # or 'app/templates', 'views/templates', etc.
54
+ config.template_paths = ['templates']
54
55
  config.features = { dark_mode: true }
55
56
  config.site_host = 'example.com'
56
57
 
57
- # Enhanced Hydration Configuration
58
- config.hydration.injection_strategy = :preload # or :late, :early, :earliest, :prefetch, :modulepreload, :lazy
59
- config.hydration.api_endpoint_path = '/api/hydration'
60
- config.hydration.fallback_to_late = true
61
- config.hydration.api_cache_enabled = true
62
- config.hydration.cors_enabled = true
63
-
64
- # CSP configuration (enabled by default)
65
- config.csp_enabled = true # Enable automatic CSP header generation
66
- config.auto_nonce = true # Automatically generate nonces
67
- config.csp_policy = { # Customize CSP policy (optional)
68
- 'default-src' => ["'self'"],
69
- 'script-src' => ["'self'", "'nonce-{{nonce}}'"],
70
- 'style-src' => ["'self'", "'nonce-{{nonce}}'", "'unsafe-hashes'"]
71
- # ... more directives
72
- }
58
+ # CSP configuration
59
+ config.csp_enabled = true
60
+ config.auto_nonce = true
73
61
  end
62
+
63
+ # Optional: Configure logger
64
+ Rhales.logger = Rails.logger
74
65
  ```
75
66
 
76
- ### 2. Create a Simple Component
67
+ ### 2. Create a .rue Component
77
68
 
78
- Create a `.rue` file in your templates directory:
69
+ Create `templates/hello.rue`:
79
70
 
80
- ```rue
81
- <!-- templates/hello.rue -->
82
- <data>
83
- {
84
- "greeting": "{{greeting}}",
85
- "user_name": "{{user.name}}"
86
- }
87
- </data>
71
+ ```xml
72
+ <schema lang="js-zod" window="appData">
73
+ const schema = z.object({
74
+ greeting: z.string(),
75
+ userName: z.string()
76
+ });
77
+ </schema>
88
78
 
89
79
  <template>
90
80
  <div class="hello-component">
91
- <h1>{{greeting}}, {{user.name}}!</h1>
92
- <p>Welcome to Rhales RSFC!</p>
81
+ <h1>{{greeting}}, {{userName}}!</h1>
82
+ <p>Welcome to Rhales v0.5</p>
93
83
  </div>
94
84
  </template>
95
85
 
96
86
  <logic>
97
- # Simple greeting component
87
+ # Simple greeting component demonstrating schema-based hydration
98
88
  </logic>
99
89
  ```
100
90
 
@@ -102,232 +92,469 @@ Create a `.rue` file in your templates directory:
102
92
 
103
93
  ```ruby
104
94
  # In your controller/route handler
105
- view = Rhales::View.new(request, session, user, 'en',
106
- props: {
95
+ view = Rhales::View.new(
96
+ request,
97
+ client: {
107
98
  greeting: 'Hello',
108
- user: { name: 'World' }
99
+ userName: 'World'
109
100
  }
110
101
  )
111
102
 
112
103
  html = view.render('hello')
113
- # Returns HTML with embedded JSON for client-side hydration
104
+ # Returns HTML with schema-validated data injected as window.appData
105
+ ```
106
+
107
+ ## The .rue File Format
108
+
109
+ A `.rue` file contains three sections:
110
+
111
+ ```xml
112
+ <schema lang="js-zod" window="data" [version="2"] [envelope="Envelope"] [layout="layouts/main"]>
113
+ const schema = z.object({
114
+ // Zod v4 schema defining client data contract
115
+ });
116
+ </schema>
117
+
118
+ <template>
119
+ <!-- Handlebars-style HTML template -->
120
+ <!-- Has access to ALL context layers -->
121
+ </template>
122
+
123
+ <logic>
124
+ # Optional Ruby documentation/comments
125
+ </logic>
126
+ ```
127
+
128
+ ### Schema Section Attributes
129
+
130
+ | Attribute | Required | Description | Example |
131
+ |-----------|----------|-------------|---------|
132
+ | `lang` | Yes | Schema language (currently only `js-zod`) | `"js-zod"` |
133
+ | `window` | Yes | Browser global name | `"appData"` → `window.appData` |
134
+ | `version` | No | Schema version | `"2"` |
135
+ | `envelope` | No | Response wrapper type | `"SuccessEnvelope"` |
136
+ | `layout` | No | Layout template reference | `"layouts/main"` |
137
+
138
+ ### Zod Schema Examples
139
+
140
+ ```javascript
141
+ // Simple types
142
+ z.object({
143
+ user: z.string(),
144
+ count: z.number(),
145
+ active: z.boolean()
146
+ })
147
+
148
+ // Complex nested structures
149
+ z.object({
150
+ user: z.object({
151
+ id: z.number(),
152
+ name: z.string(),
153
+ email: z.string().email()
154
+ }),
155
+ items: z.array(z.object({
156
+ id: z.number(),
157
+ title: z.string(),
158
+ price: z.number().positive()
159
+ })),
160
+ metadata: z.record(z.string())
161
+ })
162
+
163
+ // Optional and nullable
164
+ z.object({
165
+ theme: z.string().optional(),
166
+ lastLogin: z.string().nullable()
167
+ })
114
168
  ```
115
169
 
116
170
  ## Context and Data Model
117
171
 
118
- Rhales uses a **two-layer data model** for template rendering:
172
+ Rhales uses a **three-layer context system** that separates concerns and enforces security boundaries:
173
+
174
+ ### 1. Request Layer (Framework Data)
119
175
 
120
- ### 1. App Data (Framework Layer)
121
- All framework-provided data is available under the `app` namespace:
176
+ Framework-provided data available under the `request` namespace:
122
177
 
123
178
  ```handlebars
124
- <!-- Framework data through app namespace -->
125
- {{app.csrf_token}} <!-- CSRF token for forms -->
126
- {{app.nonce}} <!-- CSP nonce for scripts -->
127
- {{app.authenticated}} <!-- Authentication state -->
128
- {{app.environment}} <!-- Current environment -->
129
- {{app.features.dark_mode}} <!-- Feature flags -->
130
- {{app.theme_class}} <!-- Current theme -->
179
+ {{request.nonce}} <!-- CSP nonce for scripts -->
180
+ {{request.csrf_token}} <!-- CSRF token for forms -->
181
+ {{request.authenticated?}} <!-- Authentication state -->
182
+ {{request.locale}} <!-- Current locale -->
131
183
  ```
132
184
 
133
- **Available App Variables:**
134
- - `app.api_base_url` - Base URL for API calls
135
- - `app.authenticated` - Whether user is authenticated
136
- - `app.csrf_token` - CSRF token for forms
137
- - `app.development` - Whether in development mode
138
- - `app.environment` - Current environment (production/staging/dev)
139
- - `app.features` - Feature flags hash
140
- - `app.nonce` - CSP nonce for inline scripts
141
- - `app.theme_class` - Current theme CSS class
185
+ **Available Request Variables:**
186
+ - `request.nonce` - CSP nonce for inline scripts/styles
187
+ - `request.csrf_token` - CSRF token for form submissions
188
+ - `request.authenticated?` - User authentication status
189
+ - `request.locale` - Current locale (e.g., 'en', 'es')
190
+ - `request.session` - Session object (if available)
191
+ - `request.user` - User object (if available)
142
192
 
143
- ### 2. Props Data (Application Layer)
144
- Your application-specific data passed to each view:
193
+ ### 2. Server Layer (Template-Only Data)
194
+
195
+ Application data that stays on the server (not sent to browser):
196
+
197
+ ```ruby
198
+ view = Rhales::View.new(
199
+ request,
200
+ server: {
201
+ page_title: 'Dashboard',
202
+ vite_assets_html: vite_javascript_tag('application'),
203
+ admin_notes: 'Internal use only' # Never sent to client
204
+ }
205
+ )
206
+ ```
145
207
 
146
208
  ```handlebars
147
- <!-- Application data -->
148
- {{user.name}} <!-- Direct access -->
149
- {{page_title}} <!-- Props take precedence -->
150
- {{#if user.admin?}}
151
- <a href="/admin">Admin Panel</a>
152
- {{/if}}
209
+ {{server.page_title}} <!-- Available in templates -->
210
+ {{server.vite_assets_html}} <!-- Server-side only -->
211
+ ```
212
+
213
+ ### 3. Client Layer (Serialized to Browser)
214
+
215
+ Data serialized to browser via schema validation:
216
+
217
+ ```ruby
218
+ view = Rhales::View.new(
219
+ request,
220
+ client: {
221
+ user: current_user.name,
222
+ items: Item.all.map(&:to_h)
223
+ }
224
+ )
153
225
  ```
154
226
 
227
+ ```handlebars
228
+ {{client.user}} <!-- Also serialized to window.appData.user -->
229
+ {{client.items}} <!-- Also serialized to window.appData.items -->
230
+ ```
231
+
232
+ ### Context Layer Fallback
233
+
234
+ Variables can use shorthand notation (checks `client` → `server` → `request`):
235
+
236
+ ```handlebars
237
+ <!-- Explicit layer access -->
238
+ {{client.user}}
239
+ {{server.page_title}}
240
+ {{request.nonce}}
241
+
242
+ <!-- Shorthand (automatic layer lookup) -->
243
+ {{user}} <!-- Finds client.user -->
244
+ {{page_title}} <!-- Finds server.page_title -->
245
+ {{nonce}} <!-- Finds request.nonce -->
246
+ ```
247
+
248
+ ## Security Model: Server-to-Client Boundary
249
+
250
+ The `.rue` format enforces a **security boundary at the server-to-client handoff**:
251
+
155
252
  ### Server Templates: Full Context Access
156
- Templates have complete access to all server-side data:
157
- - User objects and authentication state
158
- - Database connections and internal APIs
159
- - Configuration values and secrets
160
- - Request metadata (CSRF tokens, nonces)
253
+
254
+ Templates have access to ALL context layers:
161
255
 
162
256
  ```handlebars
163
- <!-- Full server access in templates -->
164
- {{#if user.admin?}}
165
- <a href="/admin">Admin Panel</a>
257
+ <!-- Server-side template has full access -->
258
+ {{#if request.authenticated?}}
259
+ <div class="admin-panel">
260
+ <h2>Welcome {{client.user}}</h2>
261
+ <p>Secret: {{server.admin_notes}}</p> <!-- Not sent to browser -->
262
+ </div>
166
263
  {{/if}}
167
- <div class="{{app.theme_class}}">{{user.full_name}}</div>
168
264
  ```
169
265
 
170
266
  ### Client Data: Explicit Allowlist
171
- Only data declared in `<data>` sections reaches the browser:
172
267
 
173
- ```rue
174
- <data>
175
- {
176
- "display_name": "{{user.name}}",
177
- "preferences": {
178
- "theme": "{{user.theme}}"
179
- }
180
- }
181
- </data>
268
+ Only schema-declared data reaches the browser:
269
+
270
+ ```xml
271
+ <schema lang="js-zod" window="data">
272
+ const schema = z.object({
273
+ user: z.string(),
274
+ userId: z.number()
275
+ // NOT declared: admin_notes, secret_key, internal_api_url
276
+ });
277
+ </schema>
182
278
  ```
183
279
 
184
- Becomes:
280
+ **Result on client:**
281
+
185
282
  ```javascript
186
283
  window.data = {
187
- "display_name": "John Doe",
188
- "preferences": { "theme": "dark" }
284
+ user: "Alice",
285
+ userId: 123
286
+ // admin_notes, secret_key NOT included (never declared in schema)
189
287
  }
190
- // No access to user.admin?, internal APIs, etc.
191
288
  ```
192
289
 
193
- This creates a **REST API-like boundary** where you explicitly declare what data crosses the server-to-client security boundary.
290
+ This creates a **REST API-like boundary** where you explicitly declare what data crosses the security boundary.
194
291
 
195
- For complete details, see [Context and Data Boundaries Documentation](docs/CONTEXT_AND_DATA_BOUNDARIES.md).
196
- config.site_ssl_enabled = true
197
- end
292
+ ### ⚠️ Critical: Schema Validates, Does NOT Filter
293
+
294
+ **IMPORTANT**: The schema does NOT filter which data gets serialized. The ENTIRE `client:` hash is serialized to the browser. The schema only validates that the serialized data matches the expected structure.
295
+
296
+ ```ruby
297
+ # ⚠️ DANGER: ALL client data serialized (including password!)
298
+ view = Rhales::View.new(request,
299
+ client: {
300
+ user: 'Alice',
301
+ password: 'secret123', # ← Serialized to browser!
302
+ api_key: 'xyz' # ← Serialized to browser!
303
+ }
304
+ )
305
+
306
+ # Schema only validates structure, doesn't prevent serialization
307
+ # If schema doesn't include password/api_key, validation FAILS
308
+ # But data already leaked to browser in HTML response
198
309
  ```
199
310
 
200
- ### 2. Create a .rue file
311
+ **Your Responsibility**: Ensure the `client:` hash contains ONLY safe, public data. Never pass:
312
+ - Passwords or credentials
313
+ - API keys or secrets
314
+ - Internal URLs or configuration
315
+ - Personally identifiable information (PII) not intended for client
316
+
317
+ The schema is a **contract validator**, not a **data filter**.
318
+
319
+ ## Complete Example: Dashboard
320
+
321
+ ### Backend (Ruby)
201
322
 
202
- Create `templates/dashboard.rue` - notice how the `<data>` section defines exactly what your frontend app will receive:
323
+ ```ruby
324
+ # config/routes.rb (Rails) or route handler
325
+ class DashboardController < ApplicationController
326
+ def show
327
+ view = Rhales::View.new(
328
+ request,
329
+ client: {
330
+ user: current_user.name,
331
+ userId: current_user.id,
332
+ items: current_user.items.map { |i|
333
+ { id: i.id, name: i.name, price: i.price }
334
+ },
335
+ apiBaseUrl: ENV['API_BASE_URL']
336
+ },
337
+ server: {
338
+ page_title: 'Dashboard',
339
+ internal_notes: 'User has premium access', # Server-only
340
+ vite_assets: vite_javascript_tag('application')
341
+ }
342
+ )
343
+
344
+ render html: view.render('dashboard').html_safe
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### Frontend (.rue file)
203
350
 
204
351
  ```xml
205
- <data window="appState" schema="@/src/types/app-state.d.ts">
206
- {
207
- "user": {
208
- "id": "{{user.id}}",
209
- "name": "{{user.name}}",
210
- "email": "{{user.email}}",
211
- "preferences": {{user.preferences}}
212
- },
213
- "products": {{recent_products}},
214
- "cart": {
215
- "items": {{cart.items}},
216
- "total": "{{cart.total}}"
217
- },
218
- "api": {
219
- "baseUrl": "{{api_base_url}}",
220
- "csrfToken": "{{csrf_token}}"
221
- },
222
- "features": {{enabled_features}}
223
- }
224
- </data>
352
+ <!-- templates/dashboard.rue -->
353
+ <schema lang="js-zod" version="2" window="dashboardData" layout="layouts/main">
354
+ const schema = z.object({
355
+ user: z.string(),
356
+ userId: z.number(),
357
+ items: z.array(z.object({
358
+ id: z.number(),
359
+ name: z.string(),
360
+ price: z.number()
361
+ })),
362
+ apiBaseUrl: z.string().url()
363
+ });
364
+ </schema>
225
365
 
226
366
  <template>
227
- <!doctype html>
228
- <html lang="{{locale}}" class="{{theme_class}}">
229
- <head>
230
- <title>{{page_title}}</title>
231
- <meta name="csrf-token" content="{{csrf_token}}">
232
- </head>
233
- <body>
234
- <!-- Critical: The mounting point for your frontend framework -->
235
- <div id="app">
236
- <!-- Server-rendered content for SEO and initial load -->
237
- <nav>{{> navigation}}</nav>
238
- <main>
239
- <h1>{{page_title}}</h1>
240
- {{#if user}}
241
- <p>Welcome back, {{user.name}}!</p>
242
- {{else}}
243
- <p>Please sign in to continue.</p>
244
- {{/if}}
245
- </main>
246
- </div>
367
+ <div class="dashboard">
368
+ <h1>{{server.page_title}}</h1>
369
+
370
+ {{#if request.authenticated?}}
371
+ <p>Welcome, {{client.user}}!</p>
372
+
373
+ <div class="items">
374
+ {{#each client.items}}
375
+ <div class="item">
376
+ <h3>{{name}}</h3>
377
+ <p>${{price}}</p>
378
+ </div>
379
+ {{/each}}
380
+ </div>
381
+ {{else}}
382
+ <p>Please log in</p>
383
+ {{/if}}
384
+ </div>
247
385
 
248
- <!--
249
- RSFC automatically generates hydration scripts:
250
- - <script type="application/json" id="app-state-data">{...}</script>
251
- - <script>window.appState = JSON.parse(...);</script>
252
- -->
386
+ <!-- Client-side JavaScript can access validated data -->
387
+ <script nonce="{{request.nonce}}">
388
+ // window.dashboardData is populated with schema-validated data
389
+ console.log('User ID:', window.dashboardData.userId);
390
+ console.log('Items:', window.dashboardData.items);
253
391
 
254
- <!-- Your frontend framework takes over from here -->
255
- <script nonce="{{nonce}}" type="module" src="/assets/app.js"></script>
256
- </body>
257
- </html>
392
+ // Fetch additional data from API
393
+ fetch(window.dashboardData.apiBaseUrl + '/user/preferences')
394
+ .then(r => r.json())
395
+ .then(prefs => console.log('Preferences:', prefs));
396
+ </script>
258
397
  </template>
398
+
399
+ <logic>
400
+ # Dashboard component demonstrates:
401
+ # - Schema-based type safety
402
+ # - Three-layer context access
403
+ # - Conditional rendering based on auth
404
+ # - Client-side data hydration
405
+ # - CSP nonce support
406
+ </logic>
407
+ ```
408
+
409
+ ### Generated HTML
410
+
411
+ ```html
412
+ <div class="dashboard">
413
+ <h1>Dashboard</h1>
414
+ <p>Welcome, Alice!</p>
415
+ <div class="items">
416
+ <div class="item"><h3>Widget</h3><p>$19.99</p></div>
417
+ <div class="item"><h3>Gadget</h3><p>$29.99</p></div>
418
+ </div>
419
+ </div>
420
+
421
+ <!-- Hydration script injected automatically -->
422
+ <script id="rsfc-data-abc123" type="application/json">
423
+ {"user":"Alice","userId":123,"items":[{"id":1,"name":"Widget","price":19.99},{"id":2,"name":"Gadget","price":29.99}],"apiBaseUrl":"https://api.example.com"}
424
+ </script>
425
+ <script nonce="nonce-xyz789">
426
+ window.dashboardData = JSON.parse(document.getElementById('rsfc-data-abc123').textContent);
427
+ </script>
428
+
429
+ <!-- Your client-side script -->
430
+ <script nonce="nonce-xyz789">
431
+ console.log('User ID:', window.dashboardData.userId);
432
+ console.log('Items:', window.dashboardData.items);
433
+ fetch(window.dashboardData.apiBaseUrl + '/user/preferences')
434
+ .then(r => r.json())
435
+ .then(prefs => console.log('Preferences:', prefs));
436
+ </script>
437
+ ```
438
+
439
+ ## Template Syntax
440
+
441
+ Rhales uses Handlebars-style syntax:
442
+
443
+ ### Variables
444
+
445
+ ```handlebars
446
+ {{variable}} <!-- HTML-escaped (safe) -->
447
+ {{{variable}}} <!-- Raw output (use carefully!) -->
448
+ {{object.property}} <!-- Dot notation -->
449
+ {{array.0}} <!-- Array index -->
259
450
  ```
260
451
 
261
- ### 3. The Manifold: Server-to-SPA Handoff
452
+ ### Conditionals
262
453
 
263
- This example demonstrates Rhales' core value proposition: **eliminating the coordination gap** between server state and frontend frameworks.
454
+ ```handlebars
455
+ {{#if condition}}
456
+ Content when true
457
+ {{else}}
458
+ Content when false
459
+ {{/if}}
264
460
 
265
- **What you get:**
266
- - **Declarative data contract**: The `<data>` section explicitly defines what your frontend receives
267
- - ✅ **Type safety ready**: Schema reference points to TypeScript definitions
268
- - ✅ **Zero coordination overhead**: No separate API design needed for initial state
269
- - ✅ **SEO + SPA**: Server-rendered HTML with automatic client hydration
270
- - ✅ **Security boundaries**: Only explicitly declared data reaches the client
461
+ {{#unless condition}}
462
+ Content when false
463
+ {{/unless}}
464
+ ```
271
465
 
272
- ### 4. RSFC Security Model
466
+ **Truthy/Falsy:**
467
+ - Falsy: `nil`, `null`, `false`, `""`, `0`, `"false"`
468
+ - Truthy: All other values
273
469
 
274
- **Key Principle: The security boundary is at the server-to-client handoff, not within server-side rendering.**
470
+ ### Loops
471
+
472
+ ```handlebars
473
+ {{#each items}}
474
+ {{@index}} <!-- 0-based index -->
475
+ {{@first}} <!-- true if first item -->
476
+ {{@last}} <!-- true if last item -->
477
+ {{this}} <!-- current item (if primitive) -->
478
+ {{name}} <!-- item.name (if object) -->
479
+ {{/each}}
480
+ ```
481
+
482
+ ### Partials
483
+
484
+ ```handlebars
485
+ {{> header}} <!-- Include templates/header.rue -->
486
+ {{> components/nav}} <!-- Include templates/components/nav.rue -->
487
+ ```
488
+
489
+ ### Layouts
275
490
 
276
491
  ```xml
277
- <data>
278
- {
279
- "message": "{{greeting}}", <!-- Exposed to client -->
280
- "user": {
281
- "name": "{{user.name}}" <!-- ✅ Exposed to client -->
282
- }
283
- <!-- ❌ user.secret_key not declared, won't reach client -->
284
- }
285
- </data>
492
+ <!-- templates/pages/home.rue -->
493
+ <schema lang="js-zod" window="data" layout="layouts/main">
494
+ const schema = z.object({ page: z.string() });
495
+ </schema>
286
496
 
287
497
  <template>
288
- <h1>{{greeting}}</h1> <!-- ✅ Full server context access -->
289
- <p>{{user.name}}</p> <!-- ✅ Can access user object methods -->
290
- <p>{{user.secret_key}}</p> <!-- ✅ Server-side only, not in <data> -->
498
+ <h1>Home Page Content</h1>
291
499
  </template>
292
500
  ```
293
501
 
294
- **Template Section (`<template>`):**
295
- - **Full server context access** - like ERB, HAML, or any server-side template
296
- - ✅ **Can call object methods** - `{{user.full_name}}`, `{{products.count}}`, etc.
297
- - **Rich server-side logic** - access to full business objects and their capabilities
298
- - ✅ **Private by default** - nothing in templates reaches the client unless explicitly declared
502
+ ```xml
503
+ <!-- templates/layouts/main.rue -->
504
+ <schema lang="js-zod" window="layoutData">
505
+ const schema = z.object({ siteName: z.string() });
506
+ </schema>
299
507
 
300
- **Data Section (`<data>`):**
301
- - ✅ **Explicit client allowlist** - only declared variables reach the browser
302
- - ✅ **JSON serialization boundary** - like designing a REST API endpoint
303
- - ✅ **Type safety foundation** - can validate against schemas
304
- - ❌ **Cannot expose secrets** - `user.secret_key` won't reach client unless declared
508
+ <template>
509
+ <!DOCTYPE html>
510
+ <html>
511
+ <head>
512
+ <title>{{server.siteName}}</title>
513
+ </head>
514
+ <body>
515
+ <header>{{> components/header}}</header>
516
+ <main>
517
+ <!-- Page content injected here -->
518
+ </main>
519
+ <footer>{{> components/footer}}</footer>
520
+ </body>
521
+ </html>
522
+ </template>
523
+ ```
305
524
 
306
- This design gives you the flexibility of full server-side templating while maintaining explicit control over what data reaches the client.
525
+ ## Schema Tooling
307
526
 
308
- **Generated output:**
309
- ```html
310
- <!-- Server-rendered HTML -->
311
- <div id="app">
312
- <nav>...</nav>
313
- <main><h1>User Dashboard</h1><p>Welcome back, Alice!</p></main>
314
- </div>
527
+ Rhales provides rake tasks for schema management:
315
528
 
316
- <!-- Automatic client hydration -->
317
- <script id="app-state-data" type="application/json">
318
- {"user":{"id":123,"name":"Alice","email":"alice@example.com","preferences":{...}},"products":[...],"cart":{...},"api":{...},"features":{...}}
319
- </script>
320
- <script nonce="abc123">
321
- window.appState = JSON.parse(document.getElementById('app-state-data').textContent);
322
- </script>
529
+ ```bash
530
+ # Generate JSON schemas from .rue templates
531
+ rake rhales:schema:generate TEMPLATES_DIR=./templates
532
+
533
+ # Validate existing JSON schemas
534
+ rake rhales:schema:validate
323
535
 
324
- <!-- Your Vue/React/Svelte app mounts here with full state -->
325
- <script nonce="abc123" type="module" src="/assets/app.js"></script>
536
+ # Show schema statistics
537
+ rake rhales:schema:stats TEMPLATES_DIR=./templates
326
538
  ```
327
539
 
328
- ### 5. Framework Integration
540
+ **Example output:**
329
541
 
330
- #### Rails
542
+ ```
543
+ Schema Statistics
544
+ ============================================================
545
+ Templates directory: templates
546
+
547
+ Total .rue files: 25
548
+ Files with <schema>: 25
549
+ Files without <schema>: 0
550
+
551
+ By language:
552
+ js-zod: 25
553
+ ```
554
+
555
+ ## Framework Integration
556
+
557
+ ### Rails
331
558
 
332
559
  ```ruby
333
560
  # config/initializers/rhales.rb
@@ -338,91 +565,79 @@ end
338
565
 
339
566
  # app/controllers/application_controller.rb
340
567
  class ApplicationController < ActionController::Base
341
- def render_rhales(template_name, data = {})
342
- view = Rhales::View.new(request, session, current_user, I18n.locale)
343
- view.render(template_name, data)
568
+ def render_rhales(template_name, client: {}, server: {})
569
+ view = Rhales::View.new(request, client: client, server: server)
570
+ view.render(template_name)
344
571
  end
345
572
  end
346
573
 
347
574
  # In your controller
348
575
  def dashboard
349
- @dashboard_html = render_rhales('dashboard',
350
- page_title: 'User Dashboard',
351
- user: current_user,
352
- recent_products: Product.recent.limit(5),
353
- cart: current_user.cart,
354
- enabled_features: Feature.enabled_for(current_user)
576
+ html = render_rhales('dashboard',
577
+ client: { user: current_user.name, items: @items },
578
+ server: { page_title: 'Dashboard' }
355
579
  )
580
+ render html: html.html_safe
356
581
  end
357
582
  ```
358
583
 
359
- #### Sinatra
584
+ ### Roda
360
585
 
361
586
  ```ruby
362
587
  # app.rb
363
- require 'sinatra'
588
+ require 'roda'
364
589
  require 'rhales'
365
590
 
366
- Rhales.configure do |config|
367
- config.template_paths = ['templates']
368
- config.default_locale = 'en'
369
- end
591
+ class App < Roda
592
+ plugin :render
370
593
 
371
- helpers do
372
- def render_rhales(template_name, data = {})
373
- view = Rhales::View.new(request, session, current_user, 'en')
374
- view.render(template_name, data)
594
+ Rhales.configure do |config|
595
+ config.template_paths = ['templates']
596
+ config.default_locale = 'en'
375
597
  end
376
- end
377
598
 
378
- get '/dashboard' do
379
- @dashboard_html = render_rhales('dashboard',
380
- page_title: 'Dashboard',
381
- user: current_user,
382
- recent_products: Product.recent,
383
- cart: session[:cart] || {},
384
- enabled_features: FEATURES
385
- )
386
- erb :dashboard
599
+ route do |r|
600
+ r.on 'dashboard' do
601
+ view = Rhales::View.new(
602
+ request,
603
+ client: { user: current_user.name },
604
+ server: { page_title: 'Dashboard' }
605
+ )
606
+ view.render('dashboard')
607
+ end
608
+ end
387
609
  end
388
610
  ```
389
611
 
390
- #### Padrino
612
+ ### Sinatra
391
613
 
392
614
  ```ruby
393
- # config/apps.rb
394
- Padrino.configure_apps do
395
- Rhales.configure do |config|
396
- config.template_paths = ['app/templates']
397
- config.default_locale = 'en'
398
- end
615
+ require 'sinatra'
616
+ require 'rhales'
617
+
618
+ Rhales.configure do |config|
619
+ config.template_paths = ['templates']
620
+ config.default_locale = 'en'
399
621
  end
400
622
 
401
- # app/helpers/application_helper.rb
402
- module ApplicationHelper
403
- def render_rhales(template_name, data = {})
404
- view = Rhales::View.new(request, session, current_user, locale)
405
- view.render(template_name, data)
623
+ helpers do
624
+ def render_rhales(template_name, client: {}, server: {})
625
+ view = Rhales::View.new(request, client: client, server: server)
626
+ view.render(template_name)
406
627
  end
407
628
  end
408
629
 
409
- # app/controllers/application_controller.rb
410
- get :dashboard do
411
- @dashboard_html = render_rhales('dashboard',
412
- page_title: 'Dashboard',
413
- user: current_user,
414
- recent_products: Product.recent,
415
- cart: current_user&.cart,
416
- enabled_features: settings.features
630
+ get '/dashboard' do
631
+ render_rhales('dashboard',
632
+ client: { user: 'Alice' },
633
+ server: { page_title: 'Dashboard' }
417
634
  )
418
- render :dashboard
419
635
  end
420
636
  ```
421
637
 
422
- #### Grape
638
+ ### Grape
423
639
 
424
640
  ```ruby
425
- # config.ru or initializer
426
641
  require 'grape'
427
642
  require 'rhales'
428
643
 
@@ -431,734 +646,337 @@ Rhales.configure do |config|
431
646
  config.default_locale = 'en'
432
647
  end
433
648
 
434
- # api.rb
435
649
  class MyAPI < Grape::API
436
650
  helpers do
437
- def render_rhales(template_name, data = {})
438
- # Create mock request/session for Grape
651
+ def render_rhales(template_name, client: {}, server: {})
439
652
  mock_request = OpenStruct.new(env: env)
440
- mock_session = {}
441
-
442
- view = Rhales::View.new(mock_request, mock_session, current_user, 'en')
443
- view.render(template_name, data)
653
+ view = Rhales::View.new(mock_request, client: client, server: server)
654
+ view.render(template_name)
444
655
  end
445
656
  end
446
657
 
447
658
  get '/dashboard' do
448
659
  content_type 'text/html'
449
660
  render_rhales('dashboard',
450
- page_title: 'API Dashboard',
451
- user: current_user,
452
- recent_products: [],
453
- cart: {},
454
- enabled_features: { api_v2: true }
661
+ client: { user: 'Alice' },
662
+ server: { page_title: 'Dashboard' }
455
663
  )
456
664
  end
457
665
  end
458
666
  ```
459
667
 
460
- #### Roda
461
-
462
- ```ruby
463
- # app.rb
464
- require 'roda'
465
- require 'rhales'
466
-
467
- class App < Roda
468
- plugin :render
469
-
470
- Rhales.configure do |config|
471
- config.template_paths = ['templates']
472
- config.default_locale = 'en'
473
- end
474
-
475
- def render_rhales(template_name, data = {})
476
- view = Rhales::View.new(request, session, current_user, 'en')
477
- view.render(template_name, data)
478
- end
479
-
480
- route do |r|
481
- r.on 'dashboard' do
482
- @dashboard_html = render_rhales('dashboard',
483
- page_title: 'Dashboard',
484
- user: current_user,
485
- recent_products: [],
486
- cart: session[:cart],
487
- enabled_features: FEATURES
488
- )
489
- view('dashboard')
490
- end
491
- end
492
- end
493
- ```
494
-
495
- ### 6. Basic Usage
496
-
497
- ```ruby
498
- # Create a view instance
499
- view = Rhales::View.new(request, session, current_user, locale)
500
-
501
- # Render a template with rich data for frontend hydration
502
- html = view.render('dashboard',
503
- page_title: 'User Dashboard',
504
- user: current_user,
505
- recent_products: Product.recent.limit(5),
506
- cart: current_user.cart,
507
- enabled_features: Feature.enabled_for(current_user)
508
- )
509
-
510
- # Or use the convenience method
511
- html = Rhales.render('dashboard',
512
- request: request,
513
- session: session,
514
- user: current_user,
515
- page_title: 'User Dashboard',
516
- recent_products: products,
517
- cart: cart_data,
518
- enabled_features: features
519
- )
520
- ```
668
+ ## Content Security Policy (CSP)
521
669
 
522
- ## Authentication Adapters
670
+ Rhales provides **security by default** with automatic CSP support.
523
671
 
524
- Rhales supports pluggable authentication adapters. Implement the `Rhales::Adapters::BaseAuth` interface:
672
+ ### Default CSP Configuration
525
673
 
526
674
  ```ruby
527
- class MyAuthAdapter < Rhales::Adapters::BaseAuth
528
- def initialize(user)
529
- @user = user
530
- end
531
-
532
- def anonymous?
533
- @user.nil?
534
- end
535
-
536
- def theme_preference
537
- @user&.theme || 'light'
538
- end
539
-
540
- def user_id
541
- @user&.id
542
- end
543
-
544
- def role?(role)
545
- @user&.roles&.include?(role)
546
- end
675
+ Rhales.configure do |config|
676
+ config.csp_enabled = true # Default: true
677
+ config.auto_nonce = true # Default: true
547
678
  end
548
-
549
- # Use with Rhales
550
- user_adapter = MyAuthAdapter.new(current_user)
551
- view = Rhales::View.new(request, session, user_adapter)
552
- ```
553
-
554
- ## Template Syntax
555
-
556
- Rhales uses a Handlebars-style template syntax:
557
-
558
- ### Variables
559
- - `{{variable}}` - HTML-escaped output
560
- - `{{{variable}}}` - Raw output (use carefully!)
561
-
562
- ### Conditionals
563
- ```erb
564
- {{#if condition}}
565
- Content when true
566
- {{/if}}
567
-
568
- {{#unless condition}}
569
- Content when false
570
- {{/unless}}
571
- ```
572
-
573
- ### Iteration
574
- ```erb
575
- {{#each items}}
576
- <div>{{name}} - {{@index}}</div>
577
- {{/each}}
578
- ```
579
-
580
- ### Partials
581
- ```erb
582
- {{> header}}
583
- {{> navigation}}
584
679
  ```
585
680
 
586
- ## Enhanced Hydration Strategies
681
+ ### Using Nonces in Templates
587
682
 
588
- Rhales provides multiple hydration strategies optimized for different performance requirements and use cases:
589
-
590
- ### Traditional Strategies
591
-
592
- #### `:late` (Default - Backwards Compatible)
593
- Injects scripts before the closing `</body>` tag. Safe and reliable for all scenarios.
594
-
595
- ```ruby
596
- config.hydration.injection_strategy = :late
597
- ```
598
-
599
- #### `:early` (Mount Point Optimization)
600
- Injects scripts immediately before frontend mount points (`#app`, `#root`, etc.) for improved Time-to-Interactive.
601
-
602
- ```ruby
603
- config.hydration.injection_strategy = :early
604
- config.hydration.mount_point_selectors = ['#app', '#root', '[data-mount]']
605
- config.hydration.fallback_to_late = true
606
- ```
607
-
608
- #### `:earliest` (Head Section Injection)
609
- Injects scripts in the HTML head section for maximum performance, after meta tags and stylesheets.
610
-
611
- ```ruby
612
- config.hydration.injection_strategy = :earliest
613
- config.hydration.fallback_to_late = true
614
- ```
615
-
616
- ### Link-Based Strategies (API Endpoints)
617
-
618
- These strategies generate separate API endpoints for hydration data, enabling better caching, parallel loading, and reduced HTML payload sizes.
619
-
620
- #### `:preload` (High Priority Loading)
621
- Generates `<link rel="preload">` tags with immediate script execution for critical data.
622
-
623
- ```ruby
624
- config.hydration.injection_strategy = :preload
625
- config.hydration.api_endpoint_path = '/api/hydration'
626
- config.hydration.link_crossorigin = true
627
- ```
628
-
629
- #### `:prefetch` (Future Page Optimization)
630
- Generates `<link rel="prefetch">` tags for data that will be needed on subsequent page loads.
683
+ ```handlebars
684
+ <script nonce="{{request.nonce}}">
685
+ // Inline JavaScript with automatic nonce
686
+ console.log('Secure execution');
687
+ </script>
631
688
 
632
- ```ruby
633
- config.hydration.injection_strategy = :prefetch
689
+ <style nonce="{{request.nonce}}">
690
+ /* Inline styles with automatic nonce */
691
+ .component { color: blue; }
692
+ </style>
634
693
  ```
635
694
 
636
- #### `:modulepreload` (ES Module Support)
637
- Generates `<link rel="modulepreload">` tags with ES module imports for modern applications.
695
+ ### Custom CSP Policies
638
696
 
639
697
  ```ruby
640
- config.hydration.injection_strategy = :modulepreload
698
+ Rhales.configure do |config|
699
+ config.csp_policy = {
700
+ 'default-src' => ["'self'"],
701
+ 'script-src' => ["'self'", "'nonce-{{nonce}}'", 'https://cdn.example.com'],
702
+ 'style-src' => ["'self'", "'nonce-{{nonce}}'"],
703
+ 'img-src' => ["'self'", 'data:', 'https://images.example.com'],
704
+ 'connect-src' => ["'self'", 'https://api.example.com']
705
+ }
706
+ end
641
707
  ```
642
708
 
643
- #### `:lazy` (Intersection Observer)
644
- Loads data only when mount points become visible using Intersection Observer API.
645
-
646
- ```ruby
647
- config.hydration.injection_strategy = :lazy
648
- config.hydration.lazy_mount_selector = '#app'
649
- ```
709
+ ### Framework CSP Header Setup
650
710
 
651
- #### `:link` (Manual Loading)
652
- Generates basic link references with manual loading functions for custom hydration logic.
711
+ #### Rails
653
712
 
654
713
  ```ruby
655
- config.hydration.injection_strategy = :link
656
- ```
657
-
658
- ### Strategy Performance Comparison
659
-
660
- | Strategy | Time-to-Interactive | Caching | Parallel Loading | Best Use Case |
661
- |----------|-------------------|---------|------------------|---------------|
662
- | `:late` | Standard | Basic | No | Legacy compatibility |
663
- | `:early` | Improved | Basic | No | SPA mount point optimization |
664
- | `:earliest` | Excellent | Basic | No | Critical path optimization |
665
- | `:preload` | Excellent | Advanced | Yes | High-priority data |
666
- | `:prefetch` | Standard | Advanced | Yes | Multi-page apps |
667
- | `:modulepreload` | Excellent | Advanced | Yes | Modern ES modules |
668
- | `:lazy` | Variable | Advanced | Yes | Below-fold content |
669
- | `:link` | Manual | Advanced | Yes | Custom implementations |
670
-
671
- ### API Endpoint Setup
672
-
673
- For link-based strategies, you'll need to set up API endpoints in your application:
714
+ class ApplicationController < ActionController::Base
715
+ after_action :set_csp_header
674
716
 
675
- ```ruby
676
- # Rails example
677
- class HydrationController < ApplicationController
678
- def show
679
- template_name = params[:template]
680
- endpoint = Rhales::HydrationEndpoint.new(rhales_config, current_context)
681
-
682
- case request.format
683
- when :json
684
- result = endpoint.render_json(template_name)
685
- when :js
686
- result = endpoint.render_module(template_name)
687
- else
688
- result = endpoint.render_json(template_name)
689
- end
717
+ private
690
718
 
691
- render json: result[:content],
692
- content_type: result[:content_type],
693
- headers: result[:headers]
719
+ def set_csp_header
720
+ csp_header = request.env['csp_header']
721
+ response.headers['Content-Security-Policy'] = csp_header if csp_header
694
722
  end
695
723
  end
696
-
697
- # routes.rb
698
- get '/api/hydration/:template', to: 'hydration#show'
699
- get '/api/hydration/:template.js', to: 'hydration#show', defaults: { format: :js }
700
724
  ```
701
725
 
702
- #### Advanced Non-Rails Example
703
-
704
- For applications using custom frameworks or middleware, here's a complete controller implementation from a production Rack-based application:
726
+ #### Roda
705
727
 
706
728
  ```ruby
707
- # Example from OneTime Secret (non-Rails application)
708
- module Manifold
709
- module Controllers
710
- class Data
711
- include Controllers::Base
712
-
713
- def rhales_hydration
714
- publically do
715
- template_name = params[:template]
716
-
717
- # Build Rhales context from your app's current state
718
- context = Rhales::Context.for_view(req, sess, cust, locale)
719
- endpoint = Rhales::HydrationEndpoint.new(Rhales.configuration, context)
720
-
721
- # Handle different formats
722
- result = case req.env['HTTP_ACCEPT']
723
- when /application\/javascript/
724
- endpoint.render_module(template_name)
725
- else
726
- endpoint.render_json(template_name)
727
- end
728
-
729
- # Set response content type and headers
730
- res['Content-Type'] = result[:content_type]
731
- result[:headers]&.each { |key, value| res[key] = value }
732
-
733
- # Return the content
734
- res.body = result[:content]
735
- end
736
- end
737
- end
729
+ class App < Roda
730
+ def render_with_csp(template_name, **data)
731
+ result = render_rhales(template_name, **data)
732
+ csp_header = request.env['csp_header']
733
+ response.headers['Content-Security-Policy'] = csp_header if csp_header
734
+ result
738
735
  end
739
736
  end
740
-
741
- # Route configuration (framework-specific)
742
- # GET /api/hydration/:template -> data#rhales_hydration
743
737
  ```
744
738
 
745
- **Key differences from Rails:**
746
- - **Framework-agnostic**: Uses `req`, `res`, `sess`, `cust`, `locale` from your framework
747
- - **Context creation**: Manually creates `Rhales::Context.for_view` with app objects
748
- - **Global configuration**: Uses `Rhales.configuration` instead of passing custom config
749
- - **Direct response handling**: Sets headers and body directly instead of using `render`
750
- - **Custom format detection**: Uses `req.env['HTTP_ACCEPT']` instead of `request.format`
739
+ ## Logging
751
740
 
752
- ### Data Hydration Examples
741
+ Rhales provides production logging for security auditing and debugging:
753
742
 
754
- #### Traditional Inline Hydration
755
- ```erb
756
- <data window="myData">
757
- {
758
- "apiUrl": "{{api_base_url}}",
759
- "user": {{user}},
760
- "csrfToken": "{{csrf_token}}"
761
- }
762
- </data>
763
- ```
764
-
765
- Generates:
766
- ```html
767
- <script id="rsfc-data-abc123" type="application/json">
768
- {"apiUrl":"https://api.example.com","user":{"id":123},"csrfToken":"token"}
769
- </script>
770
- <script nonce="nonce123">
771
- window.myData = JSON.parse(document.getElementById('rsfc-data-abc123').textContent);
772
- </script>
773
- ```
774
-
775
- #### Link-Based Hydration (`:preload` strategy)
776
- ```erb
777
- <data window="myData">
778
- {
779
- "apiUrl": "{{api_base_url}}",
780
- "user": {{user}},
781
- "csrfToken": "{{csrf_token}}"
782
- }
783
- </data>
743
+ ```ruby
744
+ # Configure logger
745
+ Rhales.logger = Rails.logger # or Logger.new($stdout)
784
746
  ```
785
747
 
786
- Generates:
787
- ```html
788
- <link rel="preload" href="/api/hydration/my_template" as="fetch" crossorigin>
789
- <script nonce="nonce123" data-hydration-target="myData">
790
- fetch('/api/hydration/my_template')
791
- .then(r => r.json())
792
- .then(data => {
793
- window.myData = data;
794
- window.dispatchEvent(new CustomEvent('rhales:hydrated', {
795
- detail: { target: 'myData', data: data }
796
- }));
797
- })
798
- .catch(err => console.error('Rhales hydration error:', err));
799
- </script>
800
- ```
748
+ **Logged Events:**
749
+ - View rendering (template, layout, partials, timing, hydration size)
750
+ - Security warnings (unescaped variables, schema mismatches)
751
+ - Errors with context (line numbers, sections, full messages)
752
+ - Performance insights (cache hits, compilation timing)
801
753
 
802
- #### ES Module Hydration (`:modulepreload` strategy)
803
- ```html
804
- <link rel="modulepreload" href="/api/hydration/my_template.js">
805
- <script type="module" nonce="nonce123" data-hydration-target="myData">
806
- import data from '/api/hydration/my_template.js';
807
- window.myData = data;
808
- window.dispatchEvent(new CustomEvent('rhales:hydrated', {
809
- detail: { target: 'myData', data: data }
810
- }));
811
- </script>
754
+ ```ruby
755
+ # Example log output
756
+ INFO View rendered: template=dashboard layout=main partials=[header,footer] duration_ms=15.2
757
+ WARN Hydration schema mismatch: template=user_profile missing=[email] extra=[]
758
+ ERROR Template not found: template=missing_partial parent=dashboard
759
+ DEBUG Template cache hit: template=header
812
760
  ```
813
761
 
814
- ### Migration Guide
815
-
816
- #### From Basic to Enhanced Hydration
762
+ ## Testing
817
763
 
818
- **Step 1**: Update your configuration to use enhanced strategies:
764
+ ### Test Configuration
819
765
 
820
766
  ```ruby
821
- # Before (implicit :late strategy)
822
- Rhales.configure do |config|
823
- # Basic configuration
824
- end
767
+ # test/test_helper.rb or spec/spec_helper.rb
768
+ require 'rhales'
825
769
 
826
- # After (explicit strategy selection)
827
770
  Rhales.configure do |config|
828
- # Choose your strategy based on your needs
829
- config.hydration.injection_strategy = :preload # or :early, :earliest, etc.
830
- config.hydration.fallback_to_late = true # Safe fallback
831
- config.hydration.api_endpoint_path = '/api/hydration'
832
- config.hydration.api_cache_enabled = true
771
+ config.default_locale = 'en'
772
+ config.app_environment = 'test'
773
+ config.cache_templates = false
774
+ config.template_paths = ['test/fixtures/templates']
833
775
  end
834
776
  ```
835
777
 
836
- **Step 2**: Set up API endpoints for link-based strategies (if using `:preload`, `:prefetch`, `:modulepreload`, `:lazy`, or `:link`):
778
+ ### Testing Context
837
779
 
838
780
  ```ruby
839
- # Add to your routes
840
- get '/api/hydration/:template', to: 'hydration#show'
841
- get '/api/hydration/:template.js', to: 'hydration#show', defaults: { format: :js }
842
-
843
- # Create controller
844
- class HydrationController < ApplicationController
845
- def show
846
- template_name = params[:template]
847
- endpoint = Rhales::HydrationEndpoint.new(rhales_config, current_context)
848
- result = endpoint.render_json(template_name)
781
+ # Minimal context for testing
782
+ context = Rhales::Context.minimal(
783
+ client: { user: 'Test' },
784
+ server: { page_title: 'Test Page' }
785
+ )
849
786
 
850
- render json: result[:content],
851
- content_type: result[:content_type],
852
- headers: result[:headers]
853
- end
854
- end
787
+ expect(context.get('user')).to eq('Test')
788
+ expect(context.get('page_title')).to eq('Test Page')
855
789
  ```
856
790
 
857
- **Step 3**: Update your frontend code to listen for hydration events (optional):
791
+ ### Testing Templates
858
792
 
859
- ```javascript
860
- // Listen for hydration completion
861
- window.addEventListener('rhales:hydrated', (event) => {
862
- console.log('Data loaded:', event.detail.target, event.detail.data);
793
+ ```ruby
794
+ # Test inline template
795
+ template = '{{#if active}}Active{{else}}Inactive{{/if}}'
796
+ result = Rhales.render_template(template, active: true)
797
+ expect(result).to eq('Active')
863
798
 
864
- // Initialize your app with the loaded data
865
- if (event.detail.target === 'appData') {
866
- initializeApp(event.detail.data);
867
- }
868
- });
799
+ # Test .rue file
800
+ mock_request = OpenStruct.new(env: {})
801
+ view = Rhales::View.new(mock_request, client: { message: 'Hello' })
802
+ html = view.render('test_template')
803
+ expect(html).to include('Hello')
869
804
  ```
870
805
 
871
- ### Troubleshooting
872
-
873
- #### Common Issues
874
-
875
- **1. Link-based strategies not working**
876
- - Ensure API endpoints are set up correctly
877
- - Check that `config.hydration.api_endpoint_path` matches your routes
878
- - Verify CORS settings if loading from different domains
879
-
880
- **2. Mount points not detected with `:early` strategy**
881
- - Check that your HTML contains valid mount point selectors (`#app`, `#root`, etc.)
882
- - Verify `config.hydration.mount_point_selectors` includes your selectors
883
- - Enable fallback: `config.hydration.fallback_to_late = true`
806
+ ## Migration from v0.4 to v0.5
884
807
 
885
- **3. CSP violations with link-based strategies**
886
- - Ensure nonces are properly configured: `config.auto_nonce = true`
887
- - Add API endpoint domains to CSP `connect-src` directive
888
- - Check that `crossorigin` attribute is properly configured
808
+ ### Breaking Changes
889
809
 
890
- **4. Performance not improving with advanced strategies**
891
- - Verify browser support for chosen strategy (modulepreload requires modern browsers)
892
- - Check network timing in DevTools to confirm parallel loading
893
- - Consider using `:prefetch` for subsequent page loads vs `:preload` for current page
810
+ 1. **`<data>` sections removed** Use `<schema>` sections
811
+ 2. **Parameters removed:**
812
+ - `sess` Access via `request.session`
813
+ - `cust` Access via `request.user`
814
+ - `props:` → Use `client:`
815
+ - `app_data:` → Use `server:`
816
+ - `locale` → Set via `request.env['rhales.locale']`
817
+ 3. **Context layer renamed:** `app` → `request`
894
818
 
895
- **5. Hydration events not firing**
896
- - Ensure JavaScript is not blocked by CSP
897
- - Check browser console for script errors
898
- - Verify API endpoints return valid JSON responses
819
+ ### Migration Steps
899
820
 
900
- ### Window Collision Detection
821
+ #### 1. Update Ruby Code
901
822
 
902
- Rhales automatically detects when multiple templates try to use the same window attribute, preventing silent data overwrites:
903
-
904
- ```erb
905
- <!-- layouts/main.rue -->
906
- <data window="appData">
907
- {"user": "{{user.name}}", "csrf": "{{csrf_token}}"}
908
- </data>
909
-
910
- <!-- pages/home.rue -->
911
- <data window="appData"> <!-- ❌ Collision detected! -->
912
- {"page": "home", "features": ["feature1"]}
913
- </data>
914
- ```
915
-
916
- This raises a helpful error:
917
- ```
918
- Window attribute collision detected
823
+ ```ruby
824
+ # v0.4 (REMOVED)
825
+ view = Rhales::View.new(req, session, customer, 'en',
826
+ props: { user: customer.name },
827
+ app_data: { page_title: 'Dashboard' }
828
+ )
919
829
 
920
- Attribute: 'appData'
921
- First defined: layouts/main.rue:1
922
- Conflict with: pages/home.rue:1
830
+ # v0.5 (Current)
831
+ view = Rhales::View.new(req,
832
+ client: { user: customer.name },
833
+ server: { page_title: 'Dashboard' }
834
+ )
923
835
 
924
- Quick fixes:
925
- 1. Rename one: <data window="homeData">
926
- 2. Enable merging: <data window="appData" merge="deep">
836
+ # Set locale in request
837
+ req.env['rhales.locale'] = 'en'
927
838
  ```
928
839
 
929
- ### Merge Strategies
840
+ #### 2. Convert Data to Schema
930
841
 
931
- When you intentionally want to share data between templates, use explicit merge strategies:
932
-
933
- ```erb
934
- <!-- layouts/main.rue -->
935
- <data window="appData">
842
+ ```xml
843
+ <!-- v0.4 (REMOVED) -->
844
+ <data window="data">
936
845
  {
937
- "user": {"name": "{{user.name}}", "role": "{{user.role}}"},
938
- "csrf": "{{csrf_token}}"
846
+ "user": "{{user.name}}",
847
+ "count": {{items.count}}
939
848
  }
940
849
  </data>
941
850
 
942
- <!-- pages/home.rue with deep merge -->
943
- <data window="appData" merge="deep">
944
- {
945
- "user": {"email": "{{user.email}}"}, <!-- Merged with layout user -->
946
- "page": {"title": "Home", "features": {{features.to_json}}}
947
- }
948
- </data>
851
+ <!-- v0.5 (Current) -->
852
+ <schema lang="js-zod" window="data">
853
+ const schema = z.object({
854
+ user: z.string(),
855
+ count: z.number()
856
+ });
857
+ </schema>
949
858
  ```
950
859
 
951
- #### Available Merge Strategies
860
+ **Key difference:** In v0.5, pass resolved values in `client:` hash instead of relying on template interpolation in JSON.
952
861
 
953
- **`merge="shallow"`** - Top-level key merge, throws error on conflicts:
954
- ```javascript
955
- // Layout: {"user": {...}, "csrf": "abc"}
956
- // Page: {"page": {...}, "user": {...}} // ❌ Error: key conflict
957
- ```
862
+ #### 3. Update Context References
958
863
 
959
- **`merge="deep"`** - Recursive merge, last value wins on conflicts:
960
- ```javascript
961
- // Layout: {"user": {"name": "John", "role": "admin"}}
962
- // Page: {"user": {"email": "john@example.com"}}
963
- // Result: {"user": {"name": "John", "role": "admin", "email": "john@example.com"}}
964
- ```
864
+ ```handlebars
865
+ <!-- v0.4 (REMOVED) -->
866
+ {{app.nonce}}
867
+ {{app.csrf_token}}
965
868
 
966
- **`merge="strict"`** - Recursive merge, throws error on any conflict:
967
- ```javascript
968
- // Layout: {"user": {"name": "John"}}
969
- // Page: {"user": {"name": "Jane"}} // ❌ Error: value conflict
869
+ <!-- v0.5 (Current) -->
870
+ {{request.nonce}}
871
+ {{request.csrf_token}}
970
872
  ```
971
873
 
972
- ## Content Security Policy (CSP)
973
-
974
- Rhales provides **security by default** with automatic CSP header generation and nonce management.
975
-
976
- ### Automatic CSP Protection
977
-
978
- CSP is **enabled by default** when you configure Rhales:
874
+ #### 4. Update Backend Data Passing
979
875
 
980
876
  ```ruby
981
- Rhales.configure do |config|
982
- # CSP is enabled by default with secure settings
983
- config.csp_enabled = true # Default: true
984
- config.auto_nonce = true # Default: true
985
- end
986
- ```
987
-
988
- ### Default Security Policy
989
-
990
- Rhales ships with a secure default CSP policy:
877
+ # v0.4: Template interpolation
878
+ view = Rhales::View.new(req, sess, cust, 'en',
879
+ props: { user: cust } # Object reference, interpolated in <data>
880
+ )
991
881
 
992
- ```ruby
993
- {
994
- 'default-src' => ["'self'"],
995
- 'script-src' => ["'self'", "'nonce-{{nonce}}'"],
996
- 'style-src' => ["'self'", "'nonce-{{nonce}}'", "'unsafe-hashes'"],
997
- 'img-src' => ["'self'", 'data:'],
998
- 'font-src' => ["'self'"],
999
- 'connect-src' => ["'self'"],
1000
- 'base-uri' => ["'self'"],
1001
- 'form-action' => ["'self'"],
1002
- 'frame-ancestors' => ["'none'"],
1003
- 'object-src' => ["'none'"],
1004
- 'upgrade-insecure-requests' => []
1005
- }
882
+ # v0.5: Resolved values upfront
883
+ view = Rhales::View.new(req,
884
+ client: {
885
+ user: cust.name, # Resolved value
886
+ userId: cust.id # Resolved value
887
+ }
888
+ )
1006
889
  ```
1007
890
 
1008
- ### Automatic Nonce Generation
891
+ ## Performance Optimization
1009
892
 
1010
- Rhales automatically generates and manages CSP nonces:
893
+ ### Optional: Oj for Faster JSON Processing
1011
894
 
1012
- ```erb
1013
- <!-- In your .rue templates -->
1014
- <script nonce="{{app.nonce}}">
1015
- // Your inline JavaScript with automatic nonce
1016
- console.log('Secure script execution');
1017
- </script>
895
+ Rhales includes optional support for [Oj](https://github.com/ohler55/oj), a high-performance JSON library that provides:
1018
896
 
1019
- <style nonce="{{app.nonce}}">
1020
- /* Your inline styles with automatic nonce */
1021
- .component { color: blue; }
1022
- </style>
1023
- ```
897
+ - **10-20x faster JSON parsing** compared to stdlib
898
+ - **5-10x faster JSON generation** compared to stdlib
899
+ - **Lower memory usage** for large data payloads
900
+ - **Full compatibility** with stdlib JSON API
1024
901
 
1025
- ### Framework Integration
902
+ #### Installation
1026
903
 
1027
- CSP headers are automatically set during view rendering:
904
+ Add to your Gemfile:
1028
905
 
1029
906
  ```ruby
1030
- # Your framework code (Rails, Sinatra, Roda, etc.)
1031
- def dashboard
1032
- view = Rhales::View.new(request, session, current_user, 'en')
1033
- html = view.render('dashboard', user: current_user)
1034
-
1035
- # CSP header automatically added to response:
1036
- # Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; ...
1037
- end
907
+ gem 'oj', '~> 3.13'
1038
908
  ```
1039
909
 
1040
- ### Custom CSP Policies
1041
-
1042
- Customize the CSP policy for your specific needs:
910
+ Then run:
1043
911
 
1044
- ```ruby
1045
- Rhales.configure do |config|
1046
- config.csp_policy = {
1047
- 'default-src' => ["'self'"],
1048
- 'script-src' => ["'self'", "'nonce-{{nonce}}'", 'https://cdn.example.com'],
1049
- 'style-src' => ["'self'", "'nonce-{{nonce}}'", 'https://fonts.googleapis.com'],
1050
- 'img-src' => ["'self'", 'data:', 'https://images.example.com'],
1051
- 'connect-src' => ["'self'", 'https://api.example.com'],
1052
- 'font-src' => ["'self'", 'https://fonts.gstatic.com'],
1053
- # Add your own directives...
1054
- }
1055
- end
912
+ ```bash
913
+ bundle install
1056
914
  ```
1057
915
 
1058
- ### Per-Framework CSP Setup
1059
-
1060
- #### Rails
1061
- ```ruby
1062
- # app/controllers/application_controller.rb
1063
- class ApplicationController < ActionController::Base
1064
- after_action :set_csp_header
1065
-
1066
- private
916
+ That's it! Rhales automatically detects Oj at load time and uses it for all JSON operations.
1067
917
 
1068
- def set_csp_header
1069
- csp_header = request.env['csp_header']
1070
- response.headers['Content-Security-Policy'] = csp_header if csp_header
1071
- end
1072
- end
1073
- ```
1074
-
1075
- #### Sinatra
1076
- ```ruby
1077
- helpers do
1078
- def render_with_csp(template_name, data = {})
1079
- result = render_rhales(template_name, data)
1080
- csp_header = request.env['csp_header']
1081
- headers['Content-Security-Policy'] = csp_header if csp_header
1082
- result
1083
- end
1084
- end
1085
- ```
918
+ **Note:** The backend is selected once when Rhales loads. To ensure Oj is used, require it before Rhales:
1086
919
 
1087
- #### Roda
1088
920
  ```ruby
1089
- class App < Roda
1090
- def render_with_csp(template_name, data = {})
1091
- result = render_rhales(template_name, data)
1092
- csp_header = request.env['csp_header']
1093
- response.headers['Content-Security-Policy'] = csp_header if csp_header
1094
- result
1095
- end
1096
- end
921
+ # Gemfile or application initialization
922
+ require 'oj' # Load Oj first
923
+ require 'rhales' # Rhales will detect and use Oj
1097
924
  ```
1098
925
 
1099
- ### CSP Benefits
926
+ Most bundler setups handle this automatically, but explicit ordering ensures optimal performance.
1100
927
 
1101
- - **Security by default**: Protection against XSS attacks out of the box
1102
- - **Automatic nonce management**: No manual nonce coordination needed
1103
- - **Template integration**: Nonces automatically available in templates
1104
- - **Framework agnostic**: Works with any Ruby web framework
1105
- - **Customizable policies**: Adapt CSP rules to your application needs
1106
- - **Zero configuration**: Secure defaults work immediately
928
+ #### Verification
1107
929
 
1108
- ### Disabling CSP
1109
-
1110
- If you need to disable CSP for specific environments:
930
+ Check which backend is active:
1111
931
 
1112
932
  ```ruby
1113
- Rhales.configure do |config|
1114
- config.csp_enabled = false # Disable CSP header generation
1115
- config.auto_nonce = false # Disable automatic nonce generation
1116
- end
933
+ Rhales::JSONSerializer.backend
934
+ # => :oj (if available) or :json (stdlib)
1117
935
  ```
1118
936
 
1119
- ## Testing
1120
-
1121
- Rhales includes comprehensive test helpers and is framework-agnostic:
1122
-
1123
- ```ruby
1124
- # test/test_helper.rb or spec/spec_helper.rb
1125
- require 'rhales'
937
+ #### Performance Impact
1126
938
 
1127
- Rhales.configure do |config|
1128
- config.default_locale = 'en'
1129
- config.app_environment = 'test'
1130
- config.cache_templates = false
1131
- config.template_paths = ['test/templates'] # or wherever your test templates are
1132
- end
939
+ For typical Rhales applications with hydration data:
1133
940
 
1134
- # Test context creation
1135
- context = Rhales::Context.minimal(props: { user: { name: 'Test' } })
1136
- expect(context.get('user.name')).to eq('Test')
941
+ | Operation | stdlib JSON | Oj | Improvement |
942
+ |-----------|-------------|-----|-------------|
943
+ | Parse 100KB payload | ~50ms | ~3ms | **16x faster** |
944
+ | Generate 100KB payload | ~30ms | ~5ms | **6x faster** |
945
+ | Memory usage | Baseline | -20% | **Lower** |
1137
946
 
1138
- # Test template rendering
1139
- template = '{{#if authenticated}}Welcome{{/if}}'
1140
- result = Rhales.render_template(template, authenticated: true)
1141
- expect(result).to eq('Welcome')
947
+ **Recommendation:** Install Oj for production applications with:
948
+ - Large hydration payloads (>10KB)
949
+ - High-traffic endpoints (>100 req/sec)
950
+ - Complex nested data structures
1142
951
 
1143
- # Test full template files
1144
- mock_request = OpenStruct.new(env: {})
1145
- mock_session = {}
1146
- view = Rhales::View.new(mock_request, mock_session, nil, 'en')
1147
- html = view.render('test_template', message: 'Hello World')
1148
- ```
952
+ Oj provides the most benefit for data-heavy templates and high-concurrency scenarios.
1149
953
 
1150
954
  ## Development
1151
955
 
1152
- After checking out the repo, run:
1153
-
1154
956
  ```bash
957
+ # Clone repository
958
+ git clone https://github.com/onetimesecret/rhales.git
959
+ cd rhales
960
+
961
+ # Install dependencies
1155
962
  bundle install
1156
- bundle exec rspec
963
+
964
+ # Run tests
965
+ bundle exec rspec spec/rhales/
966
+
967
+ # Run with documentation format
968
+ bundle exec rspec spec/rhales/ --format documentation
969
+
970
+ # Build gem
971
+ gem build rhales.gemspec
972
+
973
+ # Install locally
974
+ gem install ./rhales-0.5.0.gem
1157
975
  ```
1158
976
 
1159
977
  ## Contributing
1160
978
 
1161
- 1. Fork it
979
+ 1. Fork it (https://github.com/onetimesecret/rhales/fork)
1162
980
  2. Create your feature branch (`git checkout -b my-new-feature`)
1163
981
  3. Commit your changes (`git commit -am 'Add some feature'`)
1164
982
  4. Push to the branch (`git push origin my-new-feature`)
@@ -1170,11 +988,11 @@ The gem is available as open source under the [MIT License](https://opensource.o
1170
988
 
1171
989
  ## AI Development Assistance
1172
990
 
1173
- Rhales was developed with assistance from AI tools. The following tools provided significant help with architecture design, code generation, and documentation:
991
+ Rhales was developed with assistance from AI tools:
1174
992
 
1175
- - **Claude Sonnet 4** - Architecture design, code generation, and documentation
1176
- - **Claude Desktop & Claude Code** - Interactive development sessions and debugging
1177
- - **GitHub Copilot** - Code completion and refactoring assistance
993
+ - **Claude Sonnet 4.5** - Architecture design, code generation, documentation
994
+ - **Claude Desktop & Claude Code** - Interactive development and debugging
995
+ - **GitHub Copilot** - Code completion and refactoring
1178
996
  - **Qodo Merge Pro** - Code review and quality improvements
1179
997
 
1180
- I remain responsible for all design decisions and the final code. I believe in being transparent about development tools, especially as AI becomes more integrated into our workflows.
998
+ I remain responsible for all design decisions and the final code. Being transparent about development tools as AI becomes more integrated into our workflows.