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.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/rhales/core/context.rb', line 28 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)
83 84 85 |
# File 'lib/rhales/core/context.rb', line 83 def all_data @all_data end |
#client ⇒ Object (readonly)
Returns the value of attribute client.
26 27 28 |
# File 'lib/rhales/core/context.rb', line 26 def client @client end |
#config ⇒ Object (readonly)
Returns the value of attribute config.
26 27 28 |
# File 'lib/rhales/core/context.rb', line 26 def config @config end |
#req ⇒ Object (readonly)
Returns the value of attribute req.
26 27 28 |
# File 'lib/rhales/core/context.rb', line 26 def req @req end |
#server ⇒ Object (readonly)
Returns the value of attribute server.
26 27 28 |
# File 'lib/rhales/core/context.rb', line 26 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
337 338 339 340 |
# File 'lib/rhales/core/context.rb', line 337 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
343 344 345 346 |
# File 'lib/rhales/core/context.rb', line 343 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
232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/rhales/core/context.rb', line 232 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)
91 92 93 |
# File 'lib/rhales/core/context.rb', line 91 def available_variables collect_variable_paths(all_data) end |
#build_app_data ⇒ Object (private)
Build framework-provided server data
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/rhales/core/context.rb', line 191 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
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/rhales/core/context.rb', line 290 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
266 267 268 269 270 271 |
# File 'lib/rhales/core/context.rb', line 266 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
220 221 222 223 224 225 226 227 228 229 |
# File 'lib/rhales/core/context.rb', line 220 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”)
50 51 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 |
# File 'lib/rhales/core/context.rb', line 50 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
251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/rhales/core/context.rb', line 251 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
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/rhales/core/context.rb', line 136 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
179 180 181 182 183 184 185 186 |
# File 'lib/rhales/core/context.rb', line 179 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
276 277 278 279 280 281 282 283 284 285 286 287 |
# File 'lib/rhales/core/context.rb', line 276 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)
101 102 103 |
# File 'lib/rhales/core/context.rb', line 101 def request @server_data end |
#resolve_variable(variable_path) ⇒ Object
Resolve variable (alias for get method for hydrator compatibility)
96 97 98 |
# File 'lib/rhales/core/context.rb', line 96 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
111 112 113 114 115 116 117 118 |
# File 'lib/rhales/core/context.rb', line 111 def sess return Adapters::AnonymousSession.new unless req&.respond_to?(:session) session = req.session # If session is a plain hash, wrap it in AnonymousSession return Adapters::AnonymousSession.new if session.is_a?(Hash) && !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
126 127 128 129 |
# File 'lib/rhales/core/context.rb', line 126 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
246 247 248 |
# File 'lib/rhales/core/context.rb', line 246 def valid_user_present? user && !user.anonymous? end |
#variable?(variable_path) ⇒ Boolean
Check if variable exists
86 87 88 |
# File 'lib/rhales/core/context.rb', line 86 def variable?(variable_path) !get(variable_path).nil? end |
#with_client(new_client_data) ⇒ Object
Create a new context with updated client data
159 160 161 162 163 164 165 166 |
# File 'lib/rhales/core/context.rb', line 159 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
169 170 171 172 173 174 175 176 |
# File 'lib/rhales/core/context.rb', line 169 def with_server(new_server_data) self.class.new( @req, client: @client_data, server: normalize_keys(new_server_data), config: @config, ) end |