Class: Rhales::Context
- Inherits:
-
Object
- Object
- Rhales::Context
- Defined in:
- lib/rhales/core/context.rb
Overview
RSFCContext provides a clean interface for RSFC templates to access server-side data. Follows the established pattern from InitScriptContext and EnvironmentContext for focused, single-responsibility context objects.
The context provides three layers of data: 1. Request: Framework-provided data (CSRF tokens, authentication, config) 2. Server: Template-only variables (page titles, HTML content, etc.) 3. Client: Application data that gets serialized to window state
Request data and server data are accessible in templates.
Client data takes precedence over server data for variable resolution.
Only client data is serialized to the browser via
One RSFCContext instance is created per page render and shared across the main template and all partials to maintain security boundaries.
Defined Under Namespace
Classes: MinimalRequest
Instance Attribute Summary collapse
-
#all_data ⇒ Object
readonly
Get all available data (runtime + business + computed).
-
#client ⇒ Object
readonly
Returns the value of attribute client.
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#req ⇒ Object
readonly
Returns the value of attribute req.
-
#server ⇒ Object
readonly
Returns the value of attribute server.
Class Method Summary collapse
-
.for_view(req, client: {}, server: {}, config: nil, **additional_client) ⇒ Object
Create context with business data for a specific view.
-
.minimal(client: {}, server: {}, config: nil, env: nil) ⇒ Object
Create minimal context for testing with optional env override.
Instance Method Summary collapse
-
#authenticated? ⇒ Boolean
private
Check if user is authenticated.
-
#available_variables ⇒ Object
Get list of all available variable paths (for validation).
-
#build_app_data ⇒ Object
private
Build framework-provided server data.
-
#collect_variable_paths(data, prefix = '') ⇒ Object
private
Recursively collect all variable paths from nested data.
-
#csp_nonce_required? ⇒ Boolean
private
Check if CSP policy requires nonce.
-
#determine_theme_class ⇒ Object
private
Determine theme class for CSS.
-
#get(variable_path) ⇒ Object
Get variable value with dot notation support (e.g., “user.id”, “features.account_creation”).
-
#get_or_generate_nonce ⇒ Object
private
Get or generate CSP nonce.
-
#initialize(req, client: {}, server: {}, config: nil) ⇒ Context
constructor
A new instance of Context.
-
#locale ⇒ String
Get locale from request.
-
#merge_client(additional_client_data) ⇒ Object
Create a new context with merged client data.
-
#normalize_keys(data) ⇒ Object
private
Normalize hash keys to strings recursively.
-
#request ⇒ Object
Add accessor for request data (maps to @server_data for ‘app’ namespace compatibility).
-
#resolve_variable(variable_path) ⇒ Object
Resolve variable (alias for get method for hydrator compatibility).
-
#sess ⇒ Hash, AnonymousSession
Get session from request.
-
#user ⇒ Object, AnonymousAuth
Get user from request.
-
#valid_user_present? ⇒ Boolean
private
Check if we have a valid (non-anonymous) user.
-
#variable?(variable_path) ⇒ Boolean
Check if variable exists.
-
#with_client(new_client_data) ⇒ Object
Create a new context with updated client data.
-
#with_server(new_server_data) ⇒ Object
Create a new context with updated server data.
Constructor Details
#initialize(req, client: {}, server: {}, config: nil) ⇒ Context
Returns a new instance of Context.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/rhales/core/context.rb', line 30 def initialize(req, client: {}, server: {}, config: nil) @req = req @config = config || Rhales.configuration # Normalize keys to strings for consistent access and expose with clean names @client_data = normalize_keys(client).freeze @client = @client_data # Public accessor # Build context layers (three-layer model: request + server + client) # Server data is merged with built-in request/app data @server_data = build_app_data.merge(normalize_keys(server)).freeze @server = @server_data # Public accessor # Pre-compute all_data before freezing # Client takes precedence over server, and add app namespace for backward compatibility @all_data = @server_data.merge(@client_data).merge({ 'app' => @server_data }).freeze # Make context immutable after creation freeze end |
Instance Attribute Details
#all_data ⇒ Object (readonly)
Get all available data (runtime + business + computed)
85 86 87 |
# File 'lib/rhales/core/context.rb', line 85 def all_data @all_data end |
#client ⇒ Object (readonly)
Returns the value of attribute client.
28 29 30 |
# File 'lib/rhales/core/context.rb', line 28 def client @client end |
#config ⇒ Object (readonly)
Returns the value of attribute config.
28 29 30 |
# File 'lib/rhales/core/context.rb', line 28 def config @config end |
#req ⇒ Object (readonly)
Returns the value of attribute req.
28 29 30 |
# File 'lib/rhales/core/context.rb', line 28 def req @req end |
#server ⇒ Object (readonly)
Returns the value of attribute server.
28 29 30 |
# File 'lib/rhales/core/context.rb', line 28 def server @server end |
Class Method Details
.for_view(req, client: {}, server: {}, config: nil, **additional_client) ⇒ Object
Create context with business data for a specific view
342 343 344 345 |
# File 'lib/rhales/core/context.rb', line 342 def for_view(req, client: {}, server: {}, config: nil, **additional_client) all_client = client.merge(additional_client) new(req, client: all_client, server: server, config: config) end |
.minimal(client: {}, server: {}, config: nil, env: nil) ⇒ Object
Create minimal context for testing with optional env override
348 349 350 351 |
# File 'lib/rhales/core/context.rb', line 348 def minimal(client: {}, server: {}, config: nil, env: nil) req = env ? MinimalRequest.new(env) : nil new(req, client: client, server: server, config: config) end |
Instance Method Details
#authenticated? ⇒ Boolean (private)
Check if user is authenticated
237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/rhales/core/context.rb', line 237 def authenticated? # Try Otto strategy_result first (framework integration) if req&.respond_to?(:env) strategy_result = req.env['otto.strategy_result'] if strategy_result&.respond_to?(:authenticated?) return strategy_result.authenticated? && valid_user_present? end end # Fall back to checking session and user directly sess&.authenticated? && valid_user_present? end |
#available_variables ⇒ Object
Get list of all available variable paths (for validation)
93 94 95 |
# File 'lib/rhales/core/context.rb', line 93 def available_variables collect_variable_paths(all_data) end |
#build_app_data ⇒ Object (private)
Build framework-provided server data
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/rhales/core/context.rb', line 196 def build_app_data app = {} # Request context (from current runtime_data) if req && req.respond_to?(:env) && req.env app['csrf_token'] = req.env.fetch(@config.csrf_token_name, nil) app['nonce'] = get_or_generate_nonce app['request_id'] = req.env.fetch('request_id', nil) app['domain_strategy'] = req.env.fetch('domain_strategy', :default) app['display_domain'] = req.env.fetch('display_domain', nil) else # Generate nonce even without request if CSP is enabled app['nonce'] = get_or_generate_nonce end # Configuration (from both layers) app['environment'] = @config.app_environment app['api_base_url'] = @config.api_base_url app['features'] = @config.features app['development'] = @config.development? # Authentication & UI (from current computed_data) app['authenticated'] = authenticated? app['theme_class'] = determine_theme_class app end |
#collect_variable_paths(data, prefix = '') ⇒ Object (private)
Recursively collect all variable paths from nested data
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/rhales/core/context.rb', line 295 def collect_variable_paths(data, prefix = '') paths = [] case data when Hash data.each do |key, value| current_path = prefix.empty? ? key.to_s : "#{prefix}.#{key}" paths << current_path if value.is_a?(Hash) || value.is_a?(Object) paths.concat(collect_variable_paths(value, current_path)) end end when Object # For objects, collect method names that look like attributes data.public_methods(false).each do |method| method_name = method.to_s next if method_name.end_with?('=') # Skip setters next if method_name.start_with?('_') # Skip private-ish methods current_path = prefix.empty? ? method_name : "#{prefix}.#{method_name}" paths << current_path end end paths end |
#csp_nonce_required? ⇒ Boolean (private)
Check if CSP policy requires nonce
271 272 273 274 275 276 |
# File 'lib/rhales/core/context.rb', line 271 def csp_nonce_required? return false unless @config.csp_enabled csp = CSP.new(@config) csp.nonce_required? end |
#determine_theme_class ⇒ Object (private)
Determine theme class for CSS
225 226 227 228 229 230 231 232 233 234 |
# File 'lib/rhales/core/context.rb', line 225 def determine_theme_class # Default theme logic - can be overridden by business data if @client_data['theme'] "theme-#{@client_data['theme']}" elsif user && user.respond_to?(:theme_preference) "theme-#{user.theme_preference}" else 'theme-light' end end |
#get(variable_path) ⇒ Object
Get variable value with dot notation support (e.g., “user.id”, “features.account_creation”)
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/rhales/core/context.rb', line 52 def get(variable_path) path_parts = variable_path.split('.') current_value = all_data path_parts.each do |part| case current_value when Hash if current_value.key?(part) current_value = current_value[part] elsif current_value.key?(part.to_sym) current_value = current_value[part.to_sym] else return nil end when Object if current_value.respond_to?(part) current_value = current_value.public_send(part) elsif current_value.respond_to?("#{part}?") current_value = current_value.public_send("#{part}?") else return nil end else return nil end return nil if current_value.nil? end current_value end |
#get_or_generate_nonce ⇒ Object (private)
Get or generate CSP nonce
256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/rhales/core/context.rb', line 256 def get_or_generate_nonce # Try to get existing nonce from request env if req && req.respond_to?(:env) && req.env existing_nonce = req.env.fetch(@config.nonce_header_name, nil) return existing_nonce if existing_nonce end # Generate new nonce if auto_nonce is enabled or CSP is enabled return CSP.generate_nonce if @config.auto_nonce || (@config.csp_enabled && csp_nonce_required?) # Return nil if nonce is not needed nil end |
#locale ⇒ String
Get locale from request
Parses locale from HTTP_ACCEPT_LANGUAGE or rhales.locale env variable
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/rhales/core/context.rb', line 141 def locale return @config.default_locale unless req if req.respond_to?(:env) && req.env # Check for custom rhales.locale first, then HTTP_ACCEPT_LANGUAGE custom_locale = req.env['rhales.locale'] return custom_locale if custom_locale # Parse HTTP_ACCEPT_LANGUAGE header accept_language = req.env['HTTP_ACCEPT_LANGUAGE'] if accept_language # Extract first locale from Accept-Language header (e.g., "en-US,en;q=0.9" -> "en-US") first_locale = accept_language.split(',').first&.strip&.split(';')&.first return first_locale if first_locale && !first_locale.empty? end @config.default_locale else @config.default_locale end end |
#merge_client(additional_client_data) ⇒ Object
Create a new context with merged client data
184 185 186 187 188 189 190 191 |
# File 'lib/rhales/core/context.rb', line 184 def merge_client(additional_client_data) self.class.new( @req, client: @client_data.merge(normalize_keys(additional_client_data)), server: @server_data, config: @config, ) end |
#normalize_keys(data) ⇒ Object (private)
Normalize hash keys to strings recursively
281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/rhales/core/context.rb', line 281 def normalize_keys(data) case data when Hash data.each_with_object({}) do |(key, value), result| result[key.to_s] = normalize_keys(value) end when Array data.map { |item| normalize_keys(item) } else data end end |
#request ⇒ Object
Add accessor for request data (maps to @server_data for ‘app’ namespace compatibility)
103 104 105 |
# File 'lib/rhales/core/context.rb', line 103 def request @server_data end |
#resolve_variable(variable_path) ⇒ Object
Resolve variable (alias for get method for hydrator compatibility)
98 99 100 |
# File 'lib/rhales/core/context.rb', line 98 def resolve_variable(variable_path) get(variable_path) end |
#sess ⇒ Hash, AnonymousSession
Get session from request
Requires: Request object must respond to #session Provided by: Rack::Request extension in Onetime
113 114 115 116 117 118 119 120 121 122 |
# File 'lib/rhales/core/context.rb', line 113 def sess return Adapters::AnonymousSession.new unless req.respond_to?(:session) session = req.session # If session doesn't have authenticated? method, wrap it in AnonymousSession # This handles both Hash and Rack::Session hash-like objects after hot reload return Adapters::AnonymousSession.new unless session.respond_to?(:authenticated?) session end |
#user ⇒ Object, AnonymousAuth
Get user from request
Requires: Request object must respond to #user Provided by: Rack::Request extension in Onetime
130 131 132 133 134 |
# File 'lib/rhales/core/context.rb', line 130 def user return Adapters::AnonymousAuth.new unless req&.respond_to?(:user) req.user end |
#valid_user_present? ⇒ Boolean (private)
Check if we have a valid (non-anonymous) user
251 252 253 |
# File 'lib/rhales/core/context.rb', line 251 def valid_user_present? user && !user.anonymous? end |
#variable?(variable_path) ⇒ Boolean
Check if variable exists
88 89 90 |
# File 'lib/rhales/core/context.rb', line 88 def variable?(variable_path) !get(variable_path).nil? end |
#with_client(new_client_data) ⇒ Object
Create a new context with updated client data
164 165 166 167 168 169 170 171 |
# File 'lib/rhales/core/context.rb', line 164 def with_client(new_client_data) self.class.new( @req, client: normalize_keys(new_client_data), server: @server_data, config: @config, ) end |
#with_server(new_server_data) ⇒ Object
Create a new context with updated server data
174 175 176 177 178 179 180 181 |
# File 'lib/rhales/core/context.rb', line 174 def with_server(new_server_data) self.class.new( @req, client: @client_data, server: normalize_keys(new_server_data), config: @config, ) end |