togglefy 1.0.2 → 1.1.0
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.
- checksums.yaml +4 -4
- data/README.md +207 -17
- data/app/models/togglefy/feature.rb +2 -0
- data/lib/togglefy/assignable.rb +66 -0
- data/lib/togglefy/errors/assignables_not_found.rb +7 -0
- data/lib/togglefy/errors/bulk_toggle_failed.rb +11 -0
- data/lib/togglefy/errors/dependency_missing.rb +7 -0
- data/lib/togglefy/errors/error.rb +3 -0
- data/lib/togglefy/errors/feature_not_found.rb +7 -0
- data/lib/togglefy/errors/invalid_feature_attribute.rb +7 -0
- data/lib/togglefy/exceptions.rb +5 -0
- data/lib/togglefy/feature_assignable_manager.rb +30 -0
- data/lib/togglefy/feature_manager.rb +26 -12
- data/lib/togglefy/feature_query.rb +23 -14
- data/lib/togglefy/featureable.rb +4 -34
- data/lib/togglefy/scoped_bulk_wrapper.rb +13 -0
- data/lib/togglefy/services/bulk_toggler.rb +108 -0
- data/lib/togglefy/version.rb +1 -1
- data/lib/togglefy.rb +54 -9
- data/togglefy.gemspec +1 -7
- metadata +15 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2419d9b30bf9193f8262fb0437f128510be4b00a0556a0fb244cf6b316299bb5
|
4
|
+
data.tar.gz: 8e1b0f85f9aedd05eb5880277f6657eee36e1c570a768f3a037b9a1a4137852c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c40f79f61887796ac7e646c0db064fc4a90862e3eea7260597d10eb6cad57543378e7ed6ca4ef0fd8e51efdc8539147035501419de7007bc07d47c63da755d3d
|
7
|
+
data.tar.gz: 9cb7c91cc395acc224d1fa19533e86d9c899b15e61c56f05e608f0dc74d2d9f1392e3f1bf78a9d92790d67440e0b001411a2309f983134ddfe555f6ad6e5d2a1
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Togglefy
|
2
2
|
|
3
|
+

|
4
|
+

|
5
|
+
|
3
6
|
Togglefy is a simple feature management solution to help you control which features an user or a group has access to.
|
4
7
|
|
5
8
|
Togglefy is free, open source and you are welcome to help build it.
|
@@ -9,7 +12,7 @@ Togglefy is free, open source and you are welcome to help build it.
|
|
9
12
|
Add the gem manually to your Gemfile:
|
10
13
|
|
11
14
|
```gemfile
|
12
|
-
gem 'togglefy', '~> 1.0', '>= 1.0
|
15
|
+
gem 'togglefy', '~> 1.0', '>= 1.1.0'
|
13
16
|
```
|
14
17
|
|
15
18
|
Or install it and add to the application's Gemfile by executing:
|
@@ -55,14 +58,18 @@ After that, the next steps are also pretty simple.
|
|
55
58
|
|
56
59
|
Add the following to your model that will have a relation with the features. It can be an `User`, `Account` or something you decide:
|
57
60
|
```ruby
|
58
|
-
include Togglefy::
|
61
|
+
include Togglefy::Assignable
|
59
62
|
```
|
60
63
|
|
61
|
-
This will add the relationship between Togglefy's models and yours. Yours will be referred to as **assignable** throughout this documentation. If you want to check it in the source code, you can find it here: `lib/togglefy/
|
64
|
+
This will add the relationship between Togglefy's models and yours. Yours will be referred to as **assignable** throughout this documentation. If you want to check it in the source code, you can find it here: `lib/togglefy/assignable.rb` inside de `included` block.
|
65
|
+
|
66
|
+
Older versions (`<= 1.0.2`) had the `Featureable` instead of the `Assignable`. The `Featureable` is now deprecated. You can still use it and it won't impact old users, but we highly recommend you to use the `Assignable` as it is semantically more accurate about what it does.
|
62
67
|
|
63
68
|
With that, everything is ready to use **Togglefy**, welcome!
|
64
69
|
|
65
|
-
###
|
70
|
+
### Managing Features
|
71
|
+
|
72
|
+
#### Creating Features
|
66
73
|
To create features it's as simple as drinking a nice cold beer after a hard day or drinking the entire bottle of coffee in a span of 1 hour:
|
67
74
|
|
68
75
|
```ruby
|
@@ -84,6 +91,24 @@ Togglefy::Feature.create(
|
|
84
91
|
)
|
85
92
|
```
|
86
93
|
|
94
|
+
You can also use:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
Togglefy.create(
|
98
|
+
name: "Teleportation",
|
99
|
+
description: "Allows the assignable to teleport to anywhere",
|
100
|
+
group: :jumper
|
101
|
+
)
|
102
|
+
|
103
|
+
# Or
|
104
|
+
|
105
|
+
Togglefy.create_feature(
|
106
|
+
name: "Teleportation",
|
107
|
+
description: "Allows the assignable to teleport to anywhere",
|
108
|
+
group: :jumper
|
109
|
+
)
|
110
|
+
```
|
111
|
+
|
87
112
|
You don't have to fill all fields, the only one that is mandatory is the name, because is by using the name that we will create the unique identifier, which is the field we'll use to find, delete and more.
|
88
113
|
|
89
114
|
The identifier is the name, downcased and snake_cased 🐍
|
@@ -97,17 +122,60 @@ identifier: "super_powers",
|
|
97
122
|
description: "With great power comes great responsibility",
|
98
123
|
created_at: "2025-04-12 01:39:10.176561000 +0000",
|
99
124
|
updated_at: "2025-04-12 01:39:46.818928000 +0000",
|
100
|
-
|
125
|
+
tenant_id: "123abc",
|
101
126
|
group: "admin",
|
102
127
|
environment: "production",
|
103
128
|
status: "inactive"
|
104
129
|
```
|
105
130
|
|
131
|
+
#### Updating a Feature
|
132
|
+
|
133
|
+
To update a feature is as simple as riding on a monocycle while balancing a cup of water on the top of a really tall person that's on your shoulders.
|
134
|
+
|
135
|
+
Here's how you can do it:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
Togglefy.update(:super_powers, tenant_id: "abc123")
|
139
|
+
Togglefy.update_feature(:super_powers, tenant_id: "abc123")
|
140
|
+
```
|
141
|
+
|
142
|
+
Or by finding the feature manually and then updating it like you always do with Rails:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
feature = Togglefy::Feature.find_by(identifier: :super_powers)
|
146
|
+
# or
|
147
|
+
feature = Togglefy.feature(:super_powers) # This is explained more in the "Finding a specific feature" section of this README
|
148
|
+
|
149
|
+
feature.update(tenant_id: "123abc")
|
150
|
+
```
|
151
|
+
|
152
|
+
#### Destroying Features
|
153
|
+
|
154
|
+
It looks like you're mean 😈
|
155
|
+
|
156
|
+
So here's how you can destroy a feature:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
Togglefy.destroy(:super_powers)
|
160
|
+
Togglefy.destroy_feature(:super_powers)
|
161
|
+
Togglefy::Feature.identifier(:super_powers).destroy
|
162
|
+
Togglefy::Feature.find_by(identifier: :super_powers).destroy
|
163
|
+
```
|
164
|
+
|
165
|
+
#### Toggle Features
|
166
|
+
|
167
|
+
You can toggle a feature status to active or inactive by doing this:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
Togglefy.toggle(:super_powers)
|
171
|
+
Togglefy.toggle_feature(:super_powers)
|
172
|
+
```
|
173
|
+
|
106
174
|
#### About `Togglefy::Feature` status
|
107
175
|
|
108
176
|
As you can see, the `Togglefy::Feature` also has a status and it is default to `inactive`. You can change this during creation.
|
109
177
|
|
110
|
-
The status holds the `inactive` or `active` values. This status is not to define if a assignable (any model that has the `include Togglefy::
|
178
|
+
The status holds the `inactive` or `active` values. This status is not to define if a assignable (any model that has the `include Togglefy::Assignable`) either has ou hasn't a feature, but to decide if this feature is available in the entire system.
|
111
179
|
|
112
180
|
It's up to you to define how you will implement it.
|
113
181
|
|
@@ -121,14 +189,21 @@ You can change the status by:
|
|
121
189
|
* Updating the column
|
122
190
|
* Doing a:
|
123
191
|
```ruby
|
124
|
-
|
125
|
-
feature.
|
192
|
+
Togglefy::Feature.find_by(identifier: :super_powers).active!
|
193
|
+
Togglefy.feature(:super_powers).active!
|
194
|
+
Togglefy.active!(:super_powers)
|
195
|
+
Togglefy.activate_feature(:super_powers)
|
196
|
+
|
197
|
+
Togglefy::Feature.find_by(identifier: :super_powers).inactive!
|
198
|
+
Togglefy.feature(:super_powers).inactive!
|
199
|
+
Togglefy.inactive!(:super_powers)
|
200
|
+
Togglefy.inactivate_feature(:super_powers)
|
126
201
|
```
|
127
202
|
|
128
203
|
### Managing Assignables <-> Features
|
129
204
|
Now that we know how to create features, let's check how we can manage them.
|
130
205
|
|
131
|
-
An assignable has some direct methods thanks to the `include Togglefy::
|
206
|
+
An assignable has some direct methods thanks to the `include Togglefy::Assignable`, which are (and let's use an user as an example of an assignable):
|
132
207
|
|
133
208
|
```ruby
|
134
209
|
user.has_feature?(:super_powers) # Checks if user has a single feature
|
@@ -139,7 +214,7 @@ user.clear_features # Clears all features from an user
|
|
139
214
|
|
140
215
|
The assignable <-> feature relation is held by the `Togglefy::FeatureAssignment` table/model.
|
141
216
|
|
142
|
-
But there's another way to manage assignables <-> features by using the `
|
217
|
+
But there's another way to manage assignables <-> features by using the `FeatureAssignableManager`. It's up to you to decide which one.
|
143
218
|
|
144
219
|
Here are the examples:
|
145
220
|
|
@@ -150,8 +225,59 @@ Togglefy.for(assignable).disable(:super_powers) # Disables/removes a feature fro
|
|
150
225
|
Togglefy.for(assignable).clear # Clears all features from an assignable
|
151
226
|
```
|
152
227
|
|
228
|
+
You can also supercharge it by chaining methods, like:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
# Instead of doing this:
|
232
|
+
Togglefy.for(assignable).disable(:alpha_access)
|
233
|
+
Togglefy.for(assignable).enable(:beta_access)
|
234
|
+
|
235
|
+
# You can go something like this:
|
236
|
+
Togglefy.for(assignable).disable(:alpha_access).enable(:beta_access)
|
237
|
+
```
|
238
|
+
|
153
239
|
This second method may look strange, but it's the default used by the gem and you will see that right now!
|
154
240
|
|
241
|
+
#### Mass Enable/Disable of Features to/from Assignables
|
242
|
+
You can mass enable/disable features to/from Assignables.
|
243
|
+
|
244
|
+
Doing that is simple! Let's assume that your assignable is an User model.
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
Togglefy.mass_for(User).bulk.enable(:super_powers) # To enable a specific feature to all users
|
248
|
+
Togglefy.mass_for(User).bulk.enable([:super_powers, :magic, ...]) # To enable two or more features to all users
|
249
|
+
Togglefy.mass_for(User).bulk.enable(:super_powers, percentage: 20) # To enable a feature to 20% of all users
|
250
|
+
Togglefy.mass_for(User).bulk.enable([:super_powers, :magic, ...], percentage: 50) # To enable two or more features to 50% of all users
|
251
|
+
```
|
252
|
+
|
253
|
+
The same applies to the disable:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
Togglefy.mass_for(User).bulk.disable(:super_powers) # To disable a specific feature to all users
|
257
|
+
Togglefy.mass_for(User).bulk.disable([:super_powers, :magic, ...]) # To disable two or more features to all users
|
258
|
+
Togglefy.mass_for(User).bulk.disable(:super_powers, percentage: 5) # To disable a feature to 5% of all users
|
259
|
+
Togglefy.mass_for(User).bulk.disable([:super_powers, :magic, ...], percentage: 75) # To disable two or more features to 75% of all users
|
260
|
+
```
|
261
|
+
|
262
|
+
There are a few things to pay attention:
|
263
|
+
* Whenever you do a enable/disable, it will only query for valid assignables, so:
|
264
|
+
* If you do a enable, it will query all assignables that don't have the feature(s) enabled
|
265
|
+
* If you do a disable, it will query all assignables that do already have the feature(s) enabled
|
266
|
+
* You can also use filters for:
|
267
|
+
* `group || role`
|
268
|
+
* `environment || env`
|
269
|
+
* `tenant_id`
|
270
|
+
* You can check about filters aliases at [Aliases table](#aliases-table)
|
271
|
+
|
272
|
+
These will be applied to query features that match the identifiers + the filters sent.
|
273
|
+
|
274
|
+
So it would be something like:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
Togglefy.mass_for(User).bulk.enable(:super_powers, group: :admin, percentage: 20)
|
278
|
+
Togglefy.mass_for(User).bulk.disable(:magic, group: :dev, env: :production, percentage: 75)
|
279
|
+
```
|
280
|
+
|
155
281
|
### Querying Features
|
156
282
|
Remember when I told you a looooong time ago that the strange way is the default using by the gem? If you don't, no worries. It was a really loooong time ago, like `1.minute.ago`.
|
157
283
|
|
@@ -178,12 +304,28 @@ Togglefy.for_filters(filters: {group: :admin, environment: :production})
|
|
178
304
|
|
179
305
|
This will query me all `Togglefy::Feature`s that belongs to group admin and the production environment.
|
180
306
|
|
307
|
+
The `Togglefy.for_filters` can have the following filters:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
filters: {
|
311
|
+
group:,
|
312
|
+
role:, # Acts as a group, explained in the Aliases section
|
313
|
+
environment:,
|
314
|
+
env:, # Acts as a group, explained in the Aliases section
|
315
|
+
tenant_id:,
|
316
|
+
status:,
|
317
|
+
identifier:
|
318
|
+
}
|
319
|
+
```
|
320
|
+
|
321
|
+
The `Togglefy.for_filters` will only apply the filters sent that are `nil` or `!value.blank?`.
|
322
|
+
|
181
323
|
You can send `nil` values too, like:
|
182
324
|
|
183
325
|
```ruby
|
184
326
|
Togglefy.for_tenant(nil) # This will query me all Togglefy::Features with tenant_id nil
|
185
327
|
|
186
|
-
Togglefy.for_filters(filters: {group: :admin, environment:
|
328
|
+
Togglefy.for_filters(filters: {group: :admin, environment: nil, tenant_id: nil})
|
187
329
|
```
|
188
330
|
|
189
331
|
There's also another way to filter for `nil` values:
|
@@ -210,13 +352,39 @@ Togglefy.with_status(:active)
|
|
210
352
|
#### Finding a specific feature
|
211
353
|
```ruby
|
212
354
|
Togglefy.feature(:super_powers)
|
355
|
+
Togglefy::Feature.identifier(:super_powers)
|
213
356
|
Togglefy::Feature.find_by(identifier: :super_powers)
|
214
357
|
```
|
215
358
|
|
216
|
-
#### Finding
|
359
|
+
#### Finding multiple features
|
217
360
|
```ruby
|
218
|
-
Togglefy.
|
219
|
-
Togglefy::Feature.
|
361
|
+
Togglefy.feature([:super_powers, :magic])
|
362
|
+
Togglefy::Feature.identifier([:super_powers, :magic])
|
363
|
+
Togglefy::Feature.where(identifier: [:super_powers, :magic])
|
364
|
+
```
|
365
|
+
|
366
|
+
#### List all features of an Assignable
|
367
|
+
Let's pretend that your assignable is an User.
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
user = User.find(1)
|
371
|
+
user.features
|
372
|
+
```
|
373
|
+
|
374
|
+
#### Check all features an Assignable have/doesn't have
|
375
|
+
Again, let's pretend that your assignable is an User. This is the only case you need to send the feature id and not the identifier.
|
376
|
+
|
377
|
+
```ruby
|
378
|
+
User.with_features(1)
|
379
|
+
User.with_features([2, 3])
|
380
|
+
User.without_features(1)
|
381
|
+
User.without_features([2, 3])
|
382
|
+
```
|
383
|
+
|
384
|
+
#### Querying all features
|
385
|
+
```ruby
|
386
|
+
Togglefy.features(:super_powers)
|
387
|
+
Togglefy::Feature.all
|
220
388
|
```
|
221
389
|
|
222
390
|
#### Querying all features enabled to a klass
|
@@ -225,7 +393,7 @@ Let's assume that you have two different assignables: User and Account.
|
|
225
393
|
You want to list all features being used by assignables of User type:
|
226
394
|
|
227
395
|
```ruby
|
228
|
-
Togglefy.for_type(User) # This
|
396
|
+
Togglefy.for_type(User) # This returns all current FeatureAssignment with a User assignable
|
229
397
|
```
|
230
398
|
|
231
399
|
#### Aliases
|
@@ -248,17 +416,39 @@ Togglefy.for_filters(filters: {environment: :production})
|
|
248
416
|
Togglefy.for_filters(filter: {env: :production})
|
249
417
|
```
|
250
418
|
|
419
|
+
## Aliases table
|
420
|
+
|
421
|
+
Here's a table of all aliases available on Togglefy.
|
422
|
+
|
423
|
+
You can use either, as long as you respect the rules listed.
|
424
|
+
|
425
|
+
| Original | Alias | Rules |
|
426
|
+
| --------------------- | -------------------- | --------------------------------------------------- |
|
427
|
+
| `for_group` | `for_role` | Can't be used if doing a direct `Togglefy::Feature` |
|
428
|
+
| `without_group` | `without_role` | Can't be used if doing a direct `Togglefy::Feature` |
|
429
|
+
| `for_environment` | `for_env` | Can't be used if doing a direct `Togglefy::Feature` |
|
430
|
+
| `without_environment` | `without_env` | Can't be used if doing a direct `Togglefy::Feature` |
|
431
|
+
| `group` | `role` | Used inside methods that receives filters |
|
432
|
+
| `environment` | `env` | Used inside methods that receives filters |
|
433
|
+
| `create` | `create_feature` | None |
|
434
|
+
| `update` | `update_feature` | None |
|
435
|
+
| `toggle` | `toggle_feature` | None |
|
436
|
+
| `active!` | `activate_feature` | None |
|
437
|
+
| `deactive!` | `inactivate_feature` | None |
|
438
|
+
| `destroy` | `destroy_feature` | None |
|
439
|
+
|
440
|
+
|
251
441
|
## Development
|
252
442
|
|
253
443
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
254
444
|
|
255
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
445
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
256
446
|
|
257
447
|
## Contributing
|
258
448
|
|
259
449
|
Bug reports and pull requests are welcome on GitHub at https://github.com/azeveco/togglefy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/azeveco/togglefy/blob/master/CODE_OF_CONDUCT.md).
|
260
450
|
|
261
|
-
There's a PR Template. Its
|
451
|
+
There's a PR Template. Its use is highly encouraged.
|
262
452
|
|
263
453
|
## License
|
264
454
|
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Togglefy
|
4
|
+
module Assignable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
has_many :feature_assignments, as: :assignable, class_name: "Togglefy::FeatureAssignment"
|
9
|
+
has_many :features, through: :feature_assignments, class_name: "Togglefy::Feature"
|
10
|
+
|
11
|
+
scope :with_features, ->(feature_ids) {
|
12
|
+
joins(:feature_assignments)
|
13
|
+
.where(feature_assignments: {
|
14
|
+
feature_id: feature_ids
|
15
|
+
})
|
16
|
+
.distinct
|
17
|
+
}
|
18
|
+
|
19
|
+
scope :without_features, ->(feature_ids) {
|
20
|
+
joins(left_join_on_features(feature_ids))
|
21
|
+
.where("fa.id IS NULL")
|
22
|
+
.distinct
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_feature?(identifier)
|
27
|
+
features.exists?(identifier: identifier.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_feature(feature)
|
31
|
+
feature = find_feature!(feature)
|
32
|
+
features << feature unless has_feature?(feature.identifier)
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove_feature(feature)
|
36
|
+
feature = find_feature!(feature)
|
37
|
+
features.destroy(feature) if has_feature?(feature.identifier)
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear_features
|
41
|
+
features.destroy_all
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def find_feature!(feature)
|
47
|
+
return feature if feature.is_a?(Togglefy::Feature)
|
48
|
+
|
49
|
+
Togglefy::Feature.find_by!(identifier: feature.to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
class_methods do
|
53
|
+
def left_join_on_features(feature_ids)
|
54
|
+
table = self.table_name
|
55
|
+
type = self.name
|
56
|
+
|
57
|
+
<<~SQL.squish
|
58
|
+
LEFT JOIN togglefy_feature_assignments fa
|
59
|
+
ON fa.assignable_id = #{table}.id
|
60
|
+
AND fa.assignable_type = '#{type}'
|
61
|
+
AND fa.feature_id IN (#{Array(feature_ids).join(",")})
|
62
|
+
SQL
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Togglefy
|
2
|
+
class FeatureAssignableManager
|
3
|
+
def initialize(assignable)
|
4
|
+
@assignable = assignable
|
5
|
+
end
|
6
|
+
|
7
|
+
def enable(feature)
|
8
|
+
assignable.add_feature(feature)
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def disable(feature)
|
13
|
+
assignable.remove_feature(feature)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
assignable.clear_features
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def has?(feature)
|
23
|
+
assignable.has_feature?(feature)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :assignable
|
29
|
+
end
|
30
|
+
end
|
@@ -1,27 +1,41 @@
|
|
1
1
|
module Togglefy
|
2
2
|
class FeatureManager
|
3
|
-
def initialize(
|
4
|
-
@
|
3
|
+
def initialize(identifier = nil)
|
4
|
+
@identifier = identifier unless identifier.nil?
|
5
5
|
end
|
6
6
|
|
7
|
-
def
|
8
|
-
|
7
|
+
def create(**params)
|
8
|
+
Togglefy::Feature.create!(**params)
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
11
|
+
def update(**params)
|
12
|
+
feature.update!(**params)
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
|
15
|
+
def destroy
|
16
|
+
feature.destroy
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
|
19
|
+
def toggle
|
20
|
+
return feature.inactive! if feature.active?
|
21
|
+
|
22
|
+
feature.active!
|
23
|
+
end
|
24
|
+
|
25
|
+
def active!
|
26
|
+
feature.active!
|
27
|
+
end
|
28
|
+
|
29
|
+
def inactive!
|
30
|
+
feature.inactive!
|
21
31
|
end
|
22
32
|
|
23
33
|
private
|
24
34
|
|
25
|
-
attr_reader :
|
35
|
+
attr_reader :identifier
|
36
|
+
|
37
|
+
def feature
|
38
|
+
Togglefy::Feature.find_by!(identifier:)
|
39
|
+
end
|
26
40
|
end
|
27
|
-
end
|
41
|
+
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
module Togglefy
|
2
2
|
class FeatureQuery
|
3
|
-
def
|
4
|
-
Togglefy::Feature.
|
3
|
+
def features
|
4
|
+
Togglefy::Feature.all
|
5
5
|
end
|
6
6
|
|
7
|
-
def
|
8
|
-
|
9
|
-
|
7
|
+
def feature(identifier)
|
8
|
+
return Togglefy::Feature.identifier(identifier) if identifier.is_a?(Array)
|
9
|
+
|
10
|
+
Togglefy::Feature.find_by!(identifier:)
|
10
11
|
end
|
11
12
|
|
12
13
|
def for_type(klass)
|
@@ -16,22 +17,18 @@ module Togglefy
|
|
16
17
|
def for_group(group)
|
17
18
|
Togglefy::Feature.for_group(group)
|
18
19
|
end
|
19
|
-
alias :for_role :for_group
|
20
20
|
|
21
21
|
def without_group
|
22
22
|
Togglefy::Feature.without_group
|
23
23
|
end
|
24
|
-
alias :without_role :without_group
|
25
24
|
|
26
25
|
def for_environment(environment)
|
27
26
|
Togglefy::Feature.for_environment(environment)
|
28
27
|
end
|
29
|
-
alias :for_env :for_environment
|
30
28
|
|
31
29
|
def without_environment
|
32
30
|
Togglefy::Feature.without_environment
|
33
31
|
end
|
34
|
-
alias :without_env :without_environment
|
35
32
|
|
36
33
|
def for_tenant(tenant_id)
|
37
34
|
Togglefy::Feature.for_tenant(tenant_id)
|
@@ -41,15 +38,27 @@ module Togglefy
|
|
41
38
|
Togglefy::Feature.without_tenant
|
42
39
|
end
|
43
40
|
|
41
|
+
def with_status(status)
|
42
|
+
Togglefy::Feature.with_status(status)
|
43
|
+
end
|
44
|
+
|
44
45
|
def for_filters(filters)
|
45
46
|
Togglefy::Feature
|
46
|
-
.
|
47
|
-
.
|
48
|
-
.
|
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)) }
|
49
52
|
end
|
53
|
+
|
54
|
+
private
|
50
55
|
|
51
|
-
def
|
52
|
-
|
56
|
+
def safe_chain(query, method, value, apply_if: true)
|
57
|
+
apply_if && nil_or_not_blank?(value) ? query.public_send(method, value) : query
|
58
|
+
end
|
59
|
+
|
60
|
+
def nil_or_not_blank?(value)
|
61
|
+
value.nil? || !value.blank?
|
53
62
|
end
|
54
63
|
end
|
55
64
|
end
|
data/lib/togglefy/featureable.rb
CHANGED
@@ -1,36 +1,6 @@
|
|
1
|
-
|
2
|
-
module Featureable
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
included do
|
6
|
-
has_many :feature_assignments, as: :assignable, class_name: "Togglefy::FeatureAssignment"
|
7
|
-
has_many :features, through: :feature_assignments, class_name: "Togglefy::Feature"
|
8
|
-
end
|
9
|
-
|
10
|
-
def has_feature?(identifier)
|
11
|
-
features.exists?(identifier: identifier.to_s)
|
12
|
-
end
|
13
|
-
|
14
|
-
def add_feature(feature)
|
15
|
-
feature = find_feature!(feature)
|
16
|
-
features << feature unless has_feature?(feature.identifier)
|
17
|
-
end
|
18
|
-
|
19
|
-
def remove_feature(feature)
|
20
|
-
feature = find_feature!(feature)
|
21
|
-
features.destroy(feature) if has_feature?(feature.identifier)
|
22
|
-
end
|
1
|
+
require "togglefy/assignable"
|
23
2
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def find_feature!(feature)
|
31
|
-
return feature if feature.is_a?(Togglefy::Feature)
|
32
|
-
|
33
|
-
Togglefy::Feature.find_by!(identifier: feature.to_s)
|
34
|
-
end
|
35
|
-
end
|
3
|
+
module Togglefy
|
4
|
+
Featureable = Assignable
|
5
|
+
warn "[DEPRECATION] `Togglefy::Featureable` is deprecated. Use `Togglefy::Assignable` instead."
|
36
6
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Togglefy
|
2
|
+
class BulkToggler
|
3
|
+
ALLOWED_ASSIGNABLE_FILTERS = %i[group role environment env tenant_id].freeze
|
4
|
+
|
5
|
+
def initialize(klass)
|
6
|
+
@klass = klass
|
7
|
+
end
|
8
|
+
|
9
|
+
def enable(identifiers, **filters)
|
10
|
+
toggle(:enable, identifiers, filters)
|
11
|
+
end
|
12
|
+
|
13
|
+
def disable(identifiers, **filters)
|
14
|
+
toggle(:disable, identifiers, filters)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :klass
|
20
|
+
|
21
|
+
def toggle(action, identifiers, filters)
|
22
|
+
identifiers = Array(identifiers)
|
23
|
+
features = Togglefy.for_filters(
|
24
|
+
filters: {identifier: identifiers}.merge(build_scope_filters(filters))
|
25
|
+
).to_a
|
26
|
+
|
27
|
+
raise Togglefy::FeatureNotFound if features.empty?
|
28
|
+
|
29
|
+
feature_ids = features.map(&:id)
|
30
|
+
|
31
|
+
assignables = if action == :enable
|
32
|
+
klass.without_features(feature_ids)
|
33
|
+
else
|
34
|
+
klass.with_features(feature_ids)
|
35
|
+
end
|
36
|
+
|
37
|
+
raise Togglefy::AssignablesNotFound.new(klass, identifiers, filters) if assignables.empty?
|
38
|
+
|
39
|
+
assignables = sample_assignables(assignables, filters[:percentage]) if filters[:percentage]
|
40
|
+
|
41
|
+
existing_assignments = Togglefy::FeatureAssignment.where(
|
42
|
+
assignable_id: assignables.map(&:id),
|
43
|
+
assignable_type: klass.name,
|
44
|
+
feature_id: features.map(&:id)
|
45
|
+
).pluck(:assignable_id, :feature_id)
|
46
|
+
|
47
|
+
existing_lookup = Set.new(existing_assignments)
|
48
|
+
|
49
|
+
if action == :enable
|
50
|
+
rows = []
|
51
|
+
|
52
|
+
assignables.each do |assignable|
|
53
|
+
features.each do |feature|
|
54
|
+
key = [assignable.id, feature.id]
|
55
|
+
next if existing_lookup.include?(key)
|
56
|
+
|
57
|
+
rows << {
|
58
|
+
assignable_id: assignable.id,
|
59
|
+
assignable_type: assignable.class.name,
|
60
|
+
feature_id: feature.id
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
begin
|
66
|
+
Togglefy::FeatureAssignment.insert_all(rows) if rows.any?
|
67
|
+
rescue => error
|
68
|
+
raise Togglefy::BulkToggleFailed.new(
|
69
|
+
"Bulk toggle enable failed for #{klass.name} with identifiers #{identifiers.inspect}",
|
70
|
+
error
|
71
|
+
)
|
72
|
+
end
|
73
|
+
elsif action == :disable
|
74
|
+
ids_to_remove = []
|
75
|
+
assignables.each do |assignable|
|
76
|
+
features.each do |feature|
|
77
|
+
key = [assignable.id, feature.id]
|
78
|
+
ids_to_remove << key if existing_lookup.include?(key)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
begin
|
83
|
+
if ids_to_remove.any?
|
84
|
+
Togglefy::FeatureAssignment.where(
|
85
|
+
assignable_id: ids_to_remove.map(&:first),
|
86
|
+
assignable_type: klass.name,
|
87
|
+
feature_id: ids_to_remove.map(&:last)
|
88
|
+
).delete_all
|
89
|
+
end
|
90
|
+
rescue => error
|
91
|
+
raise Togglefy::BulkToggleFailed.new(
|
92
|
+
"Bulk toggle disable failed for #{klass.name} with identifiers #{identifiers.inspect}",
|
93
|
+
error
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_scope_filters(filters)
|
100
|
+
filters.slice(*ALLOWED_ASSIGNABLE_FILTERS).compact
|
101
|
+
end
|
102
|
+
|
103
|
+
def sample_assignables(assignables, percentage)
|
104
|
+
count = (assignables.size * percentage.to_f / 100).round
|
105
|
+
assignables.sample(count)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/togglefy/version.rb
CHANGED
data/lib/togglefy.rb
CHANGED
@@ -1,24 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "togglefy/version"
|
4
|
-
require "togglefy/engine"
|
4
|
+
require "togglefy/engine" if defined?(Rails)
|
5
5
|
require "togglefy/featureable"
|
6
|
+
require "togglefy/assignable"
|
7
|
+
require "togglefy/feature_assignable_manager"
|
6
8
|
require "togglefy/feature_manager"
|
7
9
|
require "togglefy/feature_query"
|
10
|
+
require "togglefy/scoped_bulk_wrapper"
|
11
|
+
require "togglefy/exceptions"
|
8
12
|
|
9
13
|
module Togglefy
|
10
14
|
class Error < StandardError; end
|
11
15
|
|
12
|
-
|
13
|
-
|
16
|
+
# FeatureQuery
|
17
|
+
def self.features
|
18
|
+
FeatureQuery.new.features
|
14
19
|
end
|
15
20
|
|
16
|
-
def self.
|
17
|
-
FeatureQuery.new.
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.for(assignable)
|
21
|
-
FeatureManager.new(assignable)
|
21
|
+
def self.feature(identifier)
|
22
|
+
FeatureQuery.new.feature(identifier)
|
22
23
|
end
|
23
24
|
|
24
25
|
def self.for_type(klass)
|
@@ -57,11 +58,55 @@ module Togglefy
|
|
57
58
|
FeatureQuery.new.with_status(status)
|
58
59
|
end
|
59
60
|
|
61
|
+
# FeatureManager
|
62
|
+
def self.create(**params)
|
63
|
+
FeatureManager.new.create(**params)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.update(identifier, **params)
|
67
|
+
FeatureManager.new(identifier).update(**params)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.destroy(identifier)
|
71
|
+
FeatureManager.new(identifier).destroy
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.toggle(identifier)
|
75
|
+
FeatureManager.new(identifier).toggle
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.active!(identifier)
|
79
|
+
FeatureManager.new(identifier).active!
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.inactive!(identifier)
|
83
|
+
FeatureManager.new(identifier).inactive!
|
84
|
+
end
|
85
|
+
|
86
|
+
# FeatureAssignableManager
|
87
|
+
def self.for(assignable)
|
88
|
+
FeatureAssignableManager.new(assignable)
|
89
|
+
end
|
90
|
+
|
91
|
+
# ScopedBulkWrapper
|
92
|
+
def self.mass_for(klass)
|
93
|
+
Togglefy::ScopedBulkWrapper.new(klass)
|
94
|
+
end
|
95
|
+
|
60
96
|
class <<self
|
97
|
+
# FeatureQuery
|
61
98
|
alias_method :for_role, :for_group
|
62
99
|
alias_method :without_role, :without_group
|
63
100
|
|
64
101
|
alias_method :for_env, :for_environment
|
65
102
|
alias_method :without_env, :without_environment
|
103
|
+
|
104
|
+
# FeatureManager
|
105
|
+
alias_method :create_feature, :create
|
106
|
+
alias_method :update_feature, :update
|
107
|
+
alias_method :toggle_feature, :toggle
|
108
|
+
alias_method :activate_feature, :active!
|
109
|
+
alias_method :inactivate_feature, :inactive!
|
110
|
+
alias_method :destroy_feature, :destroy
|
66
111
|
end
|
67
112
|
end
|
data/togglefy.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.email = ["gazeveco@gmail.com"]
|
10
10
|
|
11
11
|
spec.summary = "Simple and open source Feature Management."
|
12
|
-
spec.description = "Togglefy is a feature management
|
12
|
+
spec.description = "Togglefy is a feature management Rails gem to help you control which features an user or a group has access to."
|
13
13
|
spec.homepage = "https://github.com/azeveco/Togglefy"
|
14
14
|
spec.license = "MIT"
|
15
15
|
spec.required_ruby_version = ">= 2.4.0"
|
@@ -31,10 +31,4 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.bindir = "exe"
|
32
32
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
33
|
spec.require_paths = ["lib"]
|
34
|
-
|
35
|
-
# Uncomment to register a new dependency of your gem
|
36
|
-
# spec.add_dependency "example-gem", "~> 1.0"
|
37
|
-
|
38
|
-
# For more information and examples about making a new gem, check out our
|
39
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
40
34
|
end
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: togglefy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gabriel Azevedo
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-04-
|
10
|
+
date: 2025-04-16 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
|
-
description: Togglefy is a feature management
|
13
|
-
an user or a group has access to.
|
12
|
+
description: Togglefy is a feature management Rails gem to help you control which
|
13
|
+
features an user or a group has access to.
|
14
14
|
email:
|
15
15
|
- gazeveco@gmail.com
|
16
16
|
executables: []
|
@@ -25,10 +25,21 @@ files:
|
|
25
25
|
- lib/generators/togglefy/templates/create_feature_assignments.rb
|
26
26
|
- lib/generators/togglefy/templates/create_features.rb
|
27
27
|
- lib/togglefy.rb
|
28
|
+
- lib/togglefy/assignable.rb
|
28
29
|
- lib/togglefy/engine.rb
|
30
|
+
- lib/togglefy/errors/assignables_not_found.rb
|
31
|
+
- lib/togglefy/errors/bulk_toggle_failed.rb
|
32
|
+
- lib/togglefy/errors/dependency_missing.rb
|
33
|
+
- lib/togglefy/errors/error.rb
|
34
|
+
- lib/togglefy/errors/feature_not_found.rb
|
35
|
+
- lib/togglefy/errors/invalid_feature_attribute.rb
|
36
|
+
- lib/togglefy/exceptions.rb
|
37
|
+
- lib/togglefy/feature_assignable_manager.rb
|
29
38
|
- lib/togglefy/feature_manager.rb
|
30
39
|
- lib/togglefy/feature_query.rb
|
31
40
|
- lib/togglefy/featureable.rb
|
41
|
+
- lib/togglefy/scoped_bulk_wrapper.rb
|
42
|
+
- lib/togglefy/services/bulk_toggler.rb
|
32
43
|
- lib/togglefy/version.rb
|
33
44
|
- togglefy.gemspec
|
34
45
|
homepage: https://github.com/azeveco/Togglefy
|