Summoner Engine

An advanced, flexible, and powerful Feature Toggle, Dynamic Configuration, and Attribute-Based Access Control (ABAC) engine for Ruby on Rails.

Summoner Engine goes way beyond simple booleans. It allows you to manage global or Entity-specific configurations (e.g., User, Account), perform gradual rollouts based on attributes (like role or plan), attach editable descriptions to each feature, and create surgical exceptions (overrides) directly through an elegant Web UI. All wrapped in a lightning-fast Caching layer to protect your database.

Table of Contents

Key Features

  • Dynamic Entity Injection: Features become native methods on your models (e.g., user.new_dashboard?).
  • Attribute-Based Access Control (ABAC): Automatically evaluate feature access by matching permission arrays against entity attributes (e.g., checking if a user's role is in the allowed list).
  • Instant Global Rollouts: Use the wildcard ["*"] to release a feature to everyone in seconds, without touching the codebase.
  • Granular Overrides: The default rule might be X, but for User ID 42, the rule is Y. Summoner manages these exceptions flawlessly.
  • Strong Typing: Native support for complex data types (boolean, integer, float, json, string).
  • YAML Synchronization: Keep a features.yml file as your single source of truth and sync it to the database on every deploy. YAML changes update the tracked defaults and descriptions, while manual dashboard edits remain in place until the YAML changes again.
  • Built-in Web Dashboard: A mountable Rails Engine for you and your team to manage everything visually.

Installation

Add this line to your application's Gemfile:

gem "summoner-engine"

The gem name is summoner-engine, but the internal Ruby namespace remains Summoner, so existing code and configuration can keep using Summoner::Entity, Summoner::Sync, and Summoner.configure.

Install the gem and run the installation generator:

bundle install
rails generate summoner:install

The generator will create the database migrations, an initializer, and your config/features.yml template. Then, build the tables:

rails db:migrate

Configuration

You can customize the engine's behavior, especially the built-in Cache, in config/initializers/summoner.rb:

Summoner.configure do |config|
  # Enables or disables the use of Rails.cache (Default: true)
  config.cache_enabled = true
  
  # A safe namespace to prevent key collisions in Redis/Memcached
  config.cache_namespace = 'summoner'

  # Defines expiration time for the cache (Default: 1.hour)
  config.cache_expires_in = 1.hour
end

The Web Dashboard

To manage your features and overrides visually, mount the UI inside your config/routes.rb:

Rails.application.routes.draw do
  # ... your application routes ...
  mount Summoner::Engine => "/summoner"
end

Now visit http://localhost:3000/summoner to see the magic happen!

image

Usage

Summoner's true power lies in its organization by Namespaces (Entities) and Data Types.

1. Defining your Source of Truth (features.yml)

Declare your application's base behavior in your config/features.yml. The structure uses the Entity name as the root key. Each feature can define a default value and an optional description:

# =========================================================================
# Whenever you update this file, run in your terminal: rails summoner:sync
# =========================================================================

user:
  # Example 1: Simple Boolean injected into the model
  "new_dashboard?":
    default: false
    description: Enables the new dashboard experience for users.

  # Example 2: ABAC (Attribute-Based Access Control)
  # Checks if the User's `role` method matches the values in the Array
  "can_access_admin?":
    default: ["admin", "manager"]
    match_attribute: "role"
    description: Allow admins and managers to access the admin area.

  # Example 3: Global Release with Wildcard ("*")
  # Everyone gets access, ignoring the `role` attribute entirely!
  "can_access_beta?":
    default: ["*"]
    match_attribute: "role"

  # Example 4: Dynamic Values (Integer, String, JSON)
  max_items_per_page:
    default: 20
    description: Maximum number of items shown per page.

system:
  # Features that do not belong to a specific entity
  maintenance_mode:
    default: false
    description: Toggle the maintenance page for all users.

app:
  # Global application configurations
  api_timeout:
    default: 30
    description: Request timeout in seconds for external API calls.

Sync these definitions with your database by running:

rails summoner:sync

If you edit a feature through the dashboard, the manual change stays active until the same feature changes in features.yml again. At that point, Summoner updates the tracked YAML value and YAML description back to match the file.

2. Setting up your Entities

To allow Summoner to inject these configurations directly into your classes (like your User model), simply include the core module:

class User < ApplicationRecord
  include Summoner::Entity
end

3. Checking Features in your Code

Summoner exposes an elegant, fluent API with automatic caching out of the box.

A. Checking Boolean Toggles and ABAC: Keys ending with a ? in your YAML are converted into direct query methods on your entity:

user = User.first

# Respects the YAML default or a database Override
if user.new_dashboard?
  render "dashboards/v2"
else
  render "dashboards/v1"
end

# Automatically evaluates the ABAC array against user.role!
user.can_access_admin? # => true (if the role is "admin")

B. Getting Complex Typed Values: To retrieve integers, strings, or JSON, just call the exact key name:

# Returns 20 (or the specific overridden value for this user)
limit = user.max_items_per_page 

Product.limit(limit)

C. Global Features (Without Entities): Not every feature is tied to a user or an account. You can create arbitrary namespaces in your features.yml (like system:, app:, or global:) to group application-wide settings.

Since these don't belong to an Active Record model, you can query them directly through the Summoner module using the "namespace.feature_key" format:

# Checking a global boolean toggle
if Summoner.active?("system.maintenance_mode")
  redirect_to maintenance_path
end

# Getting a global typed value
api_timeout = Summoner.get("app.api_timeout")

Targeted Overrides and Rollouts

Your YAML defines the "General Rule," but real-world apps require exceptions. Through the Web Dashboard, you can create Overrides for specific instances.

Common Use Cases:

  • Beta Testing: The user.new_dashboard? feature is false for everyone, but you create an Override set to true strictly for User ID: 1 and User ID: 42.
  • Tenant Customization: The max_items_per_page limit is 20, but you sold a VIP plan to User ID: 99 and created an Override with the value 100 just for them.
  • ABAC Bypass: can_access_admin? only allows ["admin"], but you want to grant temporary access to a specific regular user.

Summoner's Evaluation Engine always respects the following priority hierarchy:
Entity-specific Override > ABAC / Wildcard Rule > Global Default Value.

image

Contributing

Bug reports and pull requests are warmly welcome on GitHub at https://github.com/feliperodrigs1/summoner.

License

The gem is available as open source under the terms of the MIT License.