togglefy 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,48 +1,75 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/concern"
2
4
 
3
5
  module Togglefy
6
+ # The Assignable module provides functionality for models to relate with features.
7
+ # It includes methods to add, remove, and query features, as well as ActiveRecord scopes.
4
8
  module Assignable
5
9
  extend ActiveSupport::Concern
6
10
 
7
11
  included do
12
+ # Establishes a many-to-many relationship with features through feature assignments.
8
13
  has_many :feature_assignments, as: :assignable, class_name: "Togglefy::FeatureAssignment"
9
14
  has_many :features, through: :feature_assignments, class_name: "Togglefy::Feature"
10
15
 
11
- scope :with_features, ->(feature_ids) {
16
+ # Scope to retrieve assignables with specific features.
17
+ #
18
+ # @param feature_ids [Array<Integer>] The IDs of the features to filter by.
19
+ scope :with_features, lambda { |feature_ids|
12
20
  joins(:feature_assignments)
13
- .where(feature_assignments: {
14
- feature_id: feature_ids
15
- })
16
- .distinct
21
+ .where(feature_assignments: {
22
+ feature_id: feature_ids
23
+ })
24
+ .distinct
17
25
  }
18
26
 
19
- scope :without_features, ->(feature_ids) {
27
+ # Scope to retrieve assignables without specific features.
28
+ #
29
+ # @param feature_ids [Array<Integer>] The IDs of the features to filter by.
30
+ scope :without_features, lambda { |feature_ids|
20
31
  joins(left_join_on_features(feature_ids))
21
32
  .where("fa.id IS NULL")
22
33
  .distinct
23
34
  }
24
35
  end
25
36
 
26
- def has_feature?(identifier)
27
- features.exists?(identifier: identifier.to_s)
37
+ # Checks if the assignable has a specific feature.
38
+ #
39
+ # @param identifier [Symbol, String] The identifier of the feature.
40
+ # @return [Boolean] True if the feature exists, false otherwise.
41
+ def feature?(identifier)
42
+ features.active.exists?(identifier: identifier.to_s)
28
43
  end
44
+ alias has_feature? feature?
29
45
 
46
+ # Adds a feature to the assignable.
47
+ #
48
+ # @param feature [Togglefy::Feature, String] The feature or its identifier.
30
49
  def add_feature(feature)
31
50
  feature = find_feature!(feature)
32
51
  features << feature unless has_feature?(feature.identifier)
33
52
  end
34
53
 
54
+ # Removes a feature from the assignable.
55
+ #
56
+ # @param feature [Togglefy::Feature, String] The feature or its identifier.
35
57
  def remove_feature(feature)
36
58
  feature = find_feature!(feature)
37
59
  features.destroy(feature) if has_feature?(feature.identifier)
38
60
  end
39
61
 
62
+ # Clears all features from the assignable.
40
63
  def clear_features
41
64
  features.destroy_all
42
65
  end
43
66
 
44
67
  private
45
68
 
69
+ # Finds a feature by its identifier or returns the feature if already provided.
70
+ #
71
+ # @param feature [Togglefy::Feature, String] The feature or its identifier.
72
+ # @return [Togglefy::Feature] The found feature.
46
73
  def find_feature!(feature)
47
74
  return feature if feature.is_a?(Togglefy::Feature)
48
75
 
@@ -50,9 +77,13 @@ module Togglefy
50
77
  end
51
78
 
52
79
  class_methods do
80
+ # Generates a SQL LEFT JOIN clause for features.
81
+ #
82
+ # @param feature_ids [Array<Integer>] The IDs of the features to join on.
83
+ # @return [String] The SQL LEFT JOIN clause.
53
84
  def left_join_on_features(feature_ids)
54
- table = self.table_name
55
- type = self.name
85
+ table = table_name
86
+ type = name
56
87
 
57
88
  <<~SQL.squish
58
89
  LEFT JOIN togglefy_feature_assignments fa
@@ -63,4 +94,4 @@ module Togglefy
63
94
  end
64
95
  end
65
96
  end
66
- end
97
+ end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The Engine class integrates the Togglefy gem with a Rails application.
5
+ # It isolates the namespace to avoid conflicts with other parts of the application.
2
6
  class Engine < ::Rails::Engine
3
7
  isolate_namespace Togglefy
4
8
  end
5
- end
9
+ end
@@ -1,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The AssignablesNotFound class represents an error raised when no assignables
5
+ # match the provided features and filters.
2
6
  class AssignablesNotFound < Togglefy::Error
7
+ # Initializes a new AssignablesNotFound error.
8
+ #
9
+ # @param klass [Class] The class of the assignable.
3
10
  def initialize(klass)
4
11
  super("No #{klass.name} found matching features and filters sent")
5
12
  end
6
13
  end
7
- end
14
+ end
@@ -1,11 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The BulkToggleFailed error is raised when a bulk toggle operation fails.
5
+ # This error provides additional context by allowing an optional cause to be specified.
2
6
  class BulkToggleFailed < Togglefy::Error
7
+ # Initializes a new BulkToggleFailed error.
8
+ #
9
+ # @param message [String] The error message (default: "Bulk toggle operation failed").
10
+ # @param cause [Exception, nil] The underlying cause of the error, if any.
11
+ # @return [BulkToggleFailed] A new instance of the error.
3
12
  def initialize(message = "Bulk toggle operation failed", cause = nil)
4
13
  super(message)
5
14
  set_backtrace(cause.backtrace) if cause
6
15
  @cause = cause
7
16
  end
8
17
 
18
+ # @!attribute [r] cause
19
+ # @return [Exception, nil] The underlying cause of the error, if any.
9
20
  attr_reader :cause
10
21
  end
11
- end
22
+ end
@@ -1,7 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The DependencyMissing class represents an error raised when a feature
5
+ # is missing a required dependency.
2
6
  class DependencyMissing < Togglefy::Error
7
+ # Initializes a new DependencyMissing error.
8
+ #
9
+ # @param feature [String] The name of the feature.
10
+ # @param required [String] The name of the missing dependency.
3
11
  def initialize(feature, required)
4
12
  super("Feature '#{feature}' is missing dependency: '#{required}'")
5
13
  end
6
14
  end
7
- end
15
+ end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The Error class is the base class for all custom exceptions in the Togglefy gem.
5
+ # It inherits from StandardError to provide a consistent error hierarchy.
2
6
  class Error < StandardError; end
3
- end
7
+ end
@@ -1,7 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The FeatureNotFound class represents an error raised when no features
5
+ # match the provided criteria or filters.
2
6
  class FeatureNotFound < Togglefy::Error
3
- def initialize
4
- super("No features found matching features and/or filters sent")
7
+ # Initializes a new FeatureNotFound error.
8
+ def initialize(message = "No features found matching features, identifiers and/or filters sent", cause = nil)
9
+ super(message)
10
+ set_backtrace(cause.backtrace) if cause
11
+ @cause = cause
5
12
  end
13
+
14
+ # @!attribute [r] cause
15
+ # @return [Exception, nil] The underlying cause of the error, if any.
16
+ attr_reader :cause
6
17
  end
7
- end
18
+ end
@@ -1,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The InvalidFeatureAttribute class represents an error raised when an
5
+ # invalid attribute is provided for a Togglefy::Feature.
2
6
  class InvalidFeatureAttribute < Togglefy::Error
7
+ # Initializes a new InvalidFeatureAttribute error.
8
+ #
9
+ # @param attr [String] The name of the invalid attribute.
3
10
  def initialize(attr)
4
11
  super("The attribute '#{attr}' is not valid for Togglefy::Feature.")
5
12
  end
6
13
  end
7
- end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file loads all custom exception classes used in the Togglefy gem.
4
+ require "togglefy/errors/error"
5
+
6
+ require "togglefy/errors/feature_not_found"
7
+ require "togglefy/errors/assignables_not_found"
8
+ require "togglefy/errors/bulk_toggle_failed"
9
+
10
+ module Togglefy
11
+ # Custom error class for Togglefy-specific errors.
12
+ class Error < ::StandardError; end
13
+
14
+ # Overwrites the default StandardError class to provide a custom error class for Togglefy.
15
+ StandardError = Class.new(Error)
16
+ end
@@ -1,30 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The FeatureAssignableManager class provides methods to manage features
5
+ # for an assignable object, such as enabling, disabling, and clearing features.
2
6
  class FeatureAssignableManager
7
+ # Initializes a new FeatureAssignableManager.
8
+ #
9
+ # @param assignable [Object] The assignable object to manage features for.
3
10
  def initialize(assignable)
4
11
  @assignable = assignable
5
12
  end
6
13
 
14
+ # Enables a feature for the assignable.
15
+ #
16
+ # @param feature [Togglefy::Feature, Symbol, String] The feature or its identifier.
17
+ # @return [FeatureAssignableManager] Returns self for method chaining.
7
18
  def enable(feature)
8
19
  assignable.add_feature(feature)
9
20
  self
10
21
  end
11
22
 
23
+ # Disables a feature for the assignable.
24
+ #
25
+ # @param feature [Togglefy::Feature, Symbol, String] The feature or its identifier.
26
+ # @return [FeatureAssignableManager] Returns self for method chaining.
12
27
  def disable(feature)
13
28
  assignable.remove_feature(feature)
14
29
  self
15
30
  end
16
31
 
32
+ # Clears all features from the assignable.
33
+ #
34
+ # @return [FeatureAssignableManager] Returns self for method chaining.
17
35
  def clear
18
36
  assignable.clear_features
19
37
  self
20
38
  end
21
39
 
40
+ # Checks if the assignable has a specific feature.
41
+ #
42
+ # @param feature [Togglefy::Feature, Symbol, String] The feature or its identifier.
43
+ # @return [Boolean] True if the feature exists, false otherwise.
22
44
  def has?(feature)
23
45
  assignable.has_feature?(feature)
24
46
  end
25
47
 
26
48
  private
27
49
 
50
+ # @return [Object] The assignable object being managed.
28
51
  attr_reader :assignable
29
52
  end
30
53
  end
@@ -1,41 +1,74 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The FeatureManager class provides methods to manage features, including
5
+ # creating, updating, toggling, and destroying them.
2
6
  class FeatureManager
7
+ # Initializes a new FeatureManager.
8
+ #
9
+ # nil only applies when creating a new feature.
10
+ #
11
+ # @param identifier [String, nil] The identifier of the feature to manage.
3
12
  def initialize(identifier = nil)
4
13
  @identifier = identifier unless identifier.nil?
5
14
  end
6
15
 
16
+ # Creates a new feature with the given parameters.
17
+ #
18
+ # @param params [Hash] The attributes for the new feature.
19
+ # @return [Togglefy::Feature] The created feature.
7
20
  def create(**params)
8
21
  Togglefy::Feature.create!(**params)
9
22
  end
10
23
 
24
+ # Updates the feature with the given parameters.
25
+ #
26
+ # @param params [Hash] The attributes to update the feature with.
27
+ # @return [Togglefy::Feature] The updated feature.
11
28
  def update(**params)
12
29
  feature.update!(**params)
13
30
  end
14
31
 
32
+ # Destroys the feature.
33
+ #
34
+ # @return [Togglefy::Feature] The destroyed feature.
15
35
  def destroy
16
36
  feature.destroy
17
37
  end
18
38
 
39
+ # Toggles the feature's status between active and inactive.
40
+ #
41
+ # @return [Togglefy::Feature] The toggled feature.
19
42
  def toggle
20
43
  return feature.inactive! if feature.active?
21
44
 
22
45
  feature.active!
23
46
  end
24
47
 
48
+ # Activates the feature.
49
+ #
50
+ # @return [Togglefy::Feature] The activated feature.
25
51
  def active!
26
52
  feature.active!
27
53
  end
28
54
 
55
+ # Deactivates the feature.
56
+ #
57
+ # @return [Togglefy::Feature] The deactivated feature.
29
58
  def inactive!
30
59
  feature.inactive!
31
60
  end
32
61
 
33
62
  private
34
63
 
64
+ # @return [String, nil] The identifier of the feature being managed.
35
65
  attr_reader :identifier
36
66
 
67
+ # Finds the feature by its identifier.
68
+ #
69
+ # @return [Togglefy::Feature] The found feature.
37
70
  def feature
38
- Togglefy::Feature.find_by!(identifier:)
71
+ Togglefy::Feature.find_by!(identifier: identifier)
39
72
  end
40
73
  end
41
- end
74
+ end
@@ -1,64 +1,118 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Togglefy
4
+ # The FeatureQuery class provides methods to query features based on various filters.
2
5
  class FeatureQuery
6
+ # A mapping of filter keys to their corresponding query scopes.
7
+ FILTERS = {
8
+ identifier: :identifier,
9
+ group: :for_group,
10
+ role: :for_group,
11
+ environment: :for_environment,
12
+ env: :for_environment,
13
+ tenant_id: :for_tenant,
14
+ status: :with_status
15
+ }.freeze
16
+
17
+ # Retrieves all features.
18
+ #
19
+ # @return [ActiveRecord::Relation] A relation of all features.
3
20
  def features
4
21
  Togglefy::Feature.all
5
22
  end
6
23
 
24
+ # Finds a feature by its identifier.
25
+ #
26
+ # @param identifier [Symbol, Array<Symbol>, String, Array<String>] The identifier(s) of the feature(s).
27
+ # @return [Togglefy::Feature, ActiveRecord::Relation] The feature or features matching the identifier(s).
7
28
  def feature(identifier)
8
29
  return Togglefy::Feature.identifier(identifier) if identifier.is_a?(Array)
9
30
 
10
- Togglefy::Feature.find_by!(identifier:)
31
+ Togglefy::Feature.find_by!(identifier: identifier)
11
32
  end
12
33
 
34
+ # Retrieves feature assignments for a specific type.
35
+ #
36
+ # @param klass [Class] The class type to filter by.
37
+ # @return [ActiveRecord::Relation] A relation of feature assignments for the given type.
13
38
  def for_type(klass)
14
39
  Togglefy::FeatureAssignment.for_type(klass)
15
40
  end
16
41
 
42
+ # Retrieves features for a specific group.
43
+ #
44
+ # @param group [Symbol, String] The group to filter by.
45
+ # @return [ActiveRecord::Relation] A relation of features for the given group.
17
46
  def for_group(group)
18
47
  Togglefy::Feature.for_group(group)
19
48
  end
20
49
 
50
+ # Retrieves features without a group.
51
+ #
52
+ # @return [ActiveRecord::Relation] A relation of features without a group.
21
53
  def without_group
22
54
  Togglefy::Feature.without_group
23
55
  end
24
56
 
57
+ # Retrieves features for a specific environment.
58
+ #
59
+ # @param environment [Symbol, String] The environment to filter by.
60
+ # @return [ActiveRecord::Relation] A relation of features for the given environment.
25
61
  def for_environment(environment)
26
62
  Togglefy::Feature.for_environment(environment)
27
63
  end
28
64
 
65
+ # Retrieves features without an environment.
66
+ #
67
+ # @return [ActiveRecord::Relation] A relation of features without an environment.
29
68
  def without_environment
30
69
  Togglefy::Feature.without_environment
31
70
  end
32
71
 
72
+ # Retrieves features for a specific tenant.
73
+ #
74
+ # @param tenant_id [String] The tenant_id to filter by.
75
+ # @return [ActiveRecord::Relation] A relation of features for the given tenant.
33
76
  def for_tenant(tenant_id)
34
77
  Togglefy::Feature.for_tenant(tenant_id)
35
78
  end
36
79
 
80
+ # Retrieves features without a tenant.
81
+ #
82
+ # @return [ActiveRecord::Relation] A relation of features without a tenant.
37
83
  def without_tenant
38
84
  Togglefy::Feature.without_tenant
39
85
  end
40
86
 
87
+ # Retrieves features with a specific status.
88
+ #
89
+ # @param status [Symbol, String, Integer] The status to filter by.
90
+ # @return [ActiveRecord::Relation] A relation of features with the given status.
41
91
  def with_status(status)
42
92
  Togglefy::Feature.with_status(status)
43
93
  end
44
94
 
95
+ # Applies filters to retrieve features.
96
+ #
97
+ # @param filters [Hash] The filters to apply.
98
+ # @return [ActiveRecord::Relation] A relation of features matching the filters.
45
99
  def for_filters(filters)
46
- Togglefy::Feature
47
- .then { |q| safe_chain(q, :identifier, filters[:identifier], apply_if: filters.key?(:identifier)) }
48
- .then { |q| safe_chain(q, :for_group, filters[:group] || filters[:role], apply_if: filters.key?(:group) || filters.key?(:role)) }
49
- .then { |q| safe_chain(q, :for_environment, filters[:environment] || filters[:env], apply_if: filters.key?(:environment) || filters.key?(:env)) }
50
- .then { |q| safe_chain(q, :for_tenant, filters[:tenant_id], apply_if: filters.key?(:tenant_id)) }
51
- .then { |q| safe_chain(q, :with_status, filters[:status], apply_if: filters.key?(:status)) }
52
- end
53
-
54
- private
100
+ FILTERS.reduce(Togglefy::Feature) do |query, (key, scope)|
101
+ value = filters[key]
102
+ next query unless filters.key?(key) && nil_or_not_blank?(value)
55
103
 
56
- def safe_chain(query, method, value, apply_if: true)
57
- apply_if && nil_or_not_blank?(value) ? query.public_send(method, value) : query
104
+ query.public_send(scope, value)
105
+ end
58
106
  end
59
107
 
108
+ private
109
+
110
+ # Checks if a value is nil or not blank.
111
+ #
112
+ # @param value [Symbol, String, Integer] The value to check.
113
+ # @return [Boolean] True if the value is nil or not blank, false otherwise.
60
114
  def nil_or_not_blank?(value)
61
115
  value.nil? || !value.blank?
62
116
  end
63
117
  end
64
- end
118
+ end
@@ -1,6 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "togglefy/assignable"
2
4
 
3
5
  module Togglefy
6
+ # `Featureable` is an alias for `Assignable`.
7
+ #
8
+ # @deprecated Use `Togglefy::Assignable` instead.
4
9
  Featureable = Assignable
5
10
  warn "[DEPRECATION] `Togglefy::Featureable` is deprecated. Use `Togglefy::Assignable` instead."
6
- end
11
+ end
@@ -1,13 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "togglefy/services/bulk_toggler"
2
4
 
3
5
  module Togglefy
6
+ # The ScopedBulkWrapper class provides a wrapper for performing bulk operations
7
+ # on a specific class using the BulkToggler service.
4
8
  class ScopedBulkWrapper
9
+ # Initializes a new ScopedBulkWrapper.
10
+ #
11
+ # @param klass [Class] The class to perform bulk operations on.
5
12
  def initialize(klass)
6
13
  @klass = klass
7
14
  end
8
15
 
16
+ # Returns a BulkToggler instance for the specified class.
17
+ #
18
+ # @return [BulkToggler] The BulkToggler instance.
9
19
  def bulk
10
20
  BulkToggler.new(@klass)
11
21
  end
12
22
  end
13
- end
23
+ end