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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5938d2ca0587fdf06293a3e2da8ad72bb6b7c642eab6a6f12d80f51d963bc3d
4
- data.tar.gz: eab43dc22d9a8d0be5566fe2ac81c710d95a6e42972a46073e2e8e390262d894
3
+ metadata.gz: 2ec8a6fb7ce76c460e59858e7037e4f77f4c86dbc21c1584f9f65f03f39485d2
4
+ data.tar.gz: 3b8c29fd2841dc785b630698ee18978bce872eafd23fbc591ffeb2f9c9bbb210
5
5
  SHA512:
6
- metadata.gz: 62cfb19a3d90fa016d451f5fa784a5ce549f8500e43bbd75eb59fa184c3dfb999f65c82eedc41b543830772a7a116287f8a8521b04c775d9f1cea93e66938b99
7
- data.tar.gz: cdafbc45941cb93063cad62a4491fd87dc4714da4f71949b686bec1a850246a0d50a0680a4dbd5644c59f001924e01d917bc499a06063c753327121b00a94e26
6
+ metadata.gz: 98464474a3a3649798424f4c3998db66bf6e07fbeb52505ea94b0bf56ee2d6fbc0b04ecd850e2224d48750930115e55933b3f61d7f5d608b3074411eccb94df3
7
+ data.tar.gz: f8e53d024ddcfcccde3f098545ce95287d53f32774cf94197e2c2b9361f460bef05ed849af20a0ae14eaa6c8b67577c53014d3c344ba8f4f5a090e6305379fa4
data/README.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # Togglefy
2
2
 
3
- ![Gem](https://img.shields.io/gem/v/togglefy)
4
- ![Downloads](https://img.shields.io/gem/dt/togglefy)
3
+ [![Gem](https://img.shields.io/gem/v/togglefy)](https://rubygems.org/gems/togglefy)
4
+ [![CI](https://github.com/azeveco/Togglefy/actions/workflows/ci.yml/badge.svg)](https://github.com/azeveco/Togglefy/actions/workflows/ci.yml)
5
+ [![Downloads](https://img.shields.io/gem/dt/togglefy)](https://rubygems.org/gems/togglefy)
6
+ [![License](https://img.shields.io/badge/license-MIT-yellowgreen.svg)](#license)
7
+
8
+ [![GitHub](https://img.shields.io/badge/github-azeveco/Togglefy-blue.svg)](http://github.com/azeveco/Togglefy)
9
+ [![Documentation](https://img.shields.io/badge/docs-rdoc.info-yellow.svg)](https://rubydoc.info/github/azeveco/Togglefy/main)
10
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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
- ```gemfile
15
- gem 'togglefy', '~> 1.0', '>= 1.1.1'
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
- If you use an older version of Rails (< 5), then the migration files don't need you to specify the version.
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
- id: 1,
150
- name: "Super Powers",
151
- identifier: "super_powers",
152
- description: "With great power comes great responsibility",
153
- created_at: "2025-04-12 01:39:10.176561000 +0000",
154
- updated_at: "2025-04-12 01:39:46.818928000 +0000",
155
- tenant_id: "123abc",
156
- group: "admin",
157
- environment: "production",
158
- status: "inactive"
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 disabled, you can see the flag to active to an assignable but can't change the values?
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, ...]) # To enable two or more features to all users
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, ...], percentage: 50) # To enable two or more features to 50% of all users
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, ...]) # To disable two or more features to all users
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, ...], percentage: 75) # To disable two or more features to 75% of all users
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
- filters: {
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(:super_powers)
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. 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).
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
- class Feature < ApplicationRecord
5
- enum :status, %i[inactive active]
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
- scope :identifier, ->(identifier) { where(identifier:) }
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
- scope :for_group, ->(group) { where(group:) }
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
- scope :for_environment, ->(environment) { where(environment:) }
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
- scope :for_tenant, ->(tenant_id) { where(tenant_id:) }
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
- migration_template "create_features.rb", "db/migrate/create_togglefy_features.rb"
19
- sleep 1.5
20
- migration_template "create_feature_assignments.rb", "db/migrate/create_togglefy_feature_assignments.rb"
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
@@ -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