Class: Rhales::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/rhales/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 two layers of data: 1. App: Framework-provided data (CSRF tokens, authentication, config) 2. Props: Application data passed to the view (user, content, features)

App data is accessible as both direct variables and through the app.* namespace. Props take precedence over app data for variable resolution.

One RSFCContext instance is created per page render and shared across the main template and all partials to maintain security boundaries.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil) ⇒ Context

Returns a new instance of Context.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/rhales/context.rb', line 26

def initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil)
  @req           = req
  @sess          = sess || default_session
  @cust          = cust || default_customer
  @config        = config || Rhales.configuration
  @locale        = locale_override || @config.default_locale

  # Normalize props keys to strings for consistent access
  @props = normalize_keys(props).freeze

  # Build context layers (two-layer model: app + props)
  @app_data = build_app_data.freeze

  # Pre-compute all_data before freezing
  # Props take precedence over app data, and add app namespace
  @all_data = @app_data.merge(@props).merge({ 'app' => @app_data }).freeze

  # Make context immutable after creation
  freeze
end

Instance Attribute Details

#all_dataObject (readonly)

Get all available data (runtime + business + computed)



81
82
83
# File 'lib/rhales/context.rb', line 81

def all_data
  @all_data
end

#app_dataObject (readonly)

Returns the value of attribute app_data.



24
25
26
# File 'lib/rhales/context.rb', line 24

def app_data
  @app_data
end

#configObject (readonly)

Returns the value of attribute config.



24
25
26
# File 'lib/rhales/context.rb', line 24

def config
  @config
end

#custObject (readonly)

Returns the value of attribute cust.



24
25
26
# File 'lib/rhales/context.rb', line 24

def cust
  @cust
end

#localeObject (readonly)

Returns the value of attribute locale.



24
25
26
# File 'lib/rhales/context.rb', line 24

def locale
  @locale
end

#propsObject (readonly)

Returns the value of attribute props.



24
25
26
# File 'lib/rhales/context.rb', line 24

def props
  @props
end

#reqObject (readonly)

Returns the value of attribute req.



24
25
26
# File 'lib/rhales/context.rb', line 24

def req
  @req
end

#sessObject (readonly)

Returns the value of attribute sess.



24
25
26
# File 'lib/rhales/context.rb', line 24

def sess
  @sess
end

Class Method Details

.for_view(req, sess, cust, locale, config: nil, **props) ⇒ Object

Create context with business data for a specific view



229
230
231
# File 'lib/rhales/context.rb', line 229

def for_view(req, sess, cust, locale, config: nil, **props)
  new(req, sess, cust, locale, props: props, config: config)
end

.minimal(props: {}, config: nil) ⇒ Object

Create minimal context for testing



234
235
236
# File 'lib/rhales/context.rb', line 234

def minimal(props: {}, config: nil)
  new(nil, nil, nil, 'en', props: props, config: config)
end

Instance Method Details

#authenticated?Boolean (private)

Check if user is authenticated

Returns:

  • (Boolean)


147
148
149
# File 'lib/rhales/context.rb', line 147

def authenticated?
  sess && sess.authenticated? && cust && !cust.anonymous?
end

#available_variablesObject

Get list of all available variable paths (for validation)



89
90
91
# File 'lib/rhales/context.rb', line 89

def available_variables
  collect_variable_paths(all_data)
end

#build_api_base_urlObject (private)

Build API base URL from configuration (deprecated - moved to config)



130
131
132
# File 'lib/rhales/context.rb', line 130

def build_api_base_url
  @config.api_base_url
end

#build_app_dataObject (private)

Build consolidated app data (replaces runtime_data + computed_data)



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/rhales/context.rb', line 101

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



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/rhales/context.rb', line 176

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

Returns:

  • (Boolean)


220
221
222
223
224
225
# File 'lib/rhales/context.rb', line 220

def csp_nonce_required?
  return false unless @config.csp_enabled

  csp = CSP.new(@config)
  csp.nonce_required?
end

#default_customerObject (private)

Get default customer instance



157
158
159
# File 'lib/rhales/context.rb', line 157

def default_customer
  Rhales::Adapters::AnonymousAuth.new
end

#default_sessionObject (private)

Get default session instance



152
153
154
# File 'lib/rhales/context.rb', line 152

def default_session
  Rhales::Adapters::AnonymousSession.new
end

#determine_theme_classObject (private)

Determine theme class for CSS



135
136
137
138
139
140
141
142
143
144
# File 'lib/rhales/context.rb', line 135

def determine_theme_class
  # Default theme logic - can be overridden by business data
  if props['theme']
    "theme-#{props['theme']}"
  elsif cust && cust.respond_to?(:theme_preference)
    "theme-#{cust.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”)



48
49
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
# File 'lib/rhales/context.rb', line 48

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_nonceObject (private)

Get or generate nonce for CSP



205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/rhales/context.rb', line 205

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

#normalize_keys(data) ⇒ Object (private)

Normalize hash keys to strings recursively



162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/rhales/context.rb', line 162

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

#resolve_variable(variable_path) ⇒ Object

Resolve variable (alias for get method for hydrator compatibility)



94
95
96
# File 'lib/rhales/context.rb', line 94

def resolve_variable(variable_path)
  get(variable_path)
end

#variable?(variable_path) ⇒ Boolean

Check if variable exists

Returns:

  • (Boolean)


84
85
86
# File 'lib/rhales/context.rb', line 84

def variable?(variable_path)
  !get(variable_path).nil?
end