Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,20 @@ GEM
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
bootsnap (1.18.4)
bootsnap (1.18.6)
msgpack (~> 1.2)
builder (3.3.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crass (1.0.6)
database_cleaner-active_record (2.2.0)
database_cleaner-active_record (2.2.1)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.4.1)
diff-lcs (1.6.1)
drb (2.2.1)
diff-lcs (1.6.2)
drb (2.2.3)
erb (5.0.1)
erubi (1.13.1)
globalid (1.2.1)
activesupport (>= 6.1)
Expand All @@ -104,11 +105,11 @@ GEM
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.11.3)
language_server-protocol (3.17.0.4)
json (2.12.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
Expand Down Expand Up @@ -154,12 +155,12 @@ GEM
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
psych (5.2.3)
psych (5.2.6)
date
stringio
racc (1.8.1)
rack (3.1.13)
rack-session (2.1.0)
rack (3.1.15)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
Expand All @@ -180,7 +181,7 @@ GEM
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 8.0.2)
rails-dom-testing (2.2.0)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
Expand All @@ -197,7 +198,8 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.13.1)
rdoc (6.14.0)
erb
psych (>= 4.0.0)
redcarpet (3.6.1)
regexp_parser (2.10.0)
Expand All @@ -212,7 +214,7 @@ GEM
rspec-expectations (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.3)
rspec-mocks (3.13.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.1.1)
Expand All @@ -224,7 +226,7 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.3)
rubocop (1.75.4)
rubocop (1.75.7)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
Expand Down Expand Up @@ -263,7 +265,7 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
yard (0.9.37)
zeitwerk (2.7.2)
zeitwerk (2.7.3)

PLATFORMS
aarch64-linux-gnu
Expand Down
77 changes: 40 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,50 +457,53 @@ Here’s what the result might look like:

```ruby
[
{
assignable: "User",
total: 50,
enabled_count: 20,
disabled_count: 30,
percentage_enabled: "40.0%",
percentage_disabled: "60.0%",
first_created: 2025-04-21 01:43:28.266664000 UTC +00:00,
last_created: 2025-05-21 19:18:37.772172000 UTC +00:00,
past_7: 9,
past_14: 11,
past_30: 0
},
{
assignable: "Account",
total: 4,
enabled_count: 2,
disabled_count: 0,
percentage_enabled: "100.0%",
percentage_disabled: "0.0%",
first_created: 2025-04-21 01:43:28.266664000 UTC +00:00,
last_created: 2025-05-21 19:18:37.772172000 UTC +00:00,
past7: 2,
past14: 0,
past30: 0
}
{
assignable: "User",
feature: :teleportation,
total: 50,
enabled_count: 20,
disabled_count: 30,
percentage_enabled: "40.0%",
percentage_disabled: "60.0%",
first_created: 2025-04-21 01:43:28.266664000 UTC +00:00,
last_created: 2025-05-21 19:18:37.772172000 UTC +00:00,
past_7: 9,
past_14: 11,
past_30: 0
},
{
assignable: "Account",
feature: :teleportation,
total: 4,
enabled_count: 2,
disabled_count: 0,
percentage_enabled: "100.0%",
percentage_disabled: "0.0%",
first_created: 2025-04-21 01:43:28.266664000 UTC +00:00,
last_created: 2025-05-21 19:18:37.772172000 UTC +00:00,
past_7: 2,
past_14: 0,
past_30: 0
}
]
```

So, how you should interpret that data?

Here’s what each field means:

- `assignable`: The model that includes Togglefy::Assignable.
- `total`: Total number of assignables in your system.
- `enabled_count`: Number of assignables that have the feature enabled.
- `disabled_count`: Number of assignables that have the feature disabled.
- `percentage_enabled`: Percentage of assignables with the feature enabled.
- `percentage_disabled`: Percentage of assignables with the feature disabled.
- `first_created`: The first time the feature was toggled on for this assignable.
- `last_created`: The most recent time the feature was toggled on for this assignable.
- `past_7`: Number of times the feature was toggled on in the past 7 days.
- `past_14`: Number of times the feature was toggled on in the past 14 days.
- `past_30`: Number of times the feature was toggled on in the past 30 days.
* `assignable`: The model that includes Togglefy::Assignable.
* `feature`: The identifier of the feature being tracked (e.g., `:teleportation`).
* `total`: Total number of assignables in your system.
* `enabled_count`: Number of assignables that have the feature enabled.
* `disabled_count`: Number of assignables that have the feature disabled.
* `percentage_enabled`: Percentage of assignables with the feature enabled.
* `percentage_disabled`: Percentage of assignables with the feature disabled.
* `first_created`: The first time the feature was toggled on for this assignable.
* `last_created`: The most recent time the feature was toggled on for this assignable.
* `past_7`: Number of times the feature was toggled on in the past 7 days.
* `past_14`: Number of times the feature was toggled on in the past 14 days.
* `past_30`: Number of times the feature was toggled on in the past 30 days.

## Aliases

Expand Down
119 changes: 8 additions & 111 deletions app/models/togglefy/feature.rb
Original file line number Diff line number Diff line change
@@ -1,114 +1,11 @@
# frozen_string_literal: true

module Togglefy
# Represents a feature in the Togglefy system.
# A feature can have various attributes such as name, identifier, status, and associations with assignables.
class Feature < (defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base)
# Enum for feature status.
# If Rails version is 7 or higher, use the new enum syntax.
# If Rails version is lower, use the old enum syntax.
# @!attribute [r] status
# @return [Symbol] The status of the feature (:inactive or :active).
if Rails::VERSION::MAJOR >= 7
enum :status, %i[inactive active]
else
enum status: %i[inactive active]
end

# Associations
# @!attribute [rw] feature_assignments
# @return [ActiveRecord::Relation] The feature assignments associated with this feature.
has_many :feature_assignments, dependent: :destroy

# Callbacks
# Builds an identifier for the feature before validation if the name is present and identifier is blank.
before_validation :build_identifier, if: proc { |f| f.name.present? && f.identifier.blank? }

# Scopes
# Finds features by their identifier.
# @param identifier [Symbol, String, Array<Symbol, String>] The identifier to search for.
# @return [ActiveRecord::Relation] The features matching the identifier.
scope :identifier, ->(identifier) { where(identifier: identifier) }

# Finds features by their group.
# @param group [String] The group to search for.
# @return [ActiveRecord::Relation] The features matching the group.
scope :for_group, ->(group) { where(group: group) }

# Finds features without a group.
# @return [ActiveRecord::Relation] The features without a group.
scope :without_group, -> { where(group: nil) }

# Finds features by their environment.
# @param environment [String] The environment to search for.
# @return [ActiveRecord::Relation] The features matching the environment.
scope :for_environment, ->(environment) { where(environment: environment) }

# Finds features without an environment.
# @return [ActiveRecord::Relation] The features without an environment.
scope :without_environment, -> { where(environment: nil) }

# Finds features by their tenant ID.
# @param tenant_id [String] The tenant ID to search for.
# @return [ActiveRecord::Relation] The features matching the tenant ID.
scope :for_tenant, ->(tenant_id) { where(tenant_id: tenant_id) }

# Finds features without a tenant.
# @return [ActiveRecord::Relation] The features without a tenant.
scope :without_tenant, -> { where(tenant_id: nil) }

# Finds features with an inactive status.
# @return [ActiveRecord::Relation] The features with an inactive status.
scope :inactive, -> { where(status: :inactive) }

# Finds features with an active status.
# @return [ActiveRecord::Relation] The features with an active status.
scope :active, -> { where(status: :active) }

# Finds features by their status.
# @param status [Symbol, String, Integer] The status to search
# (:inactive || "inactive" || 0) or (:active || "active" || 1).
# @return [ActiveRecord::Relation] The features matching the status.
scope :with_status, ->(status) { where(status: status) }

# Validations
# Validates the presence and uniqueness of the name and identifier attributes.
validates :name, :identifier, presence: true, uniqueness: true
validates :identifier, format: {
with: /\A[a-z]+(_[a-z0-9]+)*\z/,
message: "must be in snake_case (lowercase letters and underscores only)"
}

# This method retrieves all assignables linked to the feature through feature assignments.
# @return [ActiveRecord::Relation] The assignables associated with the feature.
# @example
# feature.assignables
# Togglefy.feature(:super_powers).assignables
# Togglefy::Feature.find_by(identifier: :super_powers).assignables
# @note This method includes all assignables, regardless of their class.
def assignables
feature_assignments.includes(:assignable).map(&:assignable)
end

# This method retrieves assignables of a specific class linked to the feature through feature assignments.
# @param klass [String, Class] The class name or class of the assignable class.
# @return [ActiveRecord::Relation] The assignables of the specified class associated with the feature.
# @example
# feature.assignables_for_klass("User")
# feature.assignables_for_klass(User)
# Togglefy.feature(:super_powers).assignables_for_klass(User)
# Togglefy::Feature.find_by(identifier: :super_powers).assignables_for_klass(User)
def assignables_for_type(klass)
feature_assignments.includes(:assignable).where(assignable_type: klass.to_s).map(&:assignable)
end

private

# Builds a unique identifier for the feature based on its name.
# The identifier is generated by parameterizing the name with underscores.
# @return [void]
def build_identifier
self.identifier = name.underscore.parameterize(separator: "_")
end
end
ROOT_PATH = "../../.."

if Rails::VERSION::MAJOR >= 7
require_relative "#{ROOT_PATH}/lib/models/rails_7/feature"
elsif Rails::VERSION::MAJOR >= 5
require_relative "#{ROOT_PATH}/lib/models/rails_5/feature"
else
require_relative "#{ROOT_PATH}/lib/models/rails_legacy/feature"
end
25 changes: 7 additions & 18 deletions app/models/togglefy/feature_assignment.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
# frozen_string_literal: true

module Togglefy
# Represents the assignment of a feature to an assignable entity.
# A feature assignment links a feature to an assignable object, which can be of any polymorphic type.
class FeatureAssignment < ApplicationRecord
# Associations
# @!attribute [rw] feature
# @return [Feature] The feature associated with this assignment.
belongs_to :feature, class_name: "Togglefy::Feature"
ROOT_PATH = "../../.."

# @!attribute [rw] assignable
# @return [Object] The polymorphic assignable object associated with this assignment.
belongs_to :assignable, polymorphic: true

# Scopes
# Finds feature assignments for a specific assignable type.
# @param klass [Class] The class type of the assignable.
# @return [ActiveRecord::Relation] The feature assignments for the given type.
scope :for_type, ->(klass) { where(assignable_type: klass.to_s) }
end
if Rails::VERSION::MAJOR >= 7
require_relative "#{ROOT_PATH}/lib/models/rails_7/feature_assignment"
elsif Rails::VERSION::MAJOR >= 5
require_relative "#{ROOT_PATH}/lib/models/rails_5/feature_assignment"
else
require_relative "#{ROOT_PATH}/lib/models/rails_legacy/feature_assignment"
end
Loading