# 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 end