togglefy 1.2.0 → 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,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
6
  class Engine < ::Rails::Engine
5
7
  isolate_namespace Togglefy
6
8
  end
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The AssignablesNotFound class represents an error raised when no assignables
5
+ # match the provided features and filters.
4
6
  class AssignablesNotFound < Togglefy::Error
7
+ # Initializes a new AssignablesNotFound error.
8
+ #
9
+ # @param klass [Class] The class of the assignable.
5
10
  def initialize(klass)
6
11
  super("No #{klass.name} found matching features and filters sent")
7
12
  end
@@ -1,13 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
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.
5
12
  def initialize(message = "Bulk toggle operation failed", cause = nil)
6
13
  super(message)
7
14
  set_backtrace(cause.backtrace) if cause
8
15
  @cause = cause
9
16
  end
10
17
 
18
+ # @!attribute [r] cause
19
+ # @return [Exception, nil] The underlying cause of the error, if any.
11
20
  attr_reader :cause
12
21
  end
13
22
  end
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The DependencyMissing class represents an error raised when a feature
5
+ # is missing a required dependency.
4
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.
5
11
  def initialize(feature, required)
6
12
  super("Feature '#{feature}' is missing dependency: '#{required}'")
7
13
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
6
  class Error < StandardError; end
5
7
  end
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The FeatureNotFound class represents an error raised when no features
5
+ # match the provided criteria or filters.
4
6
  class FeatureNotFound < Togglefy::Error
5
- def initialize
6
- 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
7
12
  end
13
+
14
+ # @!attribute [r] cause
15
+ # @return [Exception, nil] The underlying cause of the error, if any.
16
+ attr_reader :cause
8
17
  end
9
18
  end
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The InvalidFeatureAttribute class represents an error raised when an
5
+ # invalid attribute is provided for a Togglefy::Feature.
4
6
  class InvalidFeatureAttribute < Togglefy::Error
7
+ # Initializes a new InvalidFeatureAttribute error.
8
+ #
9
+ # @param attr [String] The name of the invalid attribute.
5
10
  def initialize(attr)
6
11
  super("The attribute '#{attr}' is not valid for Togglefy::Feature.")
7
12
  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,32 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The FeatureAssignableManager class provides methods to manage features
5
+ # for an assignable object, such as enabling, disabling, and clearing features.
4
6
  class FeatureAssignableManager
7
+ # Initializes a new FeatureAssignableManager.
8
+ #
9
+ # @param assignable [Object] The assignable object to manage features for.
5
10
  def initialize(assignable)
6
11
  @assignable = assignable
7
12
  end
8
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.
9
18
  def enable(feature)
10
19
  assignable.add_feature(feature)
11
20
  self
12
21
  end
13
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.
14
27
  def disable(feature)
15
28
  assignable.remove_feature(feature)
16
29
  self
17
30
  end
18
31
 
32
+ # Clears all features from the assignable.
33
+ #
34
+ # @return [FeatureAssignableManager] Returns self for method chaining.
19
35
  def clear
20
36
  assignable.clear_features
21
37
  self
22
38
  end
23
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.
24
44
  def has?(feature)
25
45
  assignable.has_feature?(feature)
26
46
  end
27
47
 
28
48
  private
29
49
 
50
+ # @return [Object] The assignable object being managed.
30
51
  attr_reader :assignable
31
52
  end
32
53
  end
@@ -1,43 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The FeatureManager class provides methods to manage features, including
5
+ # creating, updating, toggling, and destroying them.
4
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.
5
12
  def initialize(identifier = nil)
6
13
  @identifier = identifier unless identifier.nil?
7
14
  end
8
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.
9
20
  def create(**params)
10
21
  Togglefy::Feature.create!(**params)
11
22
  end
12
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.
13
28
  def update(**params)
14
29
  feature.update!(**params)
15
30
  end
16
31
 
32
+ # Destroys the feature.
33
+ #
34
+ # @return [Togglefy::Feature] The destroyed feature.
17
35
  def destroy
18
36
  feature.destroy
19
37
  end
20
38
 
39
+ # Toggles the feature's status between active and inactive.
40
+ #
41
+ # @return [Togglefy::Feature] The toggled feature.
21
42
  def toggle
22
43
  return feature.inactive! if feature.active?
23
44
 
24
45
  feature.active!
25
46
  end
26
47
 
48
+ # Activates the feature.
49
+ #
50
+ # @return [Togglefy::Feature] The activated feature.
27
51
  def active!
28
52
  feature.active!
29
53
  end
30
54
 
55
+ # Deactivates the feature.
56
+ #
57
+ # @return [Togglefy::Feature] The deactivated feature.
31
58
  def inactive!
32
59
  feature.inactive!
33
60
  end
34
61
 
35
62
  private
36
63
 
64
+ # @return [String, nil] The identifier of the feature being managed.
37
65
  attr_reader :identifier
38
66
 
67
+ # Finds the feature by its identifier.
68
+ #
69
+ # @return [Togglefy::Feature] The found feature.
39
70
  def feature
40
- Togglefy::Feature.find_by!(identifier:)
71
+ Togglefy::Feature.find_by!(identifier: identifier)
41
72
  end
42
73
  end
43
74
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The FeatureQuery class provides methods to query features based on various filters.
4
5
  class FeatureQuery
6
+ # A mapping of filter keys to their corresponding query scopes.
5
7
  FILTERS = {
6
8
  identifier: :identifier,
7
9
  group: :for_group,
@@ -12,48 +14,88 @@ module Togglefy
12
14
  status: :with_status
13
15
  }.freeze
14
16
 
17
+ # Retrieves all features.
18
+ #
19
+ # @return [ActiveRecord::Relation] A relation of all features.
15
20
  def features
16
21
  Togglefy::Feature.all
17
22
  end
18
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).
19
28
  def feature(identifier)
20
29
  return Togglefy::Feature.identifier(identifier) if identifier.is_a?(Array)
21
30
 
22
- Togglefy::Feature.find_by!(identifier:)
31
+ Togglefy::Feature.find_by!(identifier: identifier)
23
32
  end
24
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.
25
38
  def for_type(klass)
26
39
  Togglefy::FeatureAssignment.for_type(klass)
27
40
  end
28
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.
29
46
  def for_group(group)
30
47
  Togglefy::Feature.for_group(group)
31
48
  end
32
49
 
50
+ # Retrieves features without a group.
51
+ #
52
+ # @return [ActiveRecord::Relation] A relation of features without a group.
33
53
  def without_group
34
54
  Togglefy::Feature.without_group
35
55
  end
36
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.
37
61
  def for_environment(environment)
38
62
  Togglefy::Feature.for_environment(environment)
39
63
  end
40
64
 
65
+ # Retrieves features without an environment.
66
+ #
67
+ # @return [ActiveRecord::Relation] A relation of features without an environment.
41
68
  def without_environment
42
69
  Togglefy::Feature.without_environment
43
70
  end
44
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.
45
76
  def for_tenant(tenant_id)
46
77
  Togglefy::Feature.for_tenant(tenant_id)
47
78
  end
48
79
 
80
+ # Retrieves features without a tenant.
81
+ #
82
+ # @return [ActiveRecord::Relation] A relation of features without a tenant.
49
83
  def without_tenant
50
84
  Togglefy::Feature.without_tenant
51
85
  end
52
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.
53
91
  def with_status(status)
54
92
  Togglefy::Feature.with_status(status)
55
93
  end
56
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.
57
99
  def for_filters(filters)
58
100
  FILTERS.reduce(Togglefy::Feature) do |query, (key, scope)|
59
101
  value = filters[key]
@@ -65,10 +107,10 @@ module Togglefy
65
107
 
66
108
  private
67
109
 
68
- def safe_chain(query, method, value, apply_if: true)
69
- apply_if && nil_or_not_blank?(value) ? query.public_send(method, value) : query
70
- end
71
-
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.
72
114
  def nil_or_not_blank?(value)
73
115
  value.nil? || !value.blank?
74
116
  end
@@ -3,6 +3,9 @@
3
3
  require "togglefy/assignable"
4
4
 
5
5
  module Togglefy
6
+ # `Featureable` is an alias for `Assignable`.
7
+ #
8
+ # @deprecated Use `Togglefy::Assignable` instead.
6
9
  Featureable = Assignable
7
10
  warn "[DEPRECATION] `Togglefy::Featureable` is deprecated. Use `Togglefy::Assignable` instead."
8
11
  end
@@ -3,11 +3,19 @@
3
3
  require "togglefy/services/bulk_toggler"
4
4
 
5
5
  module Togglefy
6
+ # The ScopedBulkWrapper class provides a wrapper for performing bulk operations
7
+ # on a specific class using the BulkToggler service.
6
8
  class ScopedBulkWrapper
9
+ # Initializes a new ScopedBulkWrapper.
10
+ #
11
+ # @param klass [Class] The class to perform bulk operations on.
7
12
  def initialize(klass)
8
13
  @klass = klass
9
14
  end
10
15
 
16
+ # Returns a BulkToggler instance for the specified class.
17
+ #
18
+ # @return [BulkToggler] The BulkToggler instance.
11
19
  def bulk
12
20
  BulkToggler.new(@klass)
13
21
  end
@@ -1,25 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
+ # The BulkToggler class provides functionality to enable or disable features
5
+ # in bulk for assignables, such as users or accounts.
4
6
  class BulkToggler
7
+ # List of allowed filters for assignables.
5
8
  ALLOWED_ASSIGNABLE_FILTERS = %i[group role environment env tenant_id].freeze
6
9
 
10
+ # Initializes a new BulkToggler instance.
11
+ #
12
+ # @param klass [Class] The assignable class (e.g., User, Account).
7
13
  def initialize(klass)
8
14
  @klass = klass
9
15
  end
10
16
 
17
+ # Enables features for assignables based on filters.
18
+ # @note All parameters but the first (identifiers) should be passed as keyword arguments.
19
+ #
20
+ # @param identifiers [Array<String>, String] The feature identifiers to enable.
21
+ # @param group [String] The group name to filter assignables by.
22
+ # @param role [String] The role name to filter assignables by.
23
+ # @param environment [String] The environment name to filter assignables by.
24
+ # @param env [String] The environment name to filter assignables by.
25
+ # @param tenant_id [String] The tenant_id to filter assignables by.
26
+ # @param percentage [Integer] The percentage of assignables to include.
11
27
  def enable(identifiers, **filters)
12
28
  toggle(:enable, identifiers, filters)
29
+ true
13
30
  end
14
31
 
32
+ # Disables features for assignables based on filters.
33
+ # @note All parameters but the first (identifiers) should be passed as keyword arguments.
34
+ #
35
+ # @param identifiers [Array<String>, String] The feature identifiers to disable.
36
+ # @param group [String] The group name to filter assignables by.
37
+ # @param role [String] The role name to filter assignables by.
38
+ # @param environment [String] The environment name to filter assignables by.
39
+ # @param env [String] The environment name to filter assignables by.
40
+ # @param tenant_id [String] The tenant_id to filter assignables by.
41
+ # @param percentage [Integer] The percentage of assignables to include.
15
42
  def disable(identifiers, **filters)
16
43
  toggle(:disable, identifiers, filters)
44
+ true
17
45
  end
18
46
 
19
47
  private
20
48
 
21
49
  attr_reader :klass
22
50
 
51
+ # Toggles features for assignables based on the action.
52
+ #
53
+ # @param action [Symbol] The action to perform (:enable or :disable).
54
+ # @param identifiers [Array<String>, String] The feature identifiers.
55
+ # @param filters [Hash] Additional filters for assignables.
23
56
  def toggle(action, identifiers, filters)
24
57
  identifiers = Array(identifiers)
25
58
  features = get_features(identifiers, filters)
@@ -34,6 +67,12 @@ module Togglefy
34
67
  disable_flow(assignables, features, identifiers) if action == :disable
35
68
  end
36
69
 
70
+ # Retrieves features based on identifiers and filters.
71
+ #
72
+ # @param identifiers [Array<String>] The feature identifiers.
73
+ # @param filters [Hash] Additional filters for features.
74
+ # @return [Array<Togglefy::Feature>] The matching features.
75
+ # @raise [Togglefy::FeatureNotFound] If no features are found.
37
76
  def get_features(identifiers, filters)
38
77
  features = Togglefy.for_filters(filters: { identifier: identifiers }.merge(build_scope_filters(filters))).to_a
39
78
 
@@ -42,6 +81,12 @@ module Togglefy
42
81
  features
43
82
  end
44
83
 
84
+ # Retrieves assignables based on the action and feature IDs.
85
+ #
86
+ # @param action [Symbol] The action to perform (:enable or :disable).
87
+ # @param feature_ids [Array<Integer>] The feature IDs.
88
+ # @return [Array<Assignable>] The matching assignables.
89
+ # @raise [Togglefy::AssignablesNotFound] If no assignables are found.
45
90
  def get_assignables(action, feature_ids)
46
91
  assignables = klass.without_features(feature_ids) if action == :enable
47
92
  assignables = klass.with_features(feature_ids) if action == :disable
@@ -51,15 +96,29 @@ module Togglefy
51
96
  assignables
52
97
  end
53
98
 
99
+ # Builds scope filters for assignables.
100
+ #
101
+ # @param filters [Hash] The filters to process.
102
+ # @return [Hash] The processed filters.
54
103
  def build_scope_filters(filters)
55
104
  filters.slice(*ALLOWED_ASSIGNABLE_FILTERS).compact
56
105
  end
57
106
 
107
+ # Samples assignables based on a percentage.
108
+ #
109
+ # @param assignables [Array<Assignable>] The assignables to sample.
110
+ # @param percentage [Float] The percentage of assignables to include.
111
+ # @return [Array<Assignable>] The sampled assignables.
58
112
  def sample_assignables(assignables, percentage)
59
113
  count = (assignables.size * percentage.to_f / 100).round
60
114
  assignables.sample(count)
61
115
  end
62
116
 
117
+ # Enables features for assignables.
118
+ #
119
+ # @param assignables [Array<Assignable>] The assignables to update.
120
+ # @param features [Array<Togglefy::Feature>] The features to enable.
121
+ # @param identifiers [Array<String>] The feature identifiers.
63
122
  def enable_flow(assignables, features, identifiers)
64
123
  rows = []
65
124
 
@@ -72,8 +131,17 @@ module Togglefy
72
131
  mass_insert(rows, identifiers)
73
132
  end
74
133
 
134
+ # Inserts feature assignments in bulk.
135
+ #
136
+ # @param rows [Array<Hash>] The rows to insert.
137
+ # @param identifiers [Array<String>] The feature identifiers.
138
+ # @raise [Togglefy::BulkToggleFailed] If the bulk insert fails.
75
139
  def mass_insert(rows, identifiers)
76
- Togglefy::FeatureAssignment.insert_all(rows) if rows.any?
140
+ return unless rows.any?
141
+
142
+ ActiveRecord::Base.transaction do
143
+ Togglefy::FeatureAssignment.insert_all(rows)
144
+ end
77
145
  rescue Togglefy::Error => e
78
146
  raise Togglefy::BulkToggleFailed.new(
79
147
  "Bulk toggle enable failed for #{klass.name} with identifiers #{identifiers.inspect}",
@@ -81,6 +149,11 @@ module Togglefy
81
149
  )
82
150
  end
83
151
 
152
+ # Disables features for assignables.
153
+ #
154
+ # @param assignables [Array<Assignable>] The assignables to update.
155
+ # @param features [Array<Togglefy::Feature>] The features to disable.
156
+ # @param identifiers [Array<String>] The feature identifiers.
84
157
  def disable_flow(assignables, features, identifiers)
85
158
  ids_to_remove = []
86
159
 
@@ -93,11 +166,16 @@ module Togglefy
93
166
  mass_delete(ids_to_remove, identifiers)
94
167
  end
95
168
 
169
+ # Deletes feature assignments in bulk.
170
+ #
171
+ # @param ids_to_remove [Array<Array>] The IDs to remove.
172
+ # @param identifiers [Array<String>] The feature identifiers.
173
+ # @raise [Togglefy::BulkToggleFailed] If the bulk delete fails.
96
174
  def mass_delete(ids_to_remove, identifiers)
97
- if ids_to_remove.any?
98
- Togglefy::FeatureAssignment.where(
99
- assignable_id: ids_to_remove.map(&:first), assignable_type: klass.name, feature_id: ids_to_remove.map(&:last)
100
- ).delete_all
175
+ return unless ids_to_remove.any?
176
+
177
+ ActiveRecord::Base.transaction do
178
+ Togglefy::FeatureAssignment.where(mass_delete_scope(ids_to_remove, klass.name)).delete_all
101
179
  end
102
180
  rescue Togglefy::Error => e
103
181
  raise Togglefy::BulkToggleFailed.new(
@@ -105,5 +183,18 @@ module Togglefy
105
183
  e
106
184
  )
107
185
  end
186
+
187
+ # Builds the scope for mass deletion.
188
+ #
189
+ # @param ids [Array<Array>] The IDs of features to delete from the assignables.
190
+ # @param klass_name [String] The class name of the assignable.
191
+ # @return [Hash] The scope for mass deletion to be used in the query.
192
+ def mass_delete_scope(ids, klass_name)
193
+ {
194
+ assignable_id: ids.map(&:first),
195
+ assignable_type: klass_name,
196
+ feature_id: ids.map(&:last)
197
+ }
198
+ end
108
199
  end
109
200
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Togglefy
4
- VERSION = "1.2.0"
4
+ # The VERSION constant defines the current version of the Togglefy gem.
5
+ VERSION = "1.2.1"
5
6
  end