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.
- checksums.yaml +7 -0
- data/CLAUDE.locale.txt +7 -0
- data/CLAUDE.md +90 -0
- data/LICENSE.txt +21 -0
- data/README.md +881 -0
- data/lib/rhales/adapters/base_auth.rb +106 -0
- data/lib/rhales/adapters/base_request.rb +97 -0
- data/lib/rhales/adapters/base_session.rb +93 -0
- data/lib/rhales/configuration.rb +156 -0
- data/lib/rhales/context.rb +240 -0
- data/lib/rhales/csp.rb +94 -0
- data/lib/rhales/errors/hydration_collision_error.rb +85 -0
- data/lib/rhales/errors.rb +36 -0
- data/lib/rhales/hydration_data_aggregator.rb +220 -0
- data/lib/rhales/hydration_registry.rb +58 -0
- data/lib/rhales/hydrator.rb +141 -0
- data/lib/rhales/parsers/handlebars-grammar-review.txt +39 -0
- data/lib/rhales/parsers/handlebars_parser.rb +727 -0
- data/lib/rhales/parsers/rue_format_parser.rb +385 -0
- data/lib/rhales/refinements/require_refinements.rb +236 -0
- data/lib/rhales/rue_document.rb +304 -0
- data/lib/rhales/template_engine.rb +353 -0
- data/lib/rhales/tilt.rb +214 -0
- data/lib/rhales/version.rb +6 -0
- data/lib/rhales/view.rb +412 -0
- data/lib/rhales/view_composition.rb +165 -0
- data/lib/rhales.rb +57 -0
- data/rhales.gemspec +46 -0
- metadata +78 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
# lib/rhales/adapters/base_auth.rb
|
2
|
+
|
3
|
+
module Rhales
|
4
|
+
module Adapters
|
5
|
+
# Base authentication adapter interface
|
6
|
+
#
|
7
|
+
# Defines the contract that authentication adapters must implement
|
8
|
+
# to work with Rhales. This allows the library to work with any
|
9
|
+
# authentication system by implementing this interface.
|
10
|
+
class BaseAuth
|
11
|
+
# Check if user is anonymous
|
12
|
+
def anonymous?
|
13
|
+
raise NotImplementedError, 'Subclasses must implement #anonymous?'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get user's theme preference
|
17
|
+
def theme_preference
|
18
|
+
raise NotImplementedError, 'Subclasses must implement #theme_preference'
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get user identifier (optional)
|
22
|
+
def user_id
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get user display name (optional)
|
27
|
+
def display_name
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if user has specific role/permission (optional)
|
32
|
+
def role?(*)
|
33
|
+
raise NotImplementedError, 'Subclasses must implement #role?'
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get user attributes as hash (optional)
|
37
|
+
def attributes
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
# Create anonymous user instance
|
43
|
+
def anonymous
|
44
|
+
new
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Default implementation for anonymous users
|
50
|
+
class AnonymousAuth < BaseAuth
|
51
|
+
def anonymous?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def theme_preference
|
56
|
+
'light'
|
57
|
+
end
|
58
|
+
|
59
|
+
def user_id
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def role?(*)
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def display_name
|
68
|
+
'Anonymous'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Example authenticated user implementation
|
73
|
+
class AuthenticatedAuth < BaseAuth
|
74
|
+
attr_reader :user_data
|
75
|
+
|
76
|
+
def initialize(user_data = {})
|
77
|
+
@user_data = user_data
|
78
|
+
end
|
79
|
+
|
80
|
+
def anonymous?
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
def theme_preference
|
85
|
+
@user_data[:theme] || @user_data['theme'] || 'light'
|
86
|
+
end
|
87
|
+
|
88
|
+
def user_id
|
89
|
+
@user_data[:id] || @user_data['id']
|
90
|
+
end
|
91
|
+
|
92
|
+
def display_name
|
93
|
+
@user_data[:name] || @user_data['name'] || 'User'
|
94
|
+
end
|
95
|
+
|
96
|
+
def role?(role)
|
97
|
+
roles = @user_data[:roles] || @user_data['roles'] || []
|
98
|
+
roles.include?(role) || roles.include?(role.to_s)
|
99
|
+
end
|
100
|
+
|
101
|
+
def attributes
|
102
|
+
@user_data
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# lib/rhales/adapters/base_request.rb
|
2
|
+
|
3
|
+
module Rhales
|
4
|
+
module Adapters
|
5
|
+
# Base request adapter interface
|
6
|
+
#
|
7
|
+
# Defines the contract that request adapters must implement
|
8
|
+
# to work with Rhales. This allows the library to work with any
|
9
|
+
# web framework by implementing this interface.
|
10
|
+
class BaseRequest
|
11
|
+
# Get request path
|
12
|
+
def path
|
13
|
+
raise NotImplementedError, 'Subclasses must implement #path'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get request method
|
17
|
+
def method
|
18
|
+
raise NotImplementedError, 'Subclasses must implement #method'
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get client IP
|
22
|
+
def ip
|
23
|
+
raise NotImplementedError, 'Subclasses must implement #ip'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get request parameters
|
27
|
+
def params
|
28
|
+
raise NotImplementedError, 'Subclasses must implement #params'
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get request environment
|
32
|
+
def env
|
33
|
+
raise NotImplementedError, 'Subclasses must implement #env'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Simple request adapter for framework integration
|
38
|
+
class SimpleRequest < BaseRequest
|
39
|
+
attr_reader :request_path, :request_method, :client_ip, :request_params, :request_env
|
40
|
+
|
41
|
+
def initialize(path: '/', method: 'GET', ip: '127.0.0.1', params: {}, env: {})
|
42
|
+
@request_path = path
|
43
|
+
@request_method = method
|
44
|
+
@client_ip = ip
|
45
|
+
@request_params = params
|
46
|
+
@request_env = env
|
47
|
+
end
|
48
|
+
|
49
|
+
def path
|
50
|
+
@request_path
|
51
|
+
end
|
52
|
+
|
53
|
+
def method
|
54
|
+
@request_method
|
55
|
+
end
|
56
|
+
|
57
|
+
def ip
|
58
|
+
@client_ip
|
59
|
+
end
|
60
|
+
|
61
|
+
def params
|
62
|
+
@request_params
|
63
|
+
end
|
64
|
+
|
65
|
+
def env
|
66
|
+
@request_env
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Framework request adapter wrapper
|
71
|
+
class FrameworkRequest < BaseRequest
|
72
|
+
def initialize(framework_request)
|
73
|
+
@framework_request = framework_request
|
74
|
+
end
|
75
|
+
|
76
|
+
def path
|
77
|
+
@framework_request.path
|
78
|
+
end
|
79
|
+
|
80
|
+
def method
|
81
|
+
@framework_request.request_method
|
82
|
+
end
|
83
|
+
|
84
|
+
def ip
|
85
|
+
@framework_request.ip
|
86
|
+
end
|
87
|
+
|
88
|
+
def params
|
89
|
+
@framework_request.params
|
90
|
+
end
|
91
|
+
|
92
|
+
def env
|
93
|
+
@framework_request.env
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# lib/rhales/adapters/base_session.rb
|
2
|
+
|
3
|
+
module Rhales
|
4
|
+
module Adapters
|
5
|
+
# Base session adapter interface
|
6
|
+
#
|
7
|
+
# Defines the contract that session adapters must implement
|
8
|
+
# to work with Rhales. This allows the library to work with any
|
9
|
+
# session management system.
|
10
|
+
class BaseSession
|
11
|
+
# Check if session is authenticated
|
12
|
+
def authenticated?
|
13
|
+
raise NotImplementedError, 'Subclasses must implement #authenticated?'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get session identifier
|
17
|
+
def session_id
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get session data
|
22
|
+
def data
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Check if session is valid/active
|
27
|
+
def valid?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get session creation time
|
32
|
+
def created_at
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get last access time
|
37
|
+
def last_accessed_at
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Default implementation for anonymous sessions
|
43
|
+
class AnonymousSession < BaseSession
|
44
|
+
def authenticated?
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def session_id
|
49
|
+
'anonymous'
|
50
|
+
end
|
51
|
+
|
52
|
+
def valid?
|
53
|
+
true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Example authenticated session implementation
|
58
|
+
class AuthenticatedSession < BaseSession
|
59
|
+
attr_reader :session_data
|
60
|
+
|
61
|
+
def initialize(session_data = {})
|
62
|
+
@session_data = session_data
|
63
|
+
end
|
64
|
+
|
65
|
+
def authenticated?
|
66
|
+
!@session_data.empty? && valid?
|
67
|
+
end
|
68
|
+
|
69
|
+
def session_id
|
70
|
+
@session_data[:id] || @session_data['id']
|
71
|
+
end
|
72
|
+
|
73
|
+
def data
|
74
|
+
@session_data
|
75
|
+
end
|
76
|
+
|
77
|
+
def valid?
|
78
|
+
return false unless @session_data[:created_at] || @session_data['created_at']
|
79
|
+
|
80
|
+
# Add session validation logic here (expiry, etc.)
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def created_at
|
85
|
+
@session_data[:created_at] || @session_data['created_at']
|
86
|
+
end
|
87
|
+
|
88
|
+
def last_accessed_at
|
89
|
+
@session_data[:last_accessed_at] || @session_data['last_accessed_at']
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# lib/rhales/configuration.rb
|
2
|
+
|
3
|
+
module Rhales
|
4
|
+
# Configuration management for Rhales library
|
5
|
+
#
|
6
|
+
# Provides a clean, testable alternative to global configuration access.
|
7
|
+
# Supports block-based configuration typical of Ruby gems and dependency injection.
|
8
|
+
#
|
9
|
+
# Usage:
|
10
|
+
# Rhales.configure do |config|
|
11
|
+
# config.default_locale = 'en'
|
12
|
+
# config.template_paths = ['app/templates', 'lib/templates']
|
13
|
+
# config.features = { account_creation: true }
|
14
|
+
# end
|
15
|
+
class Configuration
|
16
|
+
# Core application settings
|
17
|
+
attr_accessor :default_locale, :app_environment, :development_enabled
|
18
|
+
|
19
|
+
# Template settings
|
20
|
+
attr_accessor :template_paths, :template_root, :cache_templates
|
21
|
+
|
22
|
+
# Security settings
|
23
|
+
attr_accessor :csrf_token_name, :nonce_header_name, :csp_enabled, :csp_policy, :auto_nonce
|
24
|
+
|
25
|
+
# Feature flags
|
26
|
+
attr_accessor :features
|
27
|
+
|
28
|
+
# Site configuration
|
29
|
+
attr_accessor :site_host, :site_ssl_enabled, :api_base_url
|
30
|
+
|
31
|
+
# Performance settings
|
32
|
+
attr_accessor :cache_parsed_templates, :cache_ttl
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
# Set sensible defaults
|
36
|
+
@default_locale = 'en'
|
37
|
+
@app_environment = 'development'
|
38
|
+
@development_enabled = false
|
39
|
+
@template_paths = []
|
40
|
+
@cache_templates = true
|
41
|
+
@csrf_token_name = 'csrf_token'
|
42
|
+
@nonce_header_name = 'nonce'
|
43
|
+
@csp_enabled = true
|
44
|
+
@auto_nonce = true
|
45
|
+
@csp_policy = default_csp_policy
|
46
|
+
@features = {}
|
47
|
+
@site_ssl_enabled = false
|
48
|
+
@cache_parsed_templates = true
|
49
|
+
@cache_ttl = 3600 # 1 hour
|
50
|
+
end
|
51
|
+
|
52
|
+
# Build API base URL from site configuration
|
53
|
+
def api_base_url
|
54
|
+
return @api_base_url if @api_base_url
|
55
|
+
|
56
|
+
return nil unless @site_host
|
57
|
+
|
58
|
+
protocol = @site_ssl_enabled ? 'https' : 'http'
|
59
|
+
"#{protocol}://#{@site_host}/api"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Check if development mode is enabled
|
63
|
+
def development?
|
64
|
+
@development_enabled || @app_environment == 'development'
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if production mode
|
68
|
+
def production?
|
69
|
+
@app_environment == 'production'
|
70
|
+
end
|
71
|
+
|
72
|
+
# Default CSP policy with secure defaults
|
73
|
+
def default_csp_policy
|
74
|
+
{
|
75
|
+
'default-src' => ["'self'"],
|
76
|
+
'script-src' => ["'self'", "'nonce-{{nonce}}'"],
|
77
|
+
'style-src' => ["'self'", "'nonce-{{nonce}}'", "'unsafe-hashes'"],
|
78
|
+
'img-src' => ["'self'", 'data:'],
|
79
|
+
'font-src' => ["'self'"],
|
80
|
+
'connect-src' => ["'self'"],
|
81
|
+
'base-uri' => ["'self'"],
|
82
|
+
'form-action' => ["'self'"],
|
83
|
+
'frame-ancestors' => ["'none'"],
|
84
|
+
'object-src' => ["'none'"],
|
85
|
+
'media-src' => ["'self'"],
|
86
|
+
'worker-src' => ["'self'"],
|
87
|
+
'manifest-src' => ["'self'"],
|
88
|
+
'prefetch-src' => ["'self'"],
|
89
|
+
'upgrade-insecure-requests' => []
|
90
|
+
}.freeze
|
91
|
+
end
|
92
|
+
|
93
|
+
# Get feature flag value
|
94
|
+
def feature_enabled?(feature_name)
|
95
|
+
@features[feature_name] || @features[feature_name.to_s] || false
|
96
|
+
end
|
97
|
+
|
98
|
+
# Validate configuration
|
99
|
+
def validate!
|
100
|
+
errors = []
|
101
|
+
|
102
|
+
# Validate locale
|
103
|
+
if @default_locale.nil? || @default_locale.empty?
|
104
|
+
errors << 'default_locale cannot be empty'
|
105
|
+
end
|
106
|
+
|
107
|
+
# Validate template paths exist if specified
|
108
|
+
@template_paths.each do |path|
|
109
|
+
unless Dir.exist?(path)
|
110
|
+
errors << "Template path does not exist: #{path}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Validate cache TTL
|
115
|
+
if @cache_ttl && @cache_ttl <= 0
|
116
|
+
errors << 'cache_ttl must be positive'
|
117
|
+
end
|
118
|
+
|
119
|
+
raise ConfigurationError, "Configuration errors: #{errors.join(', ')}" unless errors.empty?
|
120
|
+
end
|
121
|
+
|
122
|
+
# Deep freeze configuration to prevent modification after setup
|
123
|
+
def freeze!
|
124
|
+
@features.freeze
|
125
|
+
@template_paths.freeze
|
126
|
+
freeze
|
127
|
+
end
|
128
|
+
|
129
|
+
class ConfigurationError < StandardError; end
|
130
|
+
end
|
131
|
+
|
132
|
+
class << self
|
133
|
+
# Global configuration instance
|
134
|
+
def configuration
|
135
|
+
@configuration ||= Configuration.new
|
136
|
+
end
|
137
|
+
|
138
|
+
# Configure Rhales with block
|
139
|
+
def configure
|
140
|
+
yield(configuration) if block_given?
|
141
|
+
configuration.validate!
|
142
|
+
configuration.freeze!
|
143
|
+
configuration
|
144
|
+
end
|
145
|
+
|
146
|
+
# Reset configuration (useful for testing)
|
147
|
+
def reset_configuration!
|
148
|
+
@configuration = nil
|
149
|
+
end
|
150
|
+
|
151
|
+
# Shorthand access to configuration
|
152
|
+
def config
|
153
|
+
configuration
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
# lib/rhales/context.rb
|
2
|
+
|
3
|
+
require_relative 'configuration'
|
4
|
+
require_relative 'adapters/base_auth'
|
5
|
+
require_relative 'adapters/base_session'
|
6
|
+
require_relative 'adapters/base_request'
|
7
|
+
require_relative 'csp'
|
8
|
+
|
9
|
+
module Rhales
|
10
|
+
# RSFCContext provides a clean interface for RSFC templates to access
|
11
|
+
# server-side data. Follows the established pattern from InitScriptContext
|
12
|
+
# and EnvironmentContext for focused, single-responsibility context objects.
|
13
|
+
#
|
14
|
+
# The context provides two layers of data:
|
15
|
+
# 1. App: Framework-provided data (CSRF tokens, authentication, config)
|
16
|
+
# 2. Props: Application data passed to the view (user, content, features)
|
17
|
+
#
|
18
|
+
# App data is accessible as both direct variables and through the app.* namespace.
|
19
|
+
# Props take precedence over app data for variable resolution.
|
20
|
+
#
|
21
|
+
# One RSFCContext instance is created per page render and shared across
|
22
|
+
# the main template and all partials to maintain security boundaries.
|
23
|
+
class Context
|
24
|
+
attr_reader :req, :sess, :cust, :locale, :props, :config, :app_data
|
25
|
+
|
26
|
+
def initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil)
|
27
|
+
@req = req
|
28
|
+
@sess = sess || default_session
|
29
|
+
@cust = cust || default_customer
|
30
|
+
@config = config || Rhales.configuration
|
31
|
+
@locale = locale_override || @config.default_locale
|
32
|
+
|
33
|
+
# Normalize props keys to strings for consistent access
|
34
|
+
@props = normalize_keys(props).freeze
|
35
|
+
|
36
|
+
# Build context layers (two-layer model: app + props)
|
37
|
+
@app_data = build_app_data.freeze
|
38
|
+
|
39
|
+
# Pre-compute all_data before freezing
|
40
|
+
# Props take precedence over app data, and add app namespace
|
41
|
+
@all_data = @app_data.merge(@props).merge({ 'app' => @app_data }).freeze
|
42
|
+
|
43
|
+
# Make context immutable after creation
|
44
|
+
freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get variable value with dot notation support (e.g., "user.id", "features.account_creation")
|
48
|
+
def get(variable_path)
|
49
|
+
path_parts = variable_path.split('.')
|
50
|
+
current_value = all_data
|
51
|
+
|
52
|
+
path_parts.each do |part|
|
53
|
+
case current_value
|
54
|
+
when Hash
|
55
|
+
if current_value.key?(part)
|
56
|
+
current_value = current_value[part]
|
57
|
+
elsif current_value.key?(part.to_sym)
|
58
|
+
current_value = current_value[part.to_sym]
|
59
|
+
else
|
60
|
+
return nil
|
61
|
+
end
|
62
|
+
when Object
|
63
|
+
if current_value.respond_to?(part)
|
64
|
+
current_value = current_value.public_send(part)
|
65
|
+
elsif current_value.respond_to?("#{part}?")
|
66
|
+
current_value = current_value.public_send("#{part}?")
|
67
|
+
else
|
68
|
+
return nil
|
69
|
+
end
|
70
|
+
else
|
71
|
+
return nil
|
72
|
+
end
|
73
|
+
|
74
|
+
return nil if current_value.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
current_value
|
78
|
+
end
|
79
|
+
|
80
|
+
# Get all available data (runtime + business + computed)
|
81
|
+
attr_reader :all_data
|
82
|
+
|
83
|
+
# Check if variable exists
|
84
|
+
def variable?(variable_path)
|
85
|
+
!get(variable_path).nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get list of all available variable paths (for validation)
|
89
|
+
def available_variables
|
90
|
+
collect_variable_paths(all_data)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Resolve variable (alias for get method for hydrator compatibility)
|
94
|
+
def resolve_variable(variable_path)
|
95
|
+
get(variable_path)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
|
101
|
+
# Build consolidated app data (replaces runtime_data + computed_data)
|
102
|
+
def build_app_data
|
103
|
+
app = {}
|
104
|
+
|
105
|
+
# Request context (from current runtime_data)
|
106
|
+
if req && req.respond_to?(:env) && req.env
|
107
|
+
app['csrf_token'] = req.env.fetch(@config.csrf_token_name, nil)
|
108
|
+
app['nonce'] = get_or_generate_nonce
|
109
|
+
app['request_id'] = req.env.fetch('request_id', nil)
|
110
|
+
app['domain_strategy'] = req.env.fetch('domain_strategy', :default)
|
111
|
+
app['display_domain'] = req.env.fetch('display_domain', nil)
|
112
|
+
else
|
113
|
+
# Generate nonce even without request if CSP is enabled
|
114
|
+
app['nonce'] = get_or_generate_nonce
|
115
|
+
end
|
116
|
+
|
117
|
+
# Configuration (from both layers)
|
118
|
+
app['environment'] = @config.app_environment
|
119
|
+
app['api_base_url'] = @config.api_base_url
|
120
|
+
app['features'] = @config.features
|
121
|
+
app['development'] = @config.development?
|
122
|
+
|
123
|
+
# Authentication & UI (from current computed_data)
|
124
|
+
app['authenticated'] = authenticated?
|
125
|
+
app['theme_class'] = determine_theme_class
|
126
|
+
|
127
|
+
app
|
128
|
+
end
|
129
|
+
|
130
|
+
# Build API base URL from configuration (deprecated - moved to config)
|
131
|
+
def build_api_base_url
|
132
|
+
@config.api_base_url
|
133
|
+
end
|
134
|
+
|
135
|
+
# Determine theme class for CSS
|
136
|
+
def determine_theme_class
|
137
|
+
# Default theme logic - can be overridden by business data
|
138
|
+
if props['theme']
|
139
|
+
"theme-#{props['theme']}"
|
140
|
+
elsif cust && cust.respond_to?(:theme_preference)
|
141
|
+
"theme-#{cust.theme_preference}"
|
142
|
+
else
|
143
|
+
'theme-light'
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Check if user is authenticated
|
148
|
+
def authenticated?
|
149
|
+
sess && sess.authenticated? && cust && !cust.anonymous?
|
150
|
+
end
|
151
|
+
|
152
|
+
# Get default session instance
|
153
|
+
def default_session
|
154
|
+
Rhales::Adapters::AnonymousSession.new
|
155
|
+
end
|
156
|
+
|
157
|
+
# Get default customer instance
|
158
|
+
def default_customer
|
159
|
+
Rhales::Adapters::AnonymousAuth.new
|
160
|
+
end
|
161
|
+
|
162
|
+
# Normalize hash keys to strings recursively
|
163
|
+
def normalize_keys(data)
|
164
|
+
case data
|
165
|
+
when Hash
|
166
|
+
data.each_with_object({}) do |(key, value), result|
|
167
|
+
result[key.to_s] = normalize_keys(value)
|
168
|
+
end
|
169
|
+
when Array
|
170
|
+
data.map { |item| normalize_keys(item) }
|
171
|
+
else
|
172
|
+
data
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Recursively collect all variable paths from nested data
|
177
|
+
def collect_variable_paths(data, prefix = '')
|
178
|
+
paths = []
|
179
|
+
|
180
|
+
case data
|
181
|
+
when Hash
|
182
|
+
data.each do |key, value|
|
183
|
+
current_path = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
184
|
+
paths << current_path
|
185
|
+
|
186
|
+
if value.is_a?(Hash) || value.is_a?(Object)
|
187
|
+
paths.concat(collect_variable_paths(value, current_path))
|
188
|
+
end
|
189
|
+
end
|
190
|
+
when Object
|
191
|
+
# For objects, collect method names that look like attributes
|
192
|
+
data.public_methods(false).each do |method|
|
193
|
+
method_name = method.to_s
|
194
|
+
next if method_name.end_with?('=') # Skip setters
|
195
|
+
next if method_name.start_with?('_') # Skip private-ish methods
|
196
|
+
|
197
|
+
current_path = prefix.empty? ? method_name : "#{prefix}.#{method_name}"
|
198
|
+
paths << current_path
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
paths
|
203
|
+
end
|
204
|
+
|
205
|
+
# Get or generate nonce for CSP
|
206
|
+
def get_or_generate_nonce
|
207
|
+
# Try to get existing nonce from request env
|
208
|
+
if req && req.respond_to?(:env) && req.env
|
209
|
+
existing_nonce = req.env.fetch(@config.nonce_header_name, nil)
|
210
|
+
return existing_nonce if existing_nonce
|
211
|
+
end
|
212
|
+
|
213
|
+
# Generate new nonce if auto_nonce is enabled or CSP is enabled
|
214
|
+
return CSP.generate_nonce if @config.auto_nonce || (@config.csp_enabled && csp_nonce_required?)
|
215
|
+
|
216
|
+
# Return nil if nonce is not needed
|
217
|
+
nil
|
218
|
+
end
|
219
|
+
|
220
|
+
# Check if CSP policy requires nonce
|
221
|
+
def csp_nonce_required?
|
222
|
+
return false unless @config.csp_enabled
|
223
|
+
|
224
|
+
csp = CSP.new(@config)
|
225
|
+
csp.nonce_required?
|
226
|
+
end
|
227
|
+
|
228
|
+
class << self
|
229
|
+
# Create context with business data for a specific view
|
230
|
+
def for_view(req, sess, cust, locale, config: nil, **props)
|
231
|
+
new(req, sess, cust, locale, props: props, config: config)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Create minimal context for testing
|
235
|
+
def minimal(props: {}, config: nil)
|
236
|
+
new(nil, nil, nil, 'en', props: props, config: config)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|