{"id":16747708,"url":"https://github.com/bryanp/featuring","last_synced_at":"2025-03-16T03:10:14.497Z","repository":{"id":47004305,"uuid":"407625383","full_name":"bryanp/featuring","owner":"bryanp","description":"Feature flags for Ruby objects.","archived":false,"fork":false,"pushed_at":"2022-11-09T01:14:20.000Z","size":38,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-14T23:35:51.458Z","etag":null,"topics":["feature-flags","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bryanp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-09-17T17:23:34.000Z","updated_at":"2023-04-06T12:23:32.000Z","dependencies_parsed_at":"2023-01-23T01:45:54.514Z","dependency_job_id":null,"html_url":"https://github.com/bryanp/featuring","commit_stats":null,"previous_names":["metabahn/featuring"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanp%2Ffeaturing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanp%2Ffeaturing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanp%2Ffeaturing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryanp%2Ffeaturing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bryanp","download_url":"https://codeload.github.com/bryanp/featuring/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243818205,"owners_count":20352629,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["feature-flags","ruby"],"created_at":"2024-10-13T02:10:43.194Z","updated_at":"2025-03-16T03:10:14.475Z","avatar_url":"https://github.com/bryanp.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"**Feature flags for Ruby objects.**\n\n### Declaring Feature Flags\n\nFeature flags can be declared on modules or classes:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_feature\nend\n\nclass ObjectWithFeatures\n  extend Featuring::Declarable\n\n  feature :some_feature\nend\n```\n\nBy default, a feature flag is disabled. It can be enabled by specifying a value:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_feature, true\nend\n```\n\nFeature flags can also compute a value using a block:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_feature do\n    # perform some complex logic\n  end\nend\n```\n\nThe truthiness of the block's return value determines if the feature is enabled or disabled.\n\n### Checking Feature Flags\n\nEach feature flag has a corresponding method to check its value:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_feature\nend\n\nFeatures.some_feature?\n# =\u003e false\n```\n\nWhen using feature flags on an object, checks are available through the `features` instance method:\n\n```ruby\nclass ObjectWithFeatures\n  extend Featuring::Declarable\n\n  feature :some_feature\nend\n\ninstance = ObjectWithFeatures.new\ninstance.features.some_feature?\n# =\u003e false\n```\n\n#### Passing values to feature flag blocks\n\nWhen using feature flag blocks, values can be passed through the check method:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_feature do |value|\n    value == :some_value\n  end\nend\n\nFeatures.some_feature?(:some_value)\n# =\u003e true\n\nFeatures.some_feature?(:some_other_value)\n# =\u003e false\n```\n\n#### Truthiness 100% guaranteed\n\nCheck methods are guaranteed to only return `true` or `false`:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_feature do\n    :foo\n  end\nend\n\nFeatures.some_feature?\n# =\u003e true\n```\n\n#### Check method context\n\nCheck methods have access to their context:\n\n```ruby\nclass ObjectWithFeatures\n  extend Featuring::Declarable\n\n  feature :some_feature do\n    enabled?\n  end\n\n  def enabled?\n    true\n  end\nend\n\ninstance = ObjectWithFeatures.new\ninstance.features.some_feature?\n# =\u003e true\n```\n\nNote 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`.\n\n### Persisting Feature Flags\n\nFeature 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.\n\nEnable persistence on an object by including the adapter:\n\n```ruby\nclass ObjectWithFeatures\n  include Featuring::Persistence::ActiveRecord\n  extend Featuring::Declarable\n\n  feature :some_feature\nend\n```\n\nWhile 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`.\n\nHere's the example we'll use for the next few sections:\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  include Featuring::Persistence::ActiveRecord\n  extend Featuring::Declarable\n\n  feature :some_feature\nend\n```\n\nNothing 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:\n\n```ruby\nUser.find(1).features.some_feature?\n# =\u003e false\n```\n\n#### Persisting a default value\n\nUse the `persist` method to persist a feature flag with its default value:\n\n```ruby\nUser.find(1).features.persist :some_feature\nUser.find(1).features.some_feature?\n# =\u003e false\n```\n\nThis can be used to isolate objects from future changes to default values.\n\n#### Persisting a specific value\n\nUse the `set` method to persist a feature flag with a specific value:\n\n```ruby\nUser.find(1).features.set :some_feature, true\nUser.find(1).features.some_feature?\n# =\u003e true\n```\n\n#### Enabling a feature flag\n\nEnable a flag using the `enable` method:\n\n```ruby\nUser.find(1).features.enable :some_feature\nUser.find(1).features.some_feature?\n# =\u003e true\n```\n\n#### Disabling a feature flag\n\nDisable a flag using the `disable` method:\n\n```ruby\nUser.find(1).features.disable :some_feature\nUser.find(1).features.some_feature?\n# =\u003e false\n```\n\n#### Resetting a feature flag\n\nReset a flag using the `reset` method:\n\n```ruby\nUser.find(1).features.enable :some_feature\nUser.find(1).features.reset :some_feature\nUser.find(1).features.some_feature?\n# =\u003e false\n```\n\n#### Persisting many feature flags at once\n\nMultiple feature flags can be persisted using the `transaction` method:\n\n```ruby\nUser.find(1).features.transaction |features|\n  features.enable :some_feature\n  features.disable :some_other_feature\nend\n\nUser.find(1).features.some_feature?\n# =\u003e true\n\nUser.find(1).features.some_other_feature?\n# =\u003e false\n```\n\nPersistence 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.\n\n#### Reloading the cache\n\nFor 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:\n\n```ruby\nuser = User.find(1)\n\n# enable somewhere else\nUser.find(1).features.enable :some_feature\n\n# feature still appears disabled for existing instances\nuser.features.some_feature?\n# =\u003e false\n\n# reloading the features invalidates the cache:\nuser.features.reload\nuser.features.some_feature?\n# =\u003e true\n```\n\nWhen used in an ActiveRecord model, feature flags are automatically reloaded with the object:\n\n```ruby\nuser = User.find(1)\n\n# enable somewhere else\nUser.find(1).features.enable :some_feature\n\n# feature still appears disabled for existing instances\nuser.features.some_feature?\n# =\u003e false\n\n# reloading the model invalidates the cache:\nuser.reload\nuser.features.some_feature?\n# =\u003e true\n```\n\n#### Checking the persisted status\n\nThe persisted status of a flag can be checked with the `persisted?` method:\n\n```ruby\nUser.find(1).features.persisted?(:some_feature)\n# =\u003e false\n\nUser.find(1).features.persist :some_feature\n\nUser.find(1).features.persisted?(:some_feature)\n# =\u003e true\n```\n\nChecking if a specific value is persisted for a flag is also possible:\n\n```ruby\nUser.find(1).features.enable :some_feature\n\nUser.find(1).features.persisted?(:some_feature, true)\n# =\u003e true\n\nUser.find(1).features.persisted?(:some_feature, false)\n# =\u003e false\n```\n\nAn example of where this is useful can be found in the next section.\n\n#### A note about precedence\n\nIn 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:\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  include Featuring::Persistence::ActiveRecord\n  extend Featuring::Declarable\n\n  feature :some_feature do\n    [true, false].sample \u0026\u0026 features.persisted?(:some_feature)\n  end\nend\n```\n\n#### How ActiveRecord persistence works\n\nFeature 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:\n\n* `flaggable_id`: `integer` column containing the flaggable object id\n* `flaggable_type`: `string` column containing the flaggable object type\n* `metadata`: `jsonb` column containing the feature flag values\n\n### Composing Feature Flags\n\nFeature flags can be defined in various modules and composed together:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n  feature :some_feature, true\nend\n\nmodule AllTheFeatures\n  extend Features\n\n  extend Featuring::Declarable\n  feature :another_feature, true\nend\n\nclass ObjectWithFeatures\n  include AllTheFeatures\nend\n\ninstance = ObjectWithFeatures.new\n\ninstance.some_feature?\n# =\u003e true\n\ninstance.another_feature?\n# =\u003e true\n```\n\n#### Calling `super` for overloaded feature flags\n\nSuper is fully supported! Here's an example of how it can be useful:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_feature do\n    [true, false].sample\n  end\nend\n\nclass ObjectWithFeatures\n  include Features\n\n  extend Featuring::Declarable\n  feature :some_feature do\n    persisted?(:some_feature) || super()\n  end\nend\n\nUser.find(1).features.some_feature?\n# =\u003e true/false at random\n\nUser.find(1).features.enable :some_feature\n\nUser.find(1).features.some_feature?\n# =\u003e true (always)\n```\n\n### Serializing Feature Flags\n\nFeature flag values can be serialized using `serialize`:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_enabled_feature, true\n  feature :some_disable_feature, false\nend\n\nFeatures.serialize\n=\u003e {\n  some_enabled_feature: true,\n  some_disabled_feature: false\n}\n```\n\nAll flags, persisted or not, will be included in the result.\n\n#### Including specific feature flags\n\nInclude only specific feature flags in the serialized result using `include`:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_enabled_feature, true\n  feature :some_disable_feature, false\nend\n\nFeatures.serialize do |serializer|\n  serializer.include :some_enabled_feature\nend\n# =\u003e {\n#   some_enabled_feature: true\n# }\n```\n\n#### Excluding specific feature flags\n\nExclude specific feature flags in the serialized result using `exclude`:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_enabled_feature, true\n  feature :some_disable_feature, false\nend\n\nFeatures.serialize do |serializer|\n  serializer.exclude :some_enabled_feature\nend\n# =\u003e {\n#   some_disabled_feature: false\n# }\n```\n\n#### Providing context for complex feature flags\n\nSerializing complex feature flags will fail if they require an argument:\n\n```ruby\nmodule Features\n  extend Featuring::Declarable\n\n  feature :some_complex_feature do |value|\n    value == :some_value\n  end\nend\n\nFeatures.serialize\n# =\u003e ArgumentError\n```\n\nContext can be provided for these feature flag using `context`:\n\n```ruby\nFeatures.serialize do |serializer|\n  serializer.context :some_complex_feature, :some_value\nend\n# =\u003e {\n#   some_complex_feature: true\n# }\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbryanp%2Ffeaturing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbryanp%2Ffeaturing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbryanp%2Ffeaturing/lists"}