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.
- checksums.yaml +4 -4
- data/README.md +79 -54
- data/app/models/togglefy/feature.rb +87 -8
- data/app/models/togglefy/feature_assignment.rb +12 -0
- data/lib/generators/togglefy/install_generator.rb +32 -3
- data/lib/generators/togglefy/templates/older_rails_create_feature_assignments.rb +14 -0
- data/lib/generators/togglefy/templates/older_rails_create_features.rb +18 -0
- data/lib/togglefy/assignable.rb +29 -1
- data/lib/togglefy/engine.rb +2 -0
- data/lib/togglefy/errors/assignables_not_found.rb +5 -0
- data/lib/togglefy/errors/bulk_toggle_failed.rb +9 -0
- data/lib/togglefy/errors/dependency_missing.rb +6 -0
- data/lib/togglefy/errors/error.rb +2 -0
- data/lib/togglefy/errors/feature_not_found.rb +11 -2
- data/lib/togglefy/errors/invalid_feature_attribute.rb +5 -0
- data/lib/togglefy/errors.rb +16 -0
- data/lib/togglefy/feature_assignable_manager.rb +21 -0
- data/lib/togglefy/feature_manager.rb +32 -1
- data/lib/togglefy/feature_query.rb +47 -5
- data/lib/togglefy/featureable.rb +3 -0
- data/lib/togglefy/scoped_bulk_wrapper.rb +8 -0
- data/lib/togglefy/services/bulk_toggler.rb +96 -5
- data/lib/togglefy/version.rb +2 -1
- data/lib/togglefy.rb +154 -9
- data/togglefy.gemspec +6 -2
- metadata +37 -5
- data/lib/togglefy/exceptions.rb +0 -7
- /data/{LICENSE.txt → LICENSE} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ec8a6fb7ce76c460e59858e7037e4f77f4c86dbc21c1584f9f65f03f39485d2
|
4
|
+
data.tar.gz: 3b8c29fd2841dc785b630698ee18978bce872eafd23fbc591ffeb2f9c9bbb210
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98464474a3a3649798424f4c3998db66bf6e07fbeb52505ea94b0bf56ee2d6fbc0b04ecd850e2224d48750930115e55933b3f61d7f5d608b3074411eccb94df3
|
7
|
+
data.tar.gz: f8e53d024ddcfcccde3f098545ce95287d53f32774cf94197e2c2b9361f460bef05ed849af20a0ae14eaa6c8b67577c53014d3c344ba8f4f5a090e6305379fa4
|
data/README.md
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
# Togglefy
|
2
2
|
|
3
|
-

|
4
|
-
](https://rubygems.org/gems/togglefy)
|
4
|
+
[](https://github.com/azeveco/Togglefy/actions/workflows/ci.yml)
|
5
|
+
[](https://rubygems.org/gems/togglefy)
|
6
|
+
[](#license)
|
7
|
+
|
8
|
+
[](http://github.com/azeveco/Togglefy)
|
9
|
+
[](https://rubydoc.info/github/azeveco/Togglefy/main)
|
10
|
+
[](https://deepwiki.com/azeveco/Togglefy)
|
5
11
|
|
6
12
|
Togglefy is a simple feature management solution to help you control which features an user or a group has access to.
|
7
13
|
|
@@ -11,9 +17,8 @@ Togglefy is free, open source and you are welcome to help build it.
|
|
11
17
|
|
12
18
|
Add the gem manually to your Gemfile:
|
13
19
|
|
14
|
-
```
|
15
|
-
gem
|
16
|
-
```
|
20
|
+
```ruby
|
21
|
+
gem "togglefy", "~> 1.2.1"
|
17
22
|
|
18
23
|
Or install it and add to the application's Gemfile by executing:
|
19
24
|
|
@@ -32,48 +37,27 @@ gem install togglefy
|
|
32
37
|
### First steps
|
33
38
|
|
34
39
|
#### Installing inside the project
|
40
|
+
|
35
41
|
After adding the gem to your project, you need to run the generate command to add the necessary files:
|
42
|
+
|
36
43
|
```bash
|
37
44
|
rails generate togglefy:install
|
38
45
|
```
|
39
46
|
|
40
47
|
This command will create the migrations to create the tables inside your project.
|
41
48
|
|
42
|
-
|
43
|
-
|
44
|
-
To fix this, you will have to manually go to the two migration files of Togglefy: `create_feature` and `create_feature_assignments` and do the following:
|
45
|
-
|
46
|
-
Change these lines from this:
|
47
|
-
|
48
|
-
```ruby
|
49
|
-
rails_version = "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"
|
50
|
-
|
51
|
-
class CreateTogglefyFeatures < ActiveRecord::Migration[rails_version]
|
52
|
-
```
|
53
|
-
|
54
|
-
```ruby
|
55
|
-
rails_version = "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"
|
56
|
-
|
57
|
-
class CreateTogglefyFeatureAssignments < ActiveRecord::Migration[rails_version]
|
58
|
-
```
|
59
|
-
|
60
|
-
To this:
|
61
|
-
|
62
|
-
```ruby
|
63
|
-
class CreateTogglefyFeatures < ActiveRecord::Migration
|
64
|
-
```
|
65
|
-
|
66
|
-
```ruby
|
67
|
-
class CreateTogglefyFeatureAssignments < ActiveRecord::Migration
|
68
|
-
```
|
49
|
+
Make sure that the migration `CreateTogglefyFeatureAssignments` have the correct data type for the assignable (table/model) that you're going to use.
|
69
50
|
|
70
51
|
Please, don't remove/change anything else that's there or Togglefy may not work as expected.
|
71
52
|
|
72
53
|
Run the migration to create these in your datase:
|
54
|
+
|
73
55
|
```bash
|
74
56
|
rails db:migrate
|
75
57
|
```
|
58
|
+
|
76
59
|
Or if you're using a legacy codebase:
|
60
|
+
|
77
61
|
```bash
|
78
62
|
rake db:migrate
|
79
63
|
```
|
@@ -87,6 +71,7 @@ After that, the next steps are also pretty simple.
|
|
87
71
|
#### Including inside the Assignable
|
88
72
|
|
89
73
|
Add the following to your model that will have a relation with the features. It can be an `User`, `Account` or something you decide:
|
74
|
+
|
90
75
|
```ruby
|
91
76
|
include Togglefy::Assignable
|
92
77
|
```
|
@@ -100,6 +85,7 @@ With that, everything is ready to use **Togglefy**, welcome!
|
|
100
85
|
### Managing Features
|
101
86
|
|
102
87
|
#### Creating Features
|
88
|
+
|
103
89
|
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:
|
104
90
|
|
105
91
|
```ruby
|
@@ -146,16 +132,18 @@ The identifier is the name, downcased and snake_cased 🐍
|
|
146
132
|
Whenever you create a `Togglefy::Feature`, you can expect something like this:
|
147
133
|
|
148
134
|
```ruby
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
135
|
+
{
|
136
|
+
id: 1,
|
137
|
+
name: "Super Powers",
|
138
|
+
identifier: "super_powers",
|
139
|
+
description: "With great power comes great responsibility",
|
140
|
+
created_at: "2025-04-12 01:39:10.176561000 +0000",
|
141
|
+
updated_at: "2025-04-12 01:39:46.818928000 +0000",
|
142
|
+
tenant_id: "123abc",
|
143
|
+
group: "admin",
|
144
|
+
environment: "production",
|
145
|
+
status: "inactive"
|
146
|
+
}
|
159
147
|
```
|
160
148
|
|
161
149
|
#### Updating a Feature
|
@@ -210,14 +198,16 @@ The status holds the `inactive` or `active` values. This status is not to define
|
|
210
198
|
It's up to you to define how you will implement it.
|
211
199
|
|
212
200
|
* Is it disabled? Then this feature is likely unrelease
|
213
|
-
* Or maybe if it is
|
201
|
+
* Or maybe if it is inactive, it means that the feature is unavailable for some reason? Maintenance?
|
214
202
|
|
215
203
|
Again, it's up to you!
|
216
204
|
|
217
205
|
You can change the status by:
|
206
|
+
|
218
207
|
* Sending a value during creation
|
219
208
|
* Updating the column
|
220
209
|
* Doing a:
|
210
|
+
|
221
211
|
```ruby
|
222
212
|
Togglefy::Feature.find_by(identifier: :super_powers).active!
|
223
213
|
Togglefy.feature(:super_powers).active!
|
@@ -231,6 +221,7 @@ You can change the status by:
|
|
231
221
|
```
|
232
222
|
|
233
223
|
### Managing Assignables <-> Features
|
224
|
+
|
234
225
|
Now that we know how to create features, let's check how we can manage them.
|
235
226
|
|
236
227
|
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):
|
@@ -269,27 +260,29 @@ Togglefy.for(assignable).disable(:alpha_access).enable(:beta_access)
|
|
269
260
|
This second method may look strange, but it's the default used by the gem and you will see that right now!
|
270
261
|
|
271
262
|
#### Mass Enable/Disable of Features to/from Assignables
|
263
|
+
|
272
264
|
You can mass enable/disable features to/from Assignables.
|
273
265
|
|
274
266
|
Doing that is simple! Let's assume that your assignable is an User model.
|
275
267
|
|
276
268
|
```ruby
|
277
269
|
Togglefy.mass_for(User).bulk.enable(:super_powers) # To enable a specific feature to all users
|
278
|
-
Togglefy.mass_for(User).bulk.enable([:super_powers, :magic
|
270
|
+
Togglefy.mass_for(User).bulk.enable([:super_powers, :magic]) # To enable two or more features to all users
|
279
271
|
Togglefy.mass_for(User).bulk.enable(:super_powers, percentage: 20) # To enable a feature to 20% of all users
|
280
|
-
Togglefy.mass_for(User).bulk.enable([:super_powers, :magic
|
272
|
+
Togglefy.mass_for(User).bulk.enable([:super_powers, :magic], percentage: 50) # To enable two or more features to 50% of all users
|
281
273
|
```
|
282
274
|
|
283
275
|
The same applies to the disable:
|
284
276
|
|
285
277
|
```ruby
|
286
278
|
Togglefy.mass_for(User).bulk.disable(:super_powers) # To disable a specific feature to all users
|
287
|
-
Togglefy.mass_for(User).bulk.disable([:super_powers, :magic
|
279
|
+
Togglefy.mass_for(User).bulk.disable([:super_powers, :magic]) # To disable two or more features to all users
|
288
280
|
Togglefy.mass_for(User).bulk.disable(:super_powers, percentage: 5) # To disable a feature to 5% of all users
|
289
|
-
Togglefy.mass_for(User).bulk.disable([:super_powers, :magic
|
281
|
+
Togglefy.mass_for(User).bulk.disable([:super_powers, :magic], percentage: 75) # To disable two or more features to 75% of all users
|
290
282
|
```
|
291
283
|
|
292
284
|
There are a few things to pay attention:
|
285
|
+
|
293
286
|
* Whenever you do a enable/disable, it will only query for valid assignables, so:
|
294
287
|
* If you do a enable, it will query all assignables that don't have the feature(s) enabled
|
295
288
|
* If you do a disable, it will query all assignables that do already have the feature(s) enabled
|
@@ -309,6 +302,7 @@ Togglefy.mass_for(User).bulk.disable(:magic, group: :dev, env: :production, perc
|
|
309
302
|
```
|
310
303
|
|
311
304
|
### Querying Features
|
305
|
+
|
312
306
|
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`.
|
313
307
|
|
314
308
|
It's actually pretty simple. Each line of each code block will show you a way to query to achieve the same result in the context. You **don't** need to use all of options listed in each code block.
|
@@ -337,7 +331,7 @@ This will query me all `Togglefy::Feature`s that belongs to group admin and the
|
|
337
331
|
The `Togglefy.for_filters` can have the following filters:
|
338
332
|
|
339
333
|
```ruby
|
340
|
-
|
334
|
+
{
|
341
335
|
group:,
|
342
336
|
role:, # Acts as a group, explained in the Aliases section
|
343
337
|
environment:,
|
@@ -371,6 +365,7 @@ Togglefy.without_tenant
|
|
371
365
|
```
|
372
366
|
|
373
367
|
#### Querying by Status
|
368
|
+
|
374
369
|
To query Togglefy::Features by status (the same applies to inactive).
|
375
370
|
|
376
371
|
```ruby
|
@@ -380,6 +375,7 @@ Togglefy.with_status(:active)
|
|
380
375
|
```
|
381
376
|
|
382
377
|
#### Finding a specific feature
|
378
|
+
|
383
379
|
```ruby
|
384
380
|
Togglefy.feature(:super_powers)
|
385
381
|
Togglefy::Feature.identifier(:super_powers)
|
@@ -387,6 +383,7 @@ Togglefy::Feature.find_by(identifier: :super_powers)
|
|
387
383
|
```
|
388
384
|
|
389
385
|
#### Finding multiple features
|
386
|
+
|
390
387
|
```ruby
|
391
388
|
Togglefy.feature([:super_powers, :magic])
|
392
389
|
Togglefy::Feature.identifier([:super_powers, :magic])
|
@@ -394,6 +391,7 @@ Togglefy::Feature.where(identifier: [:super_powers, :magic])
|
|
394
391
|
```
|
395
392
|
|
396
393
|
#### List all features of an Assignable
|
394
|
+
|
397
395
|
Let's pretend that your assignable is an User.
|
398
396
|
|
399
397
|
```ruby
|
@@ -402,6 +400,7 @@ user.features
|
|
402
400
|
```
|
403
401
|
|
404
402
|
#### Check all features an Assignable have/doesn't have
|
403
|
+
|
405
404
|
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.
|
406
405
|
|
407
406
|
```ruby
|
@@ -412,12 +411,14 @@ User.without_features([2, 3])
|
|
412
411
|
```
|
413
412
|
|
414
413
|
#### Querying all features
|
414
|
+
|
415
415
|
```ruby
|
416
|
-
Togglefy.features
|
416
|
+
Togglefy.features
|
417
417
|
Togglefy::Feature.all
|
418
418
|
```
|
419
419
|
|
420
420
|
#### Querying all features enabled to a klass
|
421
|
+
|
421
422
|
Let's assume that you have two different assignables: User and Account.
|
422
423
|
|
423
424
|
You want to list all features being used by assignables of User type:
|
@@ -431,6 +432,7 @@ Togglefy.for_type(User) # This returns all current FeatureAssignment with a User
|
|
431
432
|
By the way, did you notice that I wrote `group` and `role` to get group?
|
432
433
|
|
433
434
|
There are aliases for both group and environment that can be used outside of `Togglefy::Feature`. If you want to query `Togglefy::Feature` directly, use only the default name.
|
435
|
+
|
434
436
|
* `group` can be written as `role` outside of `Togglefy::Feature`
|
435
437
|
* `environment` can be written as `env` out side of `Togglefy::Feature`
|
436
438
|
|
@@ -467,44 +469,67 @@ You can use either, as long as you respect the rules listed.
|
|
467
469
|
| `deactive!` | `inactivate_feature` | None |
|
468
470
|
| `destroy` | `destroy_feature` | None |
|
469
471
|
|
470
|
-
|
471
472
|
## Development
|
473
|
+
|
472
474
|
### Setup the environment
|
475
|
+
|
473
476
|
1. Clone the repository
|
474
477
|
2. Run `bin/setup` on the Gem's root directory to install dependencies and run the dummy Rails app migrations used for tests
|
475
478
|
3. Create your branch and checkout to it
|
476
479
|
|
477
480
|
### Running the tests
|
481
|
+
|
478
482
|
1. Make sure that the dummy Rails app db and migrations were ran
|
479
483
|
2. Run `bundle exec rspec` on the Gem's root directory to run all the tests
|
480
484
|
3. If you want to specify a single file to run the tests: `bundle exec rspec path/to/spec/file.rb`
|
481
485
|
4. If you want to specify a single test of a single file to run: `bundle exec spec path/to/spec/file.rb:42` where 42 represents the number of the line
|
482
486
|
|
483
487
|
### Running RuboCop
|
488
|
+
|
484
489
|
1. Make sure you're at the Gem's root directory
|
485
490
|
2. Run `bundle exec rubocop` to run RuboCop on all files not ignored by the `.rubocop.yml` file
|
486
491
|
3. Run `bundle exec rubocop app spec lib/something.rb` to run RuboCop on specific directories/files
|
487
492
|
4. Run `bundle exec rubocop -a` to fix all the auto-correctable offenses listed by RuboCop
|
488
493
|
|
494
|
+
### Generate YARD documentation (optional)
|
495
|
+
|
496
|
+
If you want to generate the YARD documentation, you can do it by running the following command in your terminal:
|
497
|
+
|
498
|
+
```bash
|
499
|
+
yardoc
|
500
|
+
```
|
501
|
+
|
502
|
+
The documentation will be generated inside the `doc` folder.
|
503
|
+
You can also run the YARD server to check the documentation in your browser:
|
504
|
+
|
505
|
+
```bash
|
506
|
+
yard server
|
507
|
+
```
|
508
|
+
|
509
|
+
The folder `docs` is the documentation available for the gem that you can check at https://togglefy.azeveco.com and it does not relate to the `doc` folder generated by the YARD.
|
510
|
+
|
489
511
|
### Other
|
512
|
+
|
490
513
|
1. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
491
514
|
|
492
515
|
### Use local Togglefy Gem with a Rails application
|
516
|
+
|
493
517
|
1. Open the Gemfile of your Rails application
|
494
518
|
2. Add the following: `gem "togglefy", path: "path/to/togglefy/directory"`
|
495
519
|
3. Now you can test everything inside of your Rails application
|
496
520
|
|
497
|
-
#### If you make a change to the Togglefy's code and want to test it, make sure to
|
521
|
+
#### If you make a change to the Togglefy's code and want to test it, make sure to
|
522
|
+
|
498
523
|
1. If you're running a Rails server: restart
|
499
524
|
2. If you're running a Rails console: `reload!` or restart
|
500
525
|
|
501
526
|
## Contributing
|
502
527
|
|
503
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/azeveco/togglefy
|
528
|
+
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).
|
504
529
|
|
505
530
|
* Where can I check for planned features but not started?
|
506
|
-
* You can check Togglefy's project here: https://github.com/users/azeveco/projects/2 to see the priorities, status and more
|
507
|
-
* Or you can check the issues: https://github.com/azeveco/Togglefy/issues
|
531
|
+
* You can check Togglefy's project here: <https://github.com/users/azeveco/projects/2> to see the priorities, status and more
|
532
|
+
* Or you can check the issues: <https://github.com/azeveco/Togglefy/issues>
|
508
533
|
* I have an idea of a feature! What do I do?
|
509
534
|
* First things first, check if there's an issue about it first. If it does, put your comments there. If it doesn't, do the following:
|
510
535
|
* Are you a developer that wants to develop it? Create a new issue, select the New Feature 🚀 template and fill out everything
|
@@ -1,33 +1,112 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Togglefy
|
4
|
-
|
5
|
-
|
4
|
+
# Represents a feature in the Togglefy system.
|
5
|
+
# A feature can have various attributes such as name, identifier, status, and associations with assignables.
|
6
|
+
class Feature < (defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base)
|
7
|
+
# Enum for feature status.
|
8
|
+
# If Rails version is 7 or higher, use the new enum syntax.
|
9
|
+
# If Rails version is lower, use the old enum syntax.
|
10
|
+
# @!attribute [r] status
|
11
|
+
# @return [Symbol] The status of the feature (:inactive or :active).
|
12
|
+
if Rails::VERSION::MAJOR >= 7
|
13
|
+
enum :status, %i[inactive active]
|
14
|
+
else
|
15
|
+
enum status: %i[inactive active]
|
16
|
+
end
|
6
17
|
|
18
|
+
# Associations
|
19
|
+
# @!attribute [rw] feature_assignments
|
20
|
+
# @return [ActiveRecord::Relation] The feature assignments associated with this feature.
|
7
21
|
has_many :feature_assignments, dependent: :destroy
|
8
|
-
has_many :assignables, through: :feature_assignments, source: :assignable
|
9
22
|
|
23
|
+
# Callbacks
|
24
|
+
# Builds an identifier for the feature before validation if the name is present and identifier is blank.
|
10
25
|
before_validation :build_identifier, if: proc { |f| f.name.present? && f.identifier.blank? }
|
11
26
|
|
12
|
-
|
27
|
+
# Scopes
|
28
|
+
# Finds features by their identifier.
|
29
|
+
# @param identifier [Symbol, String, Array<Symbol, String>] The identifier to search for.
|
30
|
+
# @return [ActiveRecord::Relation] The features matching the identifier.
|
31
|
+
scope :identifier, ->(identifier) { where(identifier: identifier) }
|
32
|
+
|
33
|
+
# Finds features by their group.
|
34
|
+
# @param group [String] The group to search for.
|
35
|
+
# @return [ActiveRecord::Relation] The features matching the group.
|
36
|
+
scope :for_group, ->(group) { where(group: group) }
|
13
37
|
|
14
|
-
|
38
|
+
# Finds features without a group.
|
39
|
+
# @return [ActiveRecord::Relation] The features without a group.
|
15
40
|
scope :without_group, -> { where(group: nil) }
|
16
41
|
|
17
|
-
|
42
|
+
# Finds features by their environment.
|
43
|
+
# @param environment [String] The environment to search for.
|
44
|
+
# @return [ActiveRecord::Relation] The features matching the environment.
|
45
|
+
scope :for_environment, ->(environment) { where(environment: environment) }
|
46
|
+
|
47
|
+
# Finds features without an environment.
|
48
|
+
# @return [ActiveRecord::Relation] The features without an environment.
|
18
49
|
scope :without_environment, -> { where(environment: nil) }
|
19
50
|
|
20
|
-
|
51
|
+
# Finds features by their tenant ID.
|
52
|
+
# @param tenant_id [String] The tenant ID to search for.
|
53
|
+
# @return [ActiveRecord::Relation] The features matching the tenant ID.
|
54
|
+
scope :for_tenant, ->(tenant_id) { where(tenant_id: tenant_id) }
|
55
|
+
|
56
|
+
# Finds features without a tenant.
|
57
|
+
# @return [ActiveRecord::Relation] The features without a tenant.
|
21
58
|
scope :without_tenant, -> { where(tenant_id: nil) }
|
22
59
|
|
60
|
+
# Finds features with an inactive status.
|
61
|
+
# @return [ActiveRecord::Relation] The features with an inactive status.
|
23
62
|
scope :inactive, -> { where(status: :inactive) }
|
63
|
+
|
64
|
+
# Finds features with an active status.
|
65
|
+
# @return [ActiveRecord::Relation] The features with an active status.
|
24
66
|
scope :active, -> { where(status: :active) }
|
25
|
-
scope :with_status, ->(status) { where(status:) }
|
26
67
|
|
68
|
+
# Finds features by their status.
|
69
|
+
# @param status [Symbol, String, Integer] The status to search
|
70
|
+
# (:inactive || "inactive" || 0) or (:active || "active" || 1).
|
71
|
+
# @return [ActiveRecord::Relation] The features matching the status.
|
72
|
+
scope :with_status, ->(status) { where(status: status) }
|
73
|
+
|
74
|
+
# Validations
|
75
|
+
# Validates the presence and uniqueness of the name and identifier attributes.
|
27
76
|
validates :name, :identifier, presence: true, uniqueness: true
|
77
|
+
validates :identifier, format: {
|
78
|
+
with: /\A[a-z]+(_[a-z0-9]+)*\z/,
|
79
|
+
message: "must be in snake_case (lowercase letters and underscores only)"
|
80
|
+
}
|
81
|
+
|
82
|
+
# This method retrieves all assignables linked to the feature through feature assignments.
|
83
|
+
# @return [ActiveRecord::Relation] The assignables associated with the feature.
|
84
|
+
# @example
|
85
|
+
# feature.assignables
|
86
|
+
# Togglefy.feature(:super_powers).assignables
|
87
|
+
# Togglefy::Feature.find_by(identifier: :super_powers).assignables
|
88
|
+
# @note This method includes all assignables, regardless of their class.
|
89
|
+
def assignables
|
90
|
+
feature_assignments.includes(:assignable).map(&:assignable)
|
91
|
+
end
|
92
|
+
|
93
|
+
# This method retrieves assignables of a specific class linked to the feature through feature assignments.
|
94
|
+
# @param klass [String, Class] The class name or class of the assignable class.
|
95
|
+
# @return [ActiveRecord::Relation] The assignables of the specified class associated with the feature.
|
96
|
+
# @example
|
97
|
+
# feature.assignables_for_klass("User")
|
98
|
+
# feature.assignables_for_klass(User)
|
99
|
+
# Togglefy.feature(:super_powers).assignables_for_klass(User)
|
100
|
+
# Togglefy::Feature.find_by(identifier: :super_powers).assignables_for_klass(User)
|
101
|
+
def assignables_for_type(klass)
|
102
|
+
feature_assignments.includes(:assignable).where(assignable_type: klass.to_s).map(&:assignable)
|
103
|
+
end
|
28
104
|
|
29
105
|
private
|
30
106
|
|
107
|
+
# Builds a unique identifier for the feature based on its name.
|
108
|
+
# The identifier is generated by parameterizing the name with underscores.
|
109
|
+
# @return [void]
|
31
110
|
def build_identifier
|
32
111
|
self.identifier = name.underscore.parameterize(separator: "_")
|
33
112
|
end
|
@@ -1,10 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Togglefy
|
4
|
+
# Represents the assignment of a feature to an assignable entity.
|
5
|
+
# A feature assignment links a feature to an assignable object, which can be of any polymorphic type.
|
4
6
|
class FeatureAssignment < ApplicationRecord
|
7
|
+
# Associations
|
8
|
+
# @!attribute [rw] feature
|
9
|
+
# @return [Feature] The feature associated with this assignment.
|
5
10
|
belongs_to :feature, class_name: "Togglefy::Feature"
|
11
|
+
|
12
|
+
# @!attribute [rw] assignable
|
13
|
+
# @return [Object] The polymorphic assignable object associated with this assignment.
|
6
14
|
belongs_to :assignable, polymorphic: true
|
7
15
|
|
16
|
+
# Scopes
|
17
|
+
# Finds feature assignments for a specific assignable type.
|
18
|
+
# @param klass [Class] The class type of the assignable.
|
19
|
+
# @return [ActiveRecord::Relation] The feature assignments for the given type.
|
8
20
|
scope :for_type, ->(klass) { where(assignable_type: klass.to_s) }
|
9
21
|
end
|
10
22
|
end
|
@@ -4,20 +4,49 @@ require "rails/generators"
|
|
4
4
|
require "rails/generators/migration"
|
5
5
|
|
6
6
|
module Togglefy
|
7
|
+
# The Generators module contains Rails generators for setting up and managing Togglefy in a Rails application.
|
7
8
|
module Generators
|
9
|
+
# The InstallGenerator class is responsible for setting up the necessary migrations
|
10
|
+
# for the Togglefy gem in a Rails application. It generates migration files for
|
11
|
+
# creating features and feature assignments.
|
12
|
+
#
|
13
|
+
# @example Usage
|
14
|
+
# rails generate togglefy:install
|
8
15
|
class InstallGenerator < Rails::Generators::Base
|
9
16
|
include Rails::Generators::Migration
|
10
17
|
|
18
|
+
# Sets the source directory for templates used by the generator.
|
11
19
|
source_root File.expand_path("templates", __dir__)
|
12
20
|
|
21
|
+
# Generates a migration file numbered with the current timestamp.
|
22
|
+
#
|
23
|
+
# @param _path [String] The path to the migration file.
|
24
|
+
# @return [String] The migration number formatted as YYYYMMDDHHMMSS.
|
25
|
+
# @example
|
26
|
+
# def self.next_migration_number(_path)
|
27
|
+
# Time.now.utc.strftime("%Y%m%d%H%M%S")
|
28
|
+
# end
|
29
|
+
# # => "20231005123456"
|
30
|
+
# @note This method is used to ensure that the migration file has a unique
|
31
|
+
# timestamp, preventing conflicts with other migrations.
|
13
32
|
def self.next_migration_number(_path)
|
14
33
|
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
15
34
|
end
|
16
35
|
|
36
|
+
# Copies the migration templates to the Rails application's db/migrate directory.
|
37
|
+
#
|
38
|
+
# @return [void]
|
39
|
+
# @note The `sleep` call ensures that the second migration file gets a unique timestamp.
|
17
40
|
def copy_migrations
|
18
|
-
|
19
|
-
|
20
|
-
|
41
|
+
if Rails::VERSION::MAJOR >= 5
|
42
|
+
migration_template "create_features.rb", "db/migrate/create_togglefy_features.rb"
|
43
|
+
sleep 1
|
44
|
+
migration_template "create_feature_assignments.rb", "db/migrate/create_togglefy_feature_assignments.rb"
|
45
|
+
else
|
46
|
+
migration_template "older_rails_create_features.rb", "db/migrate/create_togglefy_features.rb"
|
47
|
+
sleep 1
|
48
|
+
migration_template "older_rails_create_feature_assignments.rb", "db/migrate/create_togglefy_feature_assignments.rb" # rubocop:disable Layout/LineLength
|
49
|
+
end
|
21
50
|
end
|
22
51
|
end
|
23
52
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateTogglefyFeatureAssignments < ActiveRecord::Migration
|
4
|
+
def change
|
5
|
+
create_table :togglefy_feature_assignments do |t|
|
6
|
+
t.references :feature, null: false
|
7
|
+
t.references :assignable, polymorphic: true, null: false
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
add_foreign_key :togglefy_feature_assignments, :togglefy_features, column: :feature_id
|
11
|
+
add_index :togglefy_feature_assignments, %i[feature_id assignable_type assignable_id], unique: true,
|
12
|
+
name: "index_togglefy_assignments_uniqueness"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateTogglefyFeatures < ActiveRecord::Migration
|
4
|
+
def change
|
5
|
+
create_table :togglefy_features do |t|
|
6
|
+
t.string :name, null: false
|
7
|
+
t.string :identifier, null: false
|
8
|
+
t.string :description
|
9
|
+
t.string :tenant_id
|
10
|
+
t.string :group
|
11
|
+
t.string :environment
|
12
|
+
t.integer :status, default: 0, null: false
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
add_index :togglefy_features, :name, unique: true
|
16
|
+
add_index :togglefy_features, :identifier, unique: true
|
17
|
+
end
|
18
|
+
end
|
data/lib/togglefy/assignable.rb
CHANGED
@@ -3,13 +3,19 @@
|
|
3
3
|
require "active_support/concern"
|
4
4
|
|
5
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.
|
6
8
|
module Assignable
|
7
9
|
extend ActiveSupport::Concern
|
8
10
|
|
9
11
|
included do
|
12
|
+
# Establishes a many-to-many relationship with features through feature assignments.
|
10
13
|
has_many :feature_assignments, as: :assignable, class_name: "Togglefy::FeatureAssignment"
|
11
14
|
has_many :features, through: :feature_assignments, class_name: "Togglefy::Feature"
|
12
15
|
|
16
|
+
# Scope to retrieve assignables with specific features.
|
17
|
+
#
|
18
|
+
# @param feature_ids [Array<Integer>] The IDs of the features to filter by.
|
13
19
|
scope :with_features, lambda { |feature_ids|
|
14
20
|
joins(:feature_assignments)
|
15
21
|
.where(feature_assignments: {
|
@@ -18,6 +24,9 @@ module Togglefy
|
|
18
24
|
.distinct
|
19
25
|
}
|
20
26
|
|
27
|
+
# Scope to retrieve assignables without specific features.
|
28
|
+
#
|
29
|
+
# @param feature_ids [Array<Integer>] The IDs of the features to filter by.
|
21
30
|
scope :without_features, lambda { |feature_ids|
|
22
31
|
joins(left_join_on_features(feature_ids))
|
23
32
|
.where("fa.id IS NULL")
|
@@ -25,27 +34,42 @@ module Togglefy
|
|
25
34
|
}
|
26
35
|
end
|
27
36
|
|
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.
|
28
41
|
def feature?(identifier)
|
29
|
-
features.exists?(identifier: identifier.to_s)
|
42
|
+
features.active.exists?(identifier: identifier.to_s)
|
30
43
|
end
|
31
44
|
alias has_feature? feature?
|
32
45
|
|
46
|
+
# Adds a feature to the assignable.
|
47
|
+
#
|
48
|
+
# @param feature [Togglefy::Feature, String] The feature or its identifier.
|
33
49
|
def add_feature(feature)
|
34
50
|
feature = find_feature!(feature)
|
35
51
|
features << feature unless has_feature?(feature.identifier)
|
36
52
|
end
|
37
53
|
|
54
|
+
# Removes a feature from the assignable.
|
55
|
+
#
|
56
|
+
# @param feature [Togglefy::Feature, String] The feature or its identifier.
|
38
57
|
def remove_feature(feature)
|
39
58
|
feature = find_feature!(feature)
|
40
59
|
features.destroy(feature) if has_feature?(feature.identifier)
|
41
60
|
end
|
42
61
|
|
62
|
+
# Clears all features from the assignable.
|
43
63
|
def clear_features
|
44
64
|
features.destroy_all
|
45
65
|
end
|
46
66
|
|
47
67
|
private
|
48
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.
|
49
73
|
def find_feature!(feature)
|
50
74
|
return feature if feature.is_a?(Togglefy::Feature)
|
51
75
|
|
@@ -53,6 +77,10 @@ module Togglefy
|
|
53
77
|
end
|
54
78
|
|
55
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.
|
56
84
|
def left_join_on_features(feature_ids)
|
57
85
|
table = table_name
|
58
86
|
type = name
|