rhales 0.3.0

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.
data/README.md ADDED
@@ -0,0 +1,881 @@
1
+ # Rhales - Ruby Single File Components
2
+
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.
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.
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.
10
+
11
+ ## Features
12
+
13
+ - **Server-side template rendering** with Handlebars-style syntax
14
+ - **Client-side data hydration** with secure JSON injection
15
+ - **Window collision detection** prevents silent data overwrites
16
+ - **Explicit merge strategies** for controlled data sharing (shallow, deep, strict)
17
+ - **Clear security boundaries** between server context and client data
18
+ - **Partial support** for component composition
19
+ - **Pluggable authentication adapters** for any auth system
20
+ - **Security-first design** with XSS protection and automatic CSP generation
21
+ - **Dependency injection** for testability and flexibility
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'rhales'
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ ```bash
34
+ bundle install
35
+ ```
36
+
37
+ Or install it yourself as:
38
+
39
+ ```bash
40
+ gem install rhales
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### 1. Configure Rhales
46
+
47
+ ```ruby
48
+ # Configure Rhales in your application initialization
49
+ Rhales.configure do |config|
50
+ config.default_locale = 'en'
51
+ config.template_paths = ['templates'] # or 'app/templates', 'views/templates', etc.
52
+ config.features = { dark_mode: true }
53
+ config.site_host = 'example.com'
54
+
55
+ # CSP configuration (enabled by default)
56
+ config.csp_enabled = true # Enable automatic CSP header generation
57
+ config.auto_nonce = true # Automatically generate nonces
58
+ config.csp_policy = { # Customize CSP policy (optional)
59
+ 'default-src' => ["'self'"],
60
+ 'script-src' => ["'self'", "'nonce-{{nonce}}'"],
61
+ 'style-src' => ["'self'", "'nonce-{{nonce}}'", "'unsafe-hashes'"]
62
+ # ... more directives
63
+ }
64
+ end
65
+ ```
66
+
67
+ ### 2. Create a Simple Component
68
+
69
+ Create a `.rue` file in your templates directory:
70
+
71
+ ```rue
72
+ <!-- templates/hello.rue -->
73
+ <data>
74
+ {
75
+ "greeting": "{{greeting}}",
76
+ "user_name": "{{user.name}}"
77
+ }
78
+ </data>
79
+
80
+ <template>
81
+ <div class="hello-component">
82
+ <h1>{{greeting}}, {{user.name}}!</h1>
83
+ <p>Welcome to Rhales RSFC!</p>
84
+ </div>
85
+ </template>
86
+
87
+ <logic>
88
+ # Simple greeting component
89
+ </logic>
90
+ ```
91
+
92
+ ### 3. Render in Your Application
93
+
94
+ ```ruby
95
+ # In your controller/route handler
96
+ view = Rhales::View.new(request, session, user, 'en',
97
+ props: {
98
+ greeting: 'Hello',
99
+ user: { name: 'World' }
100
+ }
101
+ )
102
+
103
+ html = view.render('hello')
104
+ # Returns HTML with embedded JSON for client-side hydration
105
+ ```
106
+
107
+ ## Context and Data Model
108
+
109
+ Rhales uses a **two-layer data model** for template rendering:
110
+
111
+ ### 1. App Data (Framework Layer)
112
+ All framework-provided data is available under the `app` namespace:
113
+
114
+ ```handlebars
115
+ <!-- Framework data through app namespace -->
116
+ {{app.csrf_token}} <!-- CSRF token for forms -->
117
+ {{app.nonce}} <!-- CSP nonce for scripts -->
118
+ {{app.authenticated}} <!-- Authentication state -->
119
+ {{app.environment}} <!-- Current environment -->
120
+ {{app.features.dark_mode}} <!-- Feature flags -->
121
+ {{app.theme_class}} <!-- Current theme -->
122
+ ```
123
+
124
+ **Available App Variables:**
125
+ - `app.api_base_url` - Base URL for API calls
126
+ - `app.authenticated` - Whether user is authenticated
127
+ - `app.csrf_token` - CSRF token for forms
128
+ - `app.development` - Whether in development mode
129
+ - `app.environment` - Current environment (production/staging/dev)
130
+ - `app.features` - Feature flags hash
131
+ - `app.nonce` - CSP nonce for inline scripts
132
+ - `app.theme_class` - Current theme CSS class
133
+
134
+ ### 2. Props Data (Application Layer)
135
+ Your application-specific data passed to each view:
136
+
137
+ ```handlebars
138
+ <!-- Application data -->
139
+ {{user.name}} <!-- Direct access -->
140
+ {{page_title}} <!-- Props take precedence -->
141
+ {{#if user.admin?}}
142
+ <a href="/admin">Admin Panel</a>
143
+ {{/if}}
144
+ ```
145
+
146
+ ### Server Templates: Full Context Access
147
+ Templates have complete access to all server-side data:
148
+ - User objects and authentication state
149
+ - Database connections and internal APIs
150
+ - Configuration values and secrets
151
+ - Request metadata (CSRF tokens, nonces)
152
+
153
+ ```handlebars
154
+ <!-- Full server access in templates -->
155
+ {{#if user.admin?}}
156
+ <a href="/admin">Admin Panel</a>
157
+ {{/if}}
158
+ <div class="{{app.theme_class}}">{{user.full_name}}</div>
159
+ ```
160
+
161
+ ### Client Data: Explicit Allowlist
162
+ Only data declared in `<data>` sections reaches the browser:
163
+
164
+ ```rue
165
+ <data>
166
+ {
167
+ "display_name": "{{user.name}}",
168
+ "preferences": {
169
+ "theme": "{{user.theme}}"
170
+ }
171
+ }
172
+ </data>
173
+ ```
174
+
175
+ Becomes:
176
+ ```javascript
177
+ window.data = {
178
+ "display_name": "John Doe",
179
+ "preferences": { "theme": "dark" }
180
+ }
181
+ // No access to user.admin?, internal APIs, etc.
182
+ ```
183
+
184
+ This creates a **REST API-like boundary** where you explicitly declare what data crosses the server-to-client security boundary.
185
+
186
+ For complete details, see [Context and Data Boundaries Documentation](docs/CONTEXT_AND_DATA_BOUNDARIES.md).
187
+ config.site_ssl_enabled = true
188
+ end
189
+ ```
190
+
191
+ ### 2. Create a .rue file
192
+
193
+ Create `templates/dashboard.rue` - notice how the `<data>` section defines exactly what your frontend app will receive:
194
+
195
+ ```xml
196
+ <data window="appState" schema="@/src/types/app-state.d.ts">
197
+ {
198
+ "user": {
199
+ "id": "{{user.id}}",
200
+ "name": "{{user.name}}",
201
+ "email": "{{user.email}}",
202
+ "preferences": {{user.preferences}}
203
+ },
204
+ "products": {{recent_products}},
205
+ "cart": {
206
+ "items": {{cart.items}},
207
+ "total": "{{cart.total}}"
208
+ },
209
+ "api": {
210
+ "baseUrl": "{{api_base_url}}",
211
+ "csrfToken": "{{csrf_token}}"
212
+ },
213
+ "features": {{enabled_features}}
214
+ }
215
+ </data>
216
+
217
+ <template>
218
+ <!doctype html>
219
+ <html lang="{{locale}}" class="{{theme_class}}">
220
+ <head>
221
+ <title>{{page_title}}</title>
222
+ <meta name="csrf-token" content="{{csrf_token}}">
223
+ </head>
224
+ <body>
225
+ <!-- Critical: The mounting point for your frontend framework -->
226
+ <div id="app">
227
+ <!-- Server-rendered content for SEO and initial load -->
228
+ <nav>{{> navigation}}</nav>
229
+ <main>
230
+ <h1>{{page_title}}</h1>
231
+ {{#if user}}
232
+ <p>Welcome back, {{user.name}}!</p>
233
+ {{else}}
234
+ <p>Please sign in to continue.</p>
235
+ {{/if}}
236
+ </main>
237
+ </div>
238
+
239
+ <!--
240
+ RSFC automatically generates hydration scripts:
241
+ - <script type="application/json" id="app-state-data">{...}</script>
242
+ - <script>window.appState = JSON.parse(...);</script>
243
+ -->
244
+
245
+ <!-- Your frontend framework takes over from here -->
246
+ <script nonce="{{nonce}}" type="module" src="/assets/app.js"></script>
247
+ </body>
248
+ </html>
249
+ </template>
250
+ ```
251
+
252
+ ### 3. The Manifold: Server-to-SPA Handoff
253
+
254
+ This example demonstrates Rhales' core value proposition: **eliminating the coordination gap** between server state and frontend frameworks.
255
+
256
+ **What you get:**
257
+ - ✅ **Declarative data contract**: The `<data>` section explicitly defines what your frontend receives
258
+ - ✅ **Type safety ready**: Schema reference points to TypeScript definitions
259
+ - ✅ **Zero coordination overhead**: No separate API design needed for initial state
260
+ - ✅ **SEO + SPA**: Server-rendered HTML with automatic client hydration
261
+ - ✅ **Security boundaries**: Only explicitly declared data reaches the client
262
+
263
+ ### 4. RSFC Security Model
264
+
265
+ **Key Principle: The security boundary is at the server-to-client handoff, not within server-side rendering.**
266
+
267
+ ```xml
268
+ <data>
269
+ {
270
+ "message": "{{greeting}}", <!-- ✅ Exposed to client -->
271
+ "user": {
272
+ "name": "{{user.name}}" <!-- ✅ Exposed to client -->
273
+ }
274
+ <!-- ❌ user.secret_key not declared, won't reach client -->
275
+ }
276
+ </data>
277
+
278
+ <template>
279
+ <h1>{{greeting}}</h1> <!-- ✅ Full server context access -->
280
+ <p>{{user.name}}</p> <!-- ✅ Can access user object methods -->
281
+ <p>{{user.secret_key}}</p> <!-- ✅ Server-side only, not in <data> -->
282
+ </template>
283
+ ```
284
+
285
+ **Template Section (`<template>`):**
286
+ - ✅ **Full server context access** - like ERB, HAML, or any server-side template
287
+ - ✅ **Can call object methods** - `{{user.full_name}}`, `{{products.count}}`, etc.
288
+ - ✅ **Rich server-side logic** - access to full business objects and their capabilities
289
+ - ✅ **Private by default** - nothing in templates reaches the client unless explicitly declared
290
+
291
+ **Data Section (`<data>`):**
292
+ - ✅ **Explicit client allowlist** - only declared variables reach the browser
293
+ - ✅ **JSON serialization boundary** - like designing a REST API endpoint
294
+ - ✅ **Type safety foundation** - can validate against schemas
295
+ - ❌ **Cannot expose secrets** - `user.secret_key` won't reach client unless declared
296
+
297
+ This design gives you the flexibility of full server-side templating while maintaining explicit control over what data reaches the client.
298
+
299
+ **Generated output:**
300
+ ```html
301
+ <!-- Server-rendered HTML -->
302
+ <div id="app">
303
+ <nav>...</nav>
304
+ <main><h1>User Dashboard</h1><p>Welcome back, Alice!</p></main>
305
+ </div>
306
+
307
+ <!-- Automatic client hydration -->
308
+ <script id="app-state-data" type="application/json">
309
+ {"user":{"id":123,"name":"Alice","email":"alice@example.com","preferences":{...}},"products":[...],"cart":{...},"api":{...},"features":{...}}
310
+ </script>
311
+ <script nonce="abc123">
312
+ window.appState = JSON.parse(document.getElementById('app-state-data').textContent);
313
+ </script>
314
+
315
+ <!-- Your Vue/React/Svelte app mounts here with full state -->
316
+ <script nonce="abc123" type="module" src="/assets/app.js"></script>
317
+ ```
318
+
319
+ ### 5. Framework Integration
320
+
321
+ #### Rails
322
+
323
+ ```ruby
324
+ # config/initializers/rhales.rb
325
+ Rhales.configure do |config|
326
+ config.template_paths = ['app/templates']
327
+ config.default_locale = 'en'
328
+ end
329
+
330
+ # app/controllers/application_controller.rb
331
+ class ApplicationController < ActionController::Base
332
+ def render_rhales(template_name, data = {})
333
+ view = Rhales::View.new(request, session, current_user, I18n.locale)
334
+ view.render(template_name, data)
335
+ end
336
+ end
337
+
338
+ # In your controller
339
+ def dashboard
340
+ @dashboard_html = render_rhales('dashboard',
341
+ page_title: 'User Dashboard',
342
+ user: current_user,
343
+ recent_products: Product.recent.limit(5),
344
+ cart: current_user.cart,
345
+ enabled_features: Feature.enabled_for(current_user)
346
+ )
347
+ end
348
+ ```
349
+
350
+ #### Sinatra
351
+
352
+ ```ruby
353
+ # app.rb
354
+ require 'sinatra'
355
+ require 'rhales'
356
+
357
+ Rhales.configure do |config|
358
+ config.template_paths = ['templates']
359
+ config.default_locale = 'en'
360
+ end
361
+
362
+ helpers do
363
+ def render_rhales(template_name, data = {})
364
+ view = Rhales::View.new(request, session, current_user, 'en')
365
+ view.render(template_name, data)
366
+ end
367
+ end
368
+
369
+ get '/dashboard' do
370
+ @dashboard_html = render_rhales('dashboard',
371
+ page_title: 'Dashboard',
372
+ user: current_user,
373
+ recent_products: Product.recent,
374
+ cart: session[:cart] || {},
375
+ enabled_features: FEATURES
376
+ )
377
+ erb :dashboard
378
+ end
379
+ ```
380
+
381
+ #### Padrino
382
+
383
+ ```ruby
384
+ # config/apps.rb
385
+ Padrino.configure_apps do
386
+ Rhales.configure do |config|
387
+ config.template_paths = ['app/templates']
388
+ config.default_locale = 'en'
389
+ end
390
+ end
391
+
392
+ # app/helpers/application_helper.rb
393
+ module ApplicationHelper
394
+ def render_rhales(template_name, data = {})
395
+ view = Rhales::View.new(request, session, current_user, locale)
396
+ view.render(template_name, data)
397
+ end
398
+ end
399
+
400
+ # app/controllers/application_controller.rb
401
+ get :dashboard do
402
+ @dashboard_html = render_rhales('dashboard',
403
+ page_title: 'Dashboard',
404
+ user: current_user,
405
+ recent_products: Product.recent,
406
+ cart: current_user&.cart,
407
+ enabled_features: settings.features
408
+ )
409
+ render :dashboard
410
+ end
411
+ ```
412
+
413
+ #### Grape
414
+
415
+ ```ruby
416
+ # config.ru or initializer
417
+ require 'grape'
418
+ require 'rhales'
419
+
420
+ Rhales.configure do |config|
421
+ config.template_paths = ['templates']
422
+ config.default_locale = 'en'
423
+ end
424
+
425
+ # api.rb
426
+ class MyAPI < Grape::API
427
+ helpers do
428
+ def render_rhales(template_name, data = {})
429
+ # Create mock request/session for Grape
430
+ mock_request = OpenStruct.new(env: env)
431
+ mock_session = {}
432
+
433
+ view = Rhales::View.new(mock_request, mock_session, current_user, 'en')
434
+ view.render(template_name, data)
435
+ end
436
+ end
437
+
438
+ get '/dashboard' do
439
+ content_type 'text/html'
440
+ render_rhales('dashboard',
441
+ page_title: 'API Dashboard',
442
+ user: current_user,
443
+ recent_products: [],
444
+ cart: {},
445
+ enabled_features: { api_v2: true }
446
+ )
447
+ end
448
+ end
449
+ ```
450
+
451
+ #### Roda
452
+
453
+ ```ruby
454
+ # app.rb
455
+ require 'roda'
456
+ require 'rhales'
457
+
458
+ class App < Roda
459
+ plugin :render
460
+
461
+ Rhales.configure do |config|
462
+ config.template_paths = ['templates']
463
+ config.default_locale = 'en'
464
+ end
465
+
466
+ def render_rhales(template_name, data = {})
467
+ view = Rhales::View.new(request, session, current_user, 'en')
468
+ view.render(template_name, data)
469
+ end
470
+
471
+ route do |r|
472
+ r.on 'dashboard' do
473
+ @dashboard_html = render_rhales('dashboard',
474
+ page_title: 'Dashboard',
475
+ user: current_user,
476
+ recent_products: [],
477
+ cart: session[:cart],
478
+ enabled_features: FEATURES
479
+ )
480
+ view('dashboard')
481
+ end
482
+ end
483
+ end
484
+ ```
485
+
486
+ ### 6. Basic Usage
487
+
488
+ ```ruby
489
+ # Create a view instance
490
+ view = Rhales::View.new(request, session, current_user, locale)
491
+
492
+ # Render a template with rich data for frontend hydration
493
+ html = view.render('dashboard',
494
+ page_title: 'User Dashboard',
495
+ user: current_user,
496
+ recent_products: Product.recent.limit(5),
497
+ cart: current_user.cart,
498
+ enabled_features: Feature.enabled_for(current_user)
499
+ )
500
+
501
+ # Or use the convenience method
502
+ html = Rhales.render('dashboard',
503
+ request: request,
504
+ session: session,
505
+ user: current_user,
506
+ page_title: 'User Dashboard',
507
+ recent_products: products,
508
+ cart: cart_data,
509
+ enabled_features: features
510
+ )
511
+ ```
512
+
513
+ ## Authentication Adapters
514
+
515
+ Rhales supports pluggable authentication adapters. Implement the `Rhales::Adapters::BaseAuth` interface:
516
+
517
+ ```ruby
518
+ class MyAuthAdapter < Rhales::Adapters::BaseAuth
519
+ def initialize(user)
520
+ @user = user
521
+ end
522
+
523
+ def anonymous?
524
+ @user.nil?
525
+ end
526
+
527
+ def theme_preference
528
+ @user&.theme || 'light'
529
+ end
530
+
531
+ def user_id
532
+ @user&.id
533
+ end
534
+
535
+ def role?(role)
536
+ @user&.roles&.include?(role)
537
+ end
538
+ end
539
+
540
+ # Use with Rhales
541
+ user_adapter = MyAuthAdapter.new(current_user)
542
+ view = Rhales::View.new(request, session, user_adapter)
543
+ ```
544
+
545
+ ## Template Syntax
546
+
547
+ Rhales uses a Handlebars-style template syntax:
548
+
549
+ ### Variables
550
+ - `{{variable}}` - HTML-escaped output
551
+ - `{{{variable}}}` - Raw output (use carefully!)
552
+
553
+ ### Conditionals
554
+ ```erb
555
+ {{#if condition}}
556
+ Content when true
557
+ {{/if}}
558
+
559
+ {{#unless condition}}
560
+ Content when false
561
+ {{/unless}}
562
+ ```
563
+
564
+ ### Iteration
565
+ ```erb
566
+ {{#each items}}
567
+ <div>{{name}} - {{@index}}</div>
568
+ {{/each}}
569
+ ```
570
+
571
+ ### Partials
572
+ ```erb
573
+ {{> header}}
574
+ {{> navigation}}
575
+ ```
576
+
577
+ ## Data Hydration
578
+
579
+ The `<data>` section creates client-side JavaScript:
580
+
581
+ ```erb
582
+ <data window="myData">
583
+ {
584
+ "apiUrl": "{{api_base_url}}",
585
+ "user": {{user}},
586
+ "csrfToken": "{{csrf_token}}"
587
+ }
588
+ </data>
589
+ ```
590
+
591
+ Generates:
592
+ ```html
593
+ <script id="rsfc-data-abc123" type="application/json">
594
+ {"apiUrl":"https://api.example.com","user":{"id":123},"csrfToken":"token"}
595
+ </script>
596
+ <script nonce="nonce123">
597
+ window.myData = JSON.parse(document.getElementById('rsfc-data-abc123').textContent);
598
+ </script>
599
+ ```
600
+
601
+ ### Window Collision Detection
602
+
603
+ Rhales automatically detects when multiple templates try to use the same window attribute, preventing silent data overwrites:
604
+
605
+ ```erb
606
+ <!-- layouts/main.rue -->
607
+ <data window="appData">
608
+ {"user": "{{user.name}}", "csrf": "{{csrf_token}}"}
609
+ </data>
610
+
611
+ <!-- pages/home.rue -->
612
+ <data window="appData"> <!-- ❌ Collision detected! -->
613
+ {"page": "home", "features": ["feature1"]}
614
+ </data>
615
+ ```
616
+
617
+ This raises a helpful error:
618
+ ```
619
+ Window attribute collision detected
620
+
621
+ Attribute: 'appData'
622
+ First defined: layouts/main.rue:1
623
+ Conflict with: pages/home.rue:1
624
+
625
+ Quick fixes:
626
+ 1. Rename one: <data window="homeData">
627
+ 2. Enable merging: <data window="appData" merge="deep">
628
+ ```
629
+
630
+ ### Merge Strategies
631
+
632
+ When you intentionally want to share data between templates, use explicit merge strategies:
633
+
634
+ ```erb
635
+ <!-- layouts/main.rue -->
636
+ <data window="appData">
637
+ {
638
+ "user": {"name": "{{user.name}}", "role": "{{user.role}}"},
639
+ "csrf": "{{csrf_token}}"
640
+ }
641
+ </data>
642
+
643
+ <!-- pages/home.rue with deep merge -->
644
+ <data window="appData" merge="deep">
645
+ {
646
+ "user": {"email": "{{user.email}}"}, <!-- Merged with layout user -->
647
+ "page": {"title": "Home", "features": {{features.to_json}}}
648
+ }
649
+ </data>
650
+ ```
651
+
652
+ #### Available Merge Strategies
653
+
654
+ **`merge="shallow"`** - Top-level key merge, throws error on conflicts:
655
+ ```javascript
656
+ // Layout: {"user": {...}, "csrf": "abc"}
657
+ // Page: {"page": {...}, "user": {...}} // ❌ Error: key conflict
658
+ ```
659
+
660
+ **`merge="deep"`** - Recursive merge, last value wins on conflicts:
661
+ ```javascript
662
+ // Layout: {"user": {"name": "John", "role": "admin"}}
663
+ // Page: {"user": {"email": "john@example.com"}}
664
+ // Result: {"user": {"name": "John", "role": "admin", "email": "john@example.com"}}
665
+ ```
666
+
667
+ **`merge="strict"`** - Recursive merge, throws error on any conflict:
668
+ ```javascript
669
+ // Layout: {"user": {"name": "John"}}
670
+ // Page: {"user": {"name": "Jane"}} // ❌ Error: value conflict
671
+ ```
672
+
673
+ ## Content Security Policy (CSP)
674
+
675
+ Rhales provides **security by default** with automatic CSP header generation and nonce management.
676
+
677
+ ### Automatic CSP Protection
678
+
679
+ CSP is **enabled by default** when you configure Rhales:
680
+
681
+ ```ruby
682
+ Rhales.configure do |config|
683
+ # CSP is enabled by default with secure settings
684
+ config.csp_enabled = true # Default: true
685
+ config.auto_nonce = true # Default: true
686
+ end
687
+ ```
688
+
689
+ ### Default Security Policy
690
+
691
+ Rhales ships with a secure default CSP policy:
692
+
693
+ ```ruby
694
+ {
695
+ 'default-src' => ["'self'"],
696
+ 'script-src' => ["'self'", "'nonce-{{nonce}}'"],
697
+ 'style-src' => ["'self'", "'nonce-{{nonce}}'", "'unsafe-hashes'"],
698
+ 'img-src' => ["'self'", 'data:'],
699
+ 'font-src' => ["'self'"],
700
+ 'connect-src' => ["'self'"],
701
+ 'base-uri' => ["'self'"],
702
+ 'form-action' => ["'self'"],
703
+ 'frame-ancestors' => ["'none'"],
704
+ 'object-src' => ["'none'"],
705
+ 'upgrade-insecure-requests' => []
706
+ }
707
+ ```
708
+
709
+ ### Automatic Nonce Generation
710
+
711
+ Rhales automatically generates and manages CSP nonces:
712
+
713
+ ```erb
714
+ <!-- In your .rue templates -->
715
+ <script nonce="{{app.nonce}}">
716
+ // Your inline JavaScript with automatic nonce
717
+ console.log('Secure script execution');
718
+ </script>
719
+
720
+ <style nonce="{{app.nonce}}">
721
+ /* Your inline styles with automatic nonce */
722
+ .component { color: blue; }
723
+ </style>
724
+ ```
725
+
726
+ ### Framework Integration
727
+
728
+ CSP headers are automatically set during view rendering:
729
+
730
+ ```ruby
731
+ # Your framework code (Rails, Sinatra, Roda, etc.)
732
+ def dashboard
733
+ view = Rhales::View.new(request, session, current_user, 'en')
734
+ html = view.render('dashboard', user: current_user)
735
+
736
+ # CSP header automatically added to response:
737
+ # Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; ...
738
+ end
739
+ ```
740
+
741
+ ### Custom CSP Policies
742
+
743
+ Customize the CSP policy for your specific needs:
744
+
745
+ ```ruby
746
+ Rhales.configure do |config|
747
+ config.csp_policy = {
748
+ 'default-src' => ["'self'"],
749
+ 'script-src' => ["'self'", "'nonce-{{nonce}}'", 'https://cdn.example.com'],
750
+ 'style-src' => ["'self'", "'nonce-{{nonce}}'", 'https://fonts.googleapis.com'],
751
+ 'img-src' => ["'self'", 'data:', 'https://images.example.com'],
752
+ 'connect-src' => ["'self'", 'https://api.example.com'],
753
+ 'font-src' => ["'self'", 'https://fonts.gstatic.com'],
754
+ # Add your own directives...
755
+ }
756
+ end
757
+ ```
758
+
759
+ ### Per-Framework CSP Setup
760
+
761
+ #### Rails
762
+ ```ruby
763
+ # app/controllers/application_controller.rb
764
+ class ApplicationController < ActionController::Base
765
+ after_action :set_csp_header
766
+
767
+ private
768
+
769
+ def set_csp_header
770
+ csp_header = request.env['csp_header']
771
+ response.headers['Content-Security-Policy'] = csp_header if csp_header
772
+ end
773
+ end
774
+ ```
775
+
776
+ #### Sinatra
777
+ ```ruby
778
+ helpers do
779
+ def render_with_csp(template_name, data = {})
780
+ result = render_rhales(template_name, data)
781
+ csp_header = request.env['csp_header']
782
+ headers['Content-Security-Policy'] = csp_header if csp_header
783
+ result
784
+ end
785
+ end
786
+ ```
787
+
788
+ #### Roda
789
+ ```ruby
790
+ class App < Roda
791
+ def render_with_csp(template_name, data = {})
792
+ result = render_rhales(template_name, data)
793
+ csp_header = request.env['csp_header']
794
+ response.headers['Content-Security-Policy'] = csp_header if csp_header
795
+ result
796
+ end
797
+ end
798
+ ```
799
+
800
+ ### CSP Benefits
801
+
802
+ - **Security by default**: Protection against XSS attacks out of the box
803
+ - **Automatic nonce management**: No manual nonce coordination needed
804
+ - **Template integration**: Nonces automatically available in templates
805
+ - **Framework agnostic**: Works with any Ruby web framework
806
+ - **Customizable policies**: Adapt CSP rules to your application needs
807
+ - **Zero configuration**: Secure defaults work immediately
808
+
809
+ ### Disabling CSP
810
+
811
+ If you need to disable CSP for specific environments:
812
+
813
+ ```ruby
814
+ Rhales.configure do |config|
815
+ config.csp_enabled = false # Disable CSP header generation
816
+ config.auto_nonce = false # Disable automatic nonce generation
817
+ end
818
+ ```
819
+
820
+ ## Testing
821
+
822
+ Rhales includes comprehensive test helpers and is framework-agnostic:
823
+
824
+ ```ruby
825
+ # test/test_helper.rb or spec/spec_helper.rb
826
+ require 'rhales'
827
+
828
+ Rhales.configure do |config|
829
+ config.default_locale = 'en'
830
+ config.app_environment = 'test'
831
+ config.cache_templates = false
832
+ config.template_paths = ['test/templates'] # or wherever your test templates are
833
+ end
834
+
835
+ # Test context creation
836
+ context = Rhales::Context.minimal(props: { user: { name: 'Test' } })
837
+ expect(context.get('user.name')).to eq('Test')
838
+
839
+ # Test template rendering
840
+ template = '{{#if authenticated}}Welcome{{/if}}'
841
+ result = Rhales.render_template(template, authenticated: true)
842
+ expect(result).to eq('Welcome')
843
+
844
+ # Test full template files
845
+ mock_request = OpenStruct.new(env: {})
846
+ mock_session = {}
847
+ view = Rhales::View.new(mock_request, mock_session, nil, 'en')
848
+ html = view.render('test_template', message: 'Hello World')
849
+ ```
850
+
851
+ ## Development
852
+
853
+ After checking out the repo, run:
854
+
855
+ ```bash
856
+ bundle install
857
+ bundle exec rspec
858
+ ```
859
+
860
+ ## Contributing
861
+
862
+ 1. Fork it
863
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
864
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
865
+ 4. Push to the branch (`git push origin my-new-feature`)
866
+ 5. Create a new Pull Request
867
+
868
+ ## License
869
+
870
+ The gem is available as open source under the [MIT License](https://opensource.org/licenses/MIT).
871
+
872
+ ## AI Development Assistance
873
+
874
+ Rhales was developed with assistance from AI tools. The following tools provided significant help with architecture design, code generation, and documentation:
875
+
876
+ - **Claude Sonnet 4** - Architecture design, code generation, and documentation
877
+ - **Claude Desktop & Claude Code** - Interactive development sessions and debugging
878
+ - **GitHub Copilot** - Code completion and refactoring assistance
879
+ - **Qodo Merge Pro** - Code review and quality improvements
880
+
881
+ 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.