Feature System Guide

Overview

Familia's feature system provides a modular architecture for extending Horreum classes with reusable functionality. Features are self-contained modules that can be mixed into classes with dependency management, conflict resolution, and automatic registration.

Core Concepts

Feature Architecture

The feature system consists of several key components:

  1. Feature Modules: Self-contained functionality modules
  2. Registration System: Automatic feature discovery and registration
  3. Dependency Management: Explicit feature dependencies
  4. Conflict Resolution: Handling method name conflicts
  5. Category-based Fields: Special field types for different purposes

Feature Lifecycle

# 1. Feature definition and registration (automatic)
class MyFeature
  def self.included(base)
    base.extend ClassMethods
    base.prepend InstanceMethods
  end

  # Self-register with Familia
  Familia::Base.add_feature self, :my_feature, depends_on: [:other_feature]
end

# 2. Feature activation in classes
class Customer < Familia::Horreum
  feature :my_feature  # Validates, checks dependencies, includes module
end

# 3. Runtime usage
customer = Customer.new
customer.my_feature_method  # Available after feature inclusion

Built-in Features

Core Features

Expiration

class Session < Familia::Horreum
  feature :expiration
  default_expiration 1.hour

  field :user_id, :data
end

session = Session.new(user_id: 123)
session.update_expiration(30.minutes)  # Custom TTL
session.ttl                            # Check remaining time

SafeDump

class Customer < Familia::Horreum
  feature :safe_dump

  field :name, :email
  field :ssn        # Sensitive field
  field :password   # Sensitive field

  # Whitelist fields for API responses
  safe_dump_fields :name, :email  # Excludes ssn, password
end

customer.safe_dump  # => { name: "John", email: "john@example.com" }
customer.dump       # => { name: "John", email: "john@example.com", ssn: "123-45-6789", password: "secret" }

Encrypted Fields

class Vault < Familia::Horreum
  feature :encrypted_fields

  field :name                    # Regular field
  encrypted_field :secret_key    # Encrypted storage
  encrypted_field :api_token     # Another encrypted field
end

vault = Vault.new(secret_key: "super-secret")
vault.save
# secret_key is encrypted in Redis, decrypted on access

Transient Fields

class ApiClient < Familia::Horreum
  feature :transient_fields

  field :endpoint               # Persistent field
  transient_field :auth_token   # Runtime only, RedactedString
end

client = ApiClient.new(auth_token: ENV['API_TOKEN'])
client.auth_token.expose { |token| make_api_call(token) }
client.auth_token.clear!  # Explicit cleanup

Relationships

class Customer < Familia::Horreum
  feature :relationships

  identifier_field :custid
  field :custid, :name, :email

  # Define relationship collections
  participates_in :active_users, type: :sorted_set
  indexed_by :email_lookup, field: :email
  set :domains
end

class Domain < Familia::Horreum
  feature :relationships

  identifier_field :domain_id
  field :domain_id, :name

  # Declare membership in customer collections
  participates_in Customer, :domains, type: :set
end

# Usage
customer = Customer.new(custid: "cust123", name: "Acme Corp")
domain = Domain.new(domain_id: "dom456", name: "acme.com")

# Establish bidirectional relationships
domain.add_to_customer_domains(customer.custid)
customer.domains.add(domain.identifier)

# Query relationships
domain.in_customer_domains?(customer.custid)  # => true

Quantization

class Metric < Familia::Horreum
  feature :quantization

  field :value, :timestamp
end

# Generate quantized timestamps for time bucketing
metric = Metric.new(value: 42)
hourly_bucket = metric.qstamp(1.hour)    # Rounded to hour boundary
daily_bucket = metric.qstamp(1.day)      # Rounded to day boundary

# Create time-based keys
hourly_key = Metric.qstamp(1.hour, pattern: '%Y%m%d%H')  # "2023010114"

Creating Custom Features

Basic Feature Structure

module Familia
  module Features
    module MyCustomFeature
      def self.included(base)
        Familia.trace :LOADED, self, base if Familia.debug?
        base.extend ClassMethods
        base.prepend InstanceMethods  # Use prepend for method interception
      end

      module ClassMethods
        def custom_class_method
          "Available on #{self} class"
        end
      end

      module InstanceMethods
        def custom_instance_method
          "Available on #{self.class} instances"
        end

        # Intercept field access (if needed)
        def field_value=(value)
          # Custom processing before field assignment
          processed_value = process_value(value)
          super(processed_value)  # Call original field setter
        end
      end

      # Register the feature
      Familia::Base.add_feature self, :my_custom_feature
    end
  end
end

Advanced Feature with Dependencies

module Familia
  module Features
    module AdvancedAudit
      def self.included(base)
        base.extend ClassMethods
        base.prepend InstanceMethods

        # Initialize audit tracking
        base.class_list :audit_log
        base.class_hashkey :field_history
      end

      module ClassMethods
        def enable_audit_for(*field_names)
          @audited_fields ||= ::Set.new
          @audited_fields.merge(field_names.map(&:to_sym))
        end

        def audited_fields
          @audited_fields || ::Set.new
        end
      end

      module InstanceMethods
        def save
          # Audit before saving
          audit_changes if respond_to?(:audit_changes)
          super
        end

        private

        def audit_changes
          self.class.audited_fields.each do |field|
            if instance_variable_changed?(field)
              record_field_change(field)
            end
          end
        end

        def record_field_change(field)
          change_record = {
            field: field,
            old_value: instance_variable_was(field),
            new_value: instance_variable_get("@#{field}"),
            timestamp: Time.now.to_f
          }

          self.class.audit_log.append(change_record.to_json)
        end
      end

      # Register with dependency on safe_dump
      Familia::Base.add_feature self, :advanced_audit, depends_on: [:safe_dump]
    end
  end
end

# Usage
class Customer < Familia::Horreum
  feature :safe_dump      # Dependency satisfied first
  feature :advanced_audit # Now can be loaded

  enable_audit_for :name, :email, :status

  field :name, :email, :status, :created_at
end

Feature with Custom Field Types

module Familia
  module Features
    module TimestampTracking
      def self.included(base)
        base.extend ClassMethods

        # Add timestamp fields automatically
        base.timestamp_field :created_at
        base.timestamp_field :updated_at
      end

      module ClassMethods
        def timestamp_field(name, auto_update: true)
          # Create custom field type for timestamps
          require_relative '../field_types/timestamp_field_type'

          field_type = TimestampFieldType.new(
            name,
            auto_update: auto_update,
            format: :iso8601
          )
          register_field_type(field_type)
        end
      end

      # Register feature
      Familia::Base.add_feature self, :timestamp_tracking
    end
  end
end

# Custom field type (separate file)
class TimestampFieldType < Familia::FieldType
  def initialize(name, auto_update: true, format: :unix, **options)
    super(name, **options)
    @auto_update = auto_update
    @format = format
  end

  def serialize_value(record, value)
    case @format
    when :unix then value&.to_f
    when :iso8601 then value&.iso8601
    else value&.to_s
    end
  end

  def deserialize_value(record, stored_value)
    return nil if stored_value.nil?

    case @format
    when :unix then Time.at(stored_value.to_f)
    when :iso8601 then Time.parse(stored_value)
    else Time.parse(stored_value)
    end
  end
end

Feature Dependencies

Declaring Dependencies

# Feature with dependencies
Familia::Base.add_feature MyFeature, :my_feature, depends_on: [:safe_dump, :expiration]

# Will verify dependencies when feature is activated:
class Model < Familia::Horreum
  feature :safe_dump    # Must be loaded first
  feature :expiration   # Must be loaded first
  feature :my_feature   # Dependencies satisfied
end

Dependency Validation

# This will raise an error:
class BadModel < Familia::Horreum
  feature :my_feature  # Error: requires safe_dump, expiration
end
# => Familia::Problem: my_feature requires: safe_dump, expiration

# Correct order:
class GoodModel < Familia::Horreum
  feature :safe_dump
  feature :expiration
  feature :my_feature  # ✅ Dependencies satisfied
end

Method Conflict Resolution

Conflict Detection

class Customer < Familia::Horreum
  field :status  # Defines status= and status methods

  # This would conflict with field-generated method
  def status
    "custom implementation"  # ⚠️ Potential conflict
  end
end

Conflict Resolution Strategies

# 1. Raise on conflict (default)
field :name, on_conflict: :raise     # Raises if method exists

# 2. Skip definition if conflict
field :name, on_conflict: :skip      # Skips if method exists

# 3. Warn but proceed
field :name, on_conflict: :warn      # Warns but defines method

# 4. Ignore silently
field :name, on_conflict: :ignore    # Proceeds without warning

Using Prepend for Method Interception

module MyFeature
  def self.included(base)
    # Use prepend to intercept method calls
    base.prepend InstanceMethods
  end

  module InstanceMethods
    def save
      # Pre-processing
      validate_before_save

      # Call original save method
      result = super

      # Post-processing
      notify_after_save

      result
    end
  end
end

Feature Categories and Field Types

Field Categories

class Document < Familia::Horreum
  field :title                           # Regular field
  field :content, category: :encrypted   # Encrypted field
  field :api_key, category: :transient   # Transient field
  field :tags, category: :indexed        # Custom category
end

Category-based Processing

# Features can process fields by category
module IndexingFeature
  def self.included(base)
    base.extend ClassMethods

    # Process all :indexed category fields
    base.field_definitions.select { |f| f.category == :indexed }.each do |field|
      create_index_for(field.name)
    end
  end
end

Feature Discovery and Loading

Automatic Loading

Features are automatically loaded from the lib/familia/features/ directory:

# lib/familia/features.rb automatically loads:
features_dir = File.join(__dir__, 'features')
Dir.glob(File.join(features_dir, '*.rb')).each do |feature_file|
  require_relative feature_file
end

Manual Feature Registration

# For features outside the standard directory
class ExternalFeature
  # Feature implementation...
end

# Register manually
Familia::Base.add_feature ExternalFeature, :external_feature, depends_on: []

Advanced Usage Patterns

Feature Composition

class AdvancedModel < Familia::Horreum
  # Combine multiple features for rich functionality
  feature :expiration      # TTL support
  feature :safe_dump       # API-safe serialization
  feature :encrypted_fields # Secure storage
  feature :quantization    # Time-based bucketing
  feature :transient_fields # Runtime secrets

  # Now has capabilities from all features
  field :name
  encrypted_field :api_key
  transient_field :session_token
  quantized_field :metrics, interval: 1.hour

  default_expiration 24.hours
  safe_dump_fields :name, :created_at
end

Conditional Feature Loading

class ConfigurableModel < Familia::Horreum
  # Load features based on configuration
  if Rails.env.production?
    feature :encrypted_fields
    feature :advanced_audit
  end

  if defined?(Sidekiq)
    feature :background_processing
  end

  feature :safe_dump  # Always load
end

Testing Features

Feature Testing

RSpec.describe MyCustomFeature do
  let(:test_class) do
    Class.new(Familia::Horreum) do
      feature :my_custom_feature
      field :name
    end
  end

  it "includes feature methods" do
    instance = test_class.new
    expect(instance).to respond_to(:custom_instance_method)
    expect(test_class).to respond_to(:custom_class_method)
  end

  it "validates dependencies" do
    expect {
      Class.new(Familia::Horreum) do
        feature :advanced_audit  # Missing safe_dump dependency
      end
    }.to raise_error(Familia::Problem, /requires.*safe_dump/)
  end
end

Integration Testing

RSpec.describe "Feature Integration" do
  it "combines features correctly" do
    combined_class = Class.new(Familia::Horreum) do
      feature :safe_dump
      feature :expiration
      feature :encrypted_fields

      field :name
      encrypted_field :secret
      safe_dump_fields :name
      default_expiration 1.hour
    end

    instance = combined_class.new(name: "test", secret: "hidden")

    # All features work together
    expect(instance.safe_dump).to eq(name: "test")
    expect(instance.secret).to eq("hidden")  # Decrypted
    expect(instance.ttl).to be > 0           # Has expiration
  end
end

Best Practices

Feature Design

  1. Single Responsibility: Each feature should have one clear purpose
  2. Minimal Dependencies: Avoid complex dependency chains
  3. Graceful Degradation: Handle missing dependencies gracefully
  4. Clear Naming: Use descriptive feature and method names
  5. Documentation: Document feature capabilities and usage

Method Organization

module MyFeature
  def self.included(base)
    base.extend ClassMethods
    base.prepend InstanceMethods  # For interception
    base.include HelperMethods    # For additional utilities
  end

  module ClassMethods
    # Class-level functionality
  end

  module InstanceMethods
    # Instance method interception/override
  end

  module HelperMethods
    # Additional utility methods
  end
end

Performance Considerations

  1. Lazy Loading: Initialize expensive resources only when needed
  2. Caching: Cache computed values appropriately
  3. Method Interception: Use prepend sparingly for performance-critical methods
  4. Field Processing: Minimize overhead in field serialization/deserialization

API Reference

Class Methods

feature(feature_name = nil, **options)

Enable a feature for the current class with optional configuration.

Parameters:

  • feature_name (Symbol, String, nil) - The feature name to enable. Returns enabled features list if nil.
  • options (Hash) - Configuration options stored per-class

Returns: Array of enabled features if feature_name is nil, otherwise nil

Raises: Familia::Problem if feature is unsupported or dependencies missing

features_enabled

Returns array of enabled feature names for this class.

feature_options(feature_name)

Get stored options for a specific feature (Horreum classes only).

Parameters:

  • feature_name (Symbol) - The feature name

Returns: Hash of stored options for the feature

field_group(group_name)

Define a field group that features can populate automatically.

Parameters:

  • group_name (Symbol) - The group name

Feature Registration Methods

Familia::Base.add_feature(klass, feature_name, depends_on: [], field_group: nil)

Register a feature module for use with Familia classes.

Parameters:

  • klass (Module) - The feature module
  • feature_name (Symbol) - The feature identifier
  • depends_on (Array) - Array of required dependency feature names
  • field_group (Symbol, nil) - Optional field group to create automatically

Familia::Base.find_feature(feature_name, starting_class = self)

Find a feature by name, traversing the ancestry chain.

Parameters:

  • feature_name (Symbol) - The feature name
  • starting_class (Class) - Class to start searching from

Returns: Feature module class or nil if not found

FeatureDefinition Structure

Features are tracked using a FeatureDefinition data structure:

FeatureDefinition = Data.define(:name, :depends_on, :field_group)
  • name (Symbol) - The feature identifier
  • depends_on (Array) - Required dependency features
  • field_group (Symbol) - Optional field group name

Feature Implementation Template

module Familia
  module Features
    module MyFeature
      def self.included(base)
        Familia.trace :LOADED, self, base if Familia.debug?
        base.extend ClassMethods
      end

      module ClassMethods
        # Class-level feature methods
      end

      # Register with explicit dependencies and field group
      Familia::Base.add_feature self, :my_feature,
        depends_on: [:safe_dump],
        field_group: :my_fields
    end
  end
end

Dependency Validation

The system validates dependencies when features are enabled:

# This validates that safe_dump is already enabled
feature :advanced_audit  # depends_on: [:safe_dump]

# Error if dependencies missing:
# => Familia::Problem: Feature advanced_audit requires missing dependencies: safe_dump

Feature Options Storage

Feature options are stored per-class using class-level instance variables:

class User < Familia::Horreum
  feature :object_identifier, generator: :uuid_v4
end

User.feature_options(:object_identifier)  # => {generator: :uuid_v4}

The feature system provides a powerful foundation for extending Familia with reusable, composable functionality while maintaining clean separation of concerns and explicit dependency management.