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
         
     |