Feature System Developer Guide
Overview
The Familia Feature system provides a simple, modular way to add optional functionality to Familia classes. Features are Ruby modules that get included into classes, extending them with additional methods and capabilities.
Architecture
Core Components
1. Feature Registration (Familia::Base)
Features are registered using the add_feature method:
module Familia::Base
def self.add_feature(klass, feature_name, depends_on: [], field_group: nil)
@features_available ||= {}
# Create simple feature definition
feature_def = FeatureDefinition.new(
name: feature_name,
depends_on: depends_on,
field_group: field_group
)
# Track feature definitions and availability
@feature_definitions ||= {}
@feature_definitions[feature_name] = feature_def
features_available[feature_name] = klass
end
end
2. Feature Activation (Horreum Classes)
Features are activated using the feature method:
class MyModel < Familia::Horreum
feature :expiration
feature :encrypted_fields
feature :safe_dump
end
3. Feature Definition Structure
Features are defined using a simple Data class:
FeatureDefinition = Data.define(:name, :depends_on, :field_group)
Feature Loading Lifecycle
1. Feature Self-Registration
Each feature module registers itself:
module Familia::Features::MyFeature
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
module ClassMethods
def my_feature_config
# Class-level functionality
end
end
module InstanceMethods
def my_feature_method
# Instance-level functionality
end
end
# Self-register with the feature system
Familia::Base.add_feature self, :my_feature, depends_on: []
end
2. Runtime Inclusion
When feature is called, the system:
- Validates the feature exists
- Checks dependencies are satisfied
- Includes the feature module into the class
- Stores feature options if provided
def feature(feature_name = nil, **)
@features_enabled ||= []
return features_enabled if feature_name.nil?
feature_name = feature_name.to_sym
feature_module = Familia::Base.find_feature(feature_name, self)
raise Familia::Problem, "Unsupported feature: #{feature_name}" unless feature_module
# Check dependencies
feature_def = Familia::Base.feature_definitions[feature_name]
if feature_def&.depends_on&.any?
missing = feature_def.depends_on - features_enabled
if missing.any?
raise Familia::Problem,
"Feature #{feature_name} requires missing dependencies: #{missing.join(', ')}"
end
end
features_enabled << feature_name
include feature_module
end
Basic Feature Development
1. Feature Structure Template
module Familia
module Features
module MyFeature
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
module ClassMethods
def my_feature_config
@my_feature_config ||= {}
end
def configure_my_feature(**)
my_feature_config.merge!()
end
def my_feature_enabled?
features_enabled.include?(:my_feature)
end
end
module InstanceMethods
def save
before_my_feature_save if respond_to?(:before_my_feature_save, true)
result = super
after_my_feature_save if respond_to?(:after_my_feature_save, true)
result
end
private
def before_my_feature_save
# Pre-save logic
end
def after_my_feature_save
# Post-save logic
end
end
# Register the feature
Familia::Base.add_feature self, :my_feature
end
end
end
2. Feature with Dependencies
module Familia::Features::AdvancedFeature
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def advanced_method
# This feature requires :basic_feature to be enabled
raise "Basic feature required" unless features_enabled.include?(:basic_feature)
# Advanced functionality here
end
end
# Register with dependency
Familia::Base.add_feature self, :advanced_feature, depends_on: [:basic_feature]
end
3. Feature with Field Groups
module Familia::Features::FieldGroupFeature
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def special_field(name, **)
# Define fields that belong to this feature
field(name, **)
end
end
# Register with field group
Familia::Base.add_feature self, :field_group_feature, field_group: :special_fields
end
Feature Development Best Practices
1. Naming Conventions
- Feature names should be symbols (
:my_feature) - Module names should match:
Familia::Features::MyFeature - Method names should be prefixed with feature name to avoid conflicts
2. Error Handling
module Familia::Features::RobustFeature
class FeatureError < StandardError; end
def self.included(base)
validate_environment!
base.extend ClassMethods
end
def self.validate_environment!
raise FeatureError, "Ruby 3.0+ required" if RUBY_VERSION < "3.0"
end
module ClassMethods
def robust_feature_method
raise FeatureError, "Feature not properly configured" unless configured?
# Feature logic here
end
private
def configured?
# Check if feature is properly set up
true
end
end
Familia::Base.add_feature self, :robust_feature
end
3. Feature Options
Features can accept configuration options:
class MyModel < Familia::Horreum
feature :my_feature, timeout: 30, retries: 3
end
# Access options in the feature
module Familia::Features::MyFeature
module ClassMethods
def my_feature_timeout
(:my_feature)[:timeout] || 60
end
end
end
Testing Features
Feature Testing Helpers
module FeatureTestHelpers
def with_feature(klass, feature_name, **)
original_features = klass.features_enabled.dup
begin
klass.feature(feature_name, **)
yield
ensure
# Reset features (note: this is simplified - actual reset is more complex)
klass.instance_variable_set(:@features_enabled, original_features)
end
end
def feature_enabled?(klass, feature_name)
klass.features_enabled.include?(feature_name)
end
end
# Test example
describe Familia::Features::MyFeature do
include FeatureTestHelpers
it "adds expected methods to class" do
with_feature(MyModel, :my_feature) do
expect(MyModel).to respond_to(:my_feature_config)
expect(MyModel.new).to respond_to(:my_feature_method)
end
end
it "validates dependencies" do
expect {
MyModel.feature(:advanced_feature) # requires :basic_feature
}.to raise_error(Familia::Problem, /requires missing dependencies/)
end
end
Existing Features Overview
Core Features
:expiration- TTL management for objects and fields:encrypted_fields- Encrypt sensitive fields before storage:safe_dump- API-safe serialization excluding sensitive fields:relationships- Object associations and indexing:transient_fields- Runtime-only fields that aren't persisted:quantization- Score quantization for sorted sets:object_identifier- Flexible object identification strategies:external_identifier- External system ID management
Feature Dependencies
Most features are independent, but some have dependencies:
# relationships feature has no dependencies
Familia::Base.add_feature Relationships, :relationships
# No complex dependency chains in current implementation
Debugging Features
Feature Introspection
# Check what features are available
Familia::Base.features_available.keys
# => [:expiration, :encrypted_fields, :safe_dump, :relationships, ...]
# Check what features are enabled on a class
MyModel.features_enabled
# => [:expiration, :safe_dump]
# Check feature definitions
Familia::Base.feature_definitions[:expiration]
# => #<data FeatureDefinition name=:expiration, depends_on=[], field_group=nil>
# Check if a specific feature is enabled
MyModel.features_enabled.include?(:expiration)
# => true
Common Issues
- Feature not found: Ensure the feature module is loaded and registered
- Dependency errors: Check that required features are enabled first
- Method conflicts: Features that define the same methods will override each other
Migration Notes
The feature system is intentionally simple in the current implementation. More complex features like conflict resolution, versioning, and capability flags are not currently implemented but could be added in future versions if needed.
For now, feature developers should:
- Keep features focused and independent
- Use clear naming to avoid method conflicts
- Test features thoroughly in isolation
- Document any dependencies clearly