Project

featuring

0.0
No release in over a year
Feature flags for Ruby objects.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

Feature flags for Ruby objects.

Declaring Feature Flags

Feature flags can be declared on modules or classes:

module Features
  extend Featuring::Declarable

  feature :some_feature
end

class ObjectWithFeatures
  extend Featuring::Declarable

  feature :some_feature
end

By default, a feature flag is disabled. It can be enabled by specifying a value:

module Features
  extend Featuring::Declarable

  feature :some_feature, true
end

Feature flags can also compute a value using a block:

module Features
  extend Featuring::Declarable

  feature :some_feature do
    # perform some complex logic
  end
end

The truthiness of the block's return value determines if the feature is enabled or disabled.

Checking Feature Flags

Each feature flag has a corresponding method to check its value:

module Features
  extend Featuring::Declarable

  feature :some_feature
end

Features.some_feature?
# => false

When using feature flags on an object, checks are available through the features instance method:

class ObjectWithFeatures
  extend Featuring::Declarable

  feature :some_feature
end

instance = ObjectWithFeatures.new
instance.features.some_feature?
# => false

Passing values to feature flag blocks

When using feature flag blocks, values can be passed through the check method:

module Features
  extend Featuring::Declarable

  feature :some_feature do |value|
    value == :some_value
  end
end

Features.some_feature?(:some_value)
# => true

Features.some_feature?(:some_other_value)
# => false

Truthiness 100% guaranteed

Check methods are guaranteed to only return true or false:

module Features
  extend Featuring::Declarable

  feature :some_feature do
    :foo
  end
end

Features.some_feature?
# => true

Check method context

Check methods have access to their context:

class ObjectWithFeatures
  extend Featuring::Declarable

  feature :some_feature do
    enabled?
  end

  def enabled?
    true
  end
end

instance = ObjectWithFeatures.new
instance.features.some_feature?
# => true

Note that this happens through delegators, which means that instance variables are not accessible to the feature flag. For cases like this, define an attr_accessor.

Persisting Feature Flags

Feature flag persistence can be added to any object with feature flags. Right now, persistence to an ActiveRecord model is supported. Postgres is currently the only supported database.

Enable persistence on an object by including the adapter:

class ObjectWithFeatures
  include Featuring::Persistence::ActiveRecord
  extend Featuring::Declarable

  feature :some_feature
end

While persistence is anticipated to be used mostly for other ActiveRecord models, feature flags can be persisted for any object that exposes a deterministic value for id.

Here's the example we'll use for the next few sections:

class User < ActiveRecord::Base
  include Featuring::Persistence::ActiveRecord
  extend Featuring::Declarable

  feature :some_feature
end

Nothing is persisted by default. Instead, each feature flag must be persisted explicitly. This means that by default, checks fall back to the default value of a feature flag:

User.find(1).features.some_feature?
# => false

Persisting a default value

Use the persist method to persist a feature flag with its default value:

User.find(1).features.persist :some_feature
User.find(1).features.some_feature?
# => false

This can be used to isolate objects from future changes to default values.

Persisting a specific value

Use the set method to persist a feature flag with a specific value:

User.find(1).features.set :some_feature, true
User.find(1).features.some_feature?
# => true

Enabling a feature flag

Enable a flag using the enable method:

User.find(1).features.enable :some_feature
User.find(1).features.some_feature?
# => true

Disabling a feature flag

Disable a flag using the disable method:

User.find(1).features.disable :some_feature
User.find(1).features.some_feature?
# => false

Resetting a feature flag

Reset a flag using the reset method:

User.find(1).features.enable :some_feature
User.find(1).features.reset :some_feature
User.find(1).features.some_feature?
# => false

Persisting many feature flags at once

Multiple feature flags can be persisted using the transaction method:

User.find(1).features.transaction |features|
  features.enable :some_feature
  features.disable :some_other_feature
end

User.find(1).features.some_feature?
# => true

User.find(1).features.some_other_feature?
# => false

Persistence happens in one step. Using the ActiveRecord adapter, all feature flag changes within the transaction block will be committed in a single INSERT or UPDATE query.

Reloading the cache

For performance, persisted feature flags are loaded only once for an instance. This means if a different value is persisted for a feature flag in another part of the system, the change won't be immediately available to other instances until they are reloaded:

user = User.find(1)

# enable somewhere else
User.find(1).features.enable :some_feature

# feature still appears disabled for existing instances
user.features.some_feature?
# => false

# reloading the features invalidates the cache:
user.features.reload
user.features.some_feature?
# => true

When used in an ActiveRecord model, feature flags are automatically reloaded with the object:

user = User.find(1)

# enable somewhere else
User.find(1).features.enable :some_feature

# feature still appears disabled for existing instances
user.features.some_feature?
# => false

# reloading the model invalidates the cache:
user.reload
user.features.some_feature?
# => true

Checking the persisted status

The persisted status of a flag can be checked with the persisted? method:

User.find(1).features.persisted?(:some_feature)
# => false

User.find(1).features.persist :some_feature

User.find(1).features.persisted?(:some_feature)
# => true

Checking if a specific value is persisted for a flag is also possible:

User.find(1).features.enable :some_feature

User.find(1).features.persisted?(:some_feature, true)
# => true

User.find(1).features.persisted?(:some_feature, false)
# => false

An example of where this is useful can be found in the next section.

A note about precedence

In most cases, a feature flag's persisted value takes precedence over its default value. The single exception to this rule is when using feature flags defined with blocks. If the persisted value is false, the persisted value is always given precedence. But if the persisted value is true, the value returned from the block must also be truthy. This lets us do complex things like enable a feature 50% of the time for users that are given explicit access to a feature:

class User < ActiveRecord::Base
  include Featuring::Persistence::ActiveRecord
  extend Featuring::Declarable

  feature :some_feature do
    [true, false].sample && features.persisted?(:some_feature)
  end
end

How ActiveRecord persistence works

Feature flags are persisted to a database table with a polymorphic association to flaggable objects. By default, the ActiveRecord adapter expects a top-level FeatureFlag model to be available, along with a feature_flags database table. The table is expected to contain the following fields:

  • flaggable_id: integer column containing the flaggable object id
  • flaggable_type: string column containing the flaggable object type
  • metadata: jsonb column containing the feature flag values

Composing Feature Flags

Feature flags can be defined in various modules and composed together:

module Features
  extend Featuring::Declarable
  feature :some_feature, true
end

module AllTheFeatures
  extend Features

  extend Featuring::Declarable
  feature :another_feature, true
end

class ObjectWithFeatures
  include AllTheFeatures
end

instance = ObjectWithFeatures.new

instance.some_feature?
# => true

instance.another_feature?
# => true

Calling super for overloaded feature flags

Super is fully supported! Here's an example of how it can be useful:

module Features
  extend Featuring::Declarable

  feature :some_feature do
    [true, false].sample
  end
end

class ObjectWithFeatures
  include Features

  extend Featuring::Declarable
  feature :some_feature do
    persisted?(:some_feature) || super()
  end
end

User.find(1).features.some_feature?
# => true/false at random

User.find(1).features.enable :some_feature

User.find(1).features.some_feature?
# => true (always)

Serializing Feature Flags

Feature flag values can be serialized using serialize:

module Features
  extend Featuring::Declarable

  feature :some_enabled_feature, true
  feature :some_disable_feature, false
end

Features.serialize
=> {
  some_enabled_feature: true,
  some_disabled_feature: false
}

All flags, persisted or not, will be included in the result.

Including specific feature flags

Include only specific feature flags in the serialized result using include:

module Features
  extend Featuring::Declarable

  feature :some_enabled_feature, true
  feature :some_disable_feature, false
end

Features.serialize do |serializer|
  serializer.include :some_enabled_feature
end
# => {
#   some_enabled_feature: true
# }

Excluding specific feature flags

Exclude specific feature flags in the serialized result using exclude:

module Features
  extend Featuring::Declarable

  feature :some_enabled_feature, true
  feature :some_disable_feature, false
end

Features.serialize do |serializer|
  serializer.exclude :some_enabled_feature
end
# => {
#   some_disabled_feature: false
# }

Providing context for complex feature flags

Serializing complex feature flags will fail if they require an argument:

module Features
  extend Featuring::Declarable

  feature :some_complex_feature do |value|
    value == :some_value
  end
end

Features.serialize
# => ArgumentError

Context can be provided for these feature flag using context:

Features.serialize do |serializer|
  serializer.context :some_complex_feature, :some_value
end
# => {
#   some_complex_feature: true
# }