ffe 0.1.0 → 0.2.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 +31 -2
- data/app/controllers/ffe/feature_flags_controller.rb +6 -3
- data/app/jobs/ffe/expired_handling_job.rb +12 -0
- data/app/models/concerns/sidekiq_adapter.rb +20 -0
- data/app/models/concerns/solid_queue_adapter.rb +19 -0
- data/app/models/ffe/feature_flag.rb +57 -3
- data/app/views/ffe/feature_flags/_form.html.erb +3 -1
- data/db/migrate/20260810150000_create_feature_flags.rb +1 -0
- data/lib/ffe/configuration.rb +2 -1
- data/lib/ffe/version.rb +1 -1
- data/lib/generators/ffe/templates/ffe_initializer.rb.tt +5 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd421a2265cc4254c4f11d652831e595994412dc411adbb8d6082b0cebd354de
|
|
4
|
+
data.tar.gz: a93f928f0810704b1fb5837719753a58d23567220065fb15e41ef705e24b1bc1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19f5e63d4a2c7616cd418b143c161850fa3329e0dce8fcb3118ed11f7cccd272ffa955d14fa0ce1e1b08d7e3a92ca3c87d46929eedfe9b037ac8e4dbe3c3bf22
|
|
7
|
+
data.tar.gz: 18f38bf47143ca87795e04163afd2d6f02b05c5ebed3d25949b89dc3195777fb7822847cf60855cfd5f8f6a86c3685adf6356f9f437034a4c2afe8236c0a7a47
|
data/README.md
CHANGED
|
@@ -18,11 +18,20 @@ Copy over the initializer with:
|
|
|
18
18
|
```bash
|
|
19
19
|
$ bin/rails g ffe:install
|
|
20
20
|
```
|
|
21
|
-
and adjust it to your needs.
|
|
21
|
+
and adjust it to your needs.
|
|
22
|
+
Defaults are:
|
|
23
|
+
```rb
|
|
24
|
+
config.bitlength = 4
|
|
25
|
+
config.milieus = { development: 0, staging: 1, production: 2 }
|
|
26
|
+
config.env_variable = 'RAILS_ENV'
|
|
27
|
+
# actual supported: :solid_queue, :sidekiq
|
|
28
|
+
config.queue_adapter = :solid_queue
|
|
29
|
+
```
|
|
22
30
|
|
|
23
31
|
Install the migration with:
|
|
24
32
|
```bash
|
|
25
33
|
$ bin/rails ffe:install:migrations
|
|
34
|
+
$ bin/rails db:migrate
|
|
26
35
|
```
|
|
27
36
|
(have an eye on the length of the `milieu` bit mask)
|
|
28
37
|
|
|
@@ -31,12 +40,32 @@ Mount the engine in your application's `config/routes.rb` file:
|
|
|
31
40
|
mount Ffe::Engine => "/feature_flags"
|
|
32
41
|
```
|
|
33
42
|
|
|
43
|
+
### Views
|
|
44
|
+
|
|
45
|
+
The engine provides base views for operating on FeatureFlags.
|
|
46
|
+
|
|
47
|
+
But I recommend implement your own ones, as it is usually easier to customize. See: [views/ffe/feature_flags](https://github.com/LeFnord/ffe/tree/master/app/views/ffe/feature_flags).
|
|
48
|
+
|
|
34
49
|
## Usage
|
|
35
50
|
|
|
36
|
-
|
|
51
|
+
Run your app and go to [http://localhost:3000/ffe/feature_flags](http://localhost:3000/ffe/feature_flags) to create your first feature flag.
|
|
52
|
+
|
|
53
|
+
1. general usage, respecting only the environment
|
|
54
|
+
```rb
|
|
55
|
+
FeatureFlag.enabled?(:flag)
|
|
56
|
+
# or
|
|
57
|
+
FeatureFlag.disabled?(:flag)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
2. check if a user is allowed
|
|
62
|
+
```rb
|
|
63
|
+
FeatureFlag.enabled_for?(:flag, user: current_user)
|
|
64
|
+
```
|
|
37
65
|
|
|
38
66
|
|
|
39
67
|
## Contributing
|
|
68
|
+
|
|
40
69
|
Contribution directions go here.
|
|
41
70
|
|
|
42
71
|
## License
|
|
@@ -17,7 +17,7 @@ module Ffe
|
|
|
17
17
|
def edit; end
|
|
18
18
|
|
|
19
19
|
def create
|
|
20
|
-
@feature_flag = ::Ffe::FeatureFlag.new(feature_flag_params.except(:milieus))
|
|
20
|
+
@feature_flag = ::Ffe::FeatureFlag.new(feature_flag_params.except(:milieus, :clear_expires_at))
|
|
21
21
|
|
|
22
22
|
if @feature_flag.save
|
|
23
23
|
redirect_to feature_flag_path(@feature_flag), notice: 'FFE wurde erstellt.'
|
|
@@ -27,7 +27,7 @@ module Ffe
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def update
|
|
30
|
-
if @feature_flag.update(feature_flag_params.except(:milieus))
|
|
30
|
+
if @feature_flag.update(feature_flag_params.except(:milieus, :clear_expires_at))
|
|
31
31
|
redirect_to feature_flag_path(@feature_flag), notice: 'FFE wurde aktualisiert.'
|
|
32
32
|
else
|
|
33
33
|
render :edit, status: :unprocessable_content
|
|
@@ -46,8 +46,11 @@ module Ffe
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def feature_flag_params
|
|
49
|
-
params.expect(
|
|
49
|
+
params.expect(
|
|
50
|
+
feature_flag: [:name, :description, :enabled, :expires_at, :clear_expires_at, { milieus: {} }]
|
|
51
|
+
).tap do |params|
|
|
50
52
|
params[:milieu] = params[:milieus].values.join.ljust(Ffe.config.bitlength, '0') if params[:milieus].present?
|
|
53
|
+
params[:expires_at] = nil if params[:clear_expires_at] == '1'
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SidekiqAdapter
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def adapter_job
|
|
7
|
+
Sidekiq::ScheduledSet.new.find do |job|
|
|
8
|
+
job.item.dig('args', 0, 'job_id') == job_id
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def adapter_update_job
|
|
13
|
+
destroy_job
|
|
14
|
+
create_job
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def adapter_destroy_job
|
|
18
|
+
job.presence&.delete
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueAdapter
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def adapter_job
|
|
7
|
+
SolidQueue::Job.scheduled.find_by(active_job_id: job_id)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def adapter_update_job
|
|
11
|
+
job.update(scheduled_at: expires_at)
|
|
12
|
+
se = SolidQueue::ScheduledExecution.find_by(job_id: job.id)
|
|
13
|
+
se.update(scheduled_at: expires_at)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def adapter_destroy_job
|
|
17
|
+
job.presence&.discard
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -2,10 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module Ffe
|
|
4
4
|
class FeatureFlag < ApplicationRecord
|
|
5
|
+
if Ffe.config.queue_adapter == :solid_queue
|
|
6
|
+
include SolidQueueAdapter
|
|
7
|
+
elsif Ffe.config.queue_adapter == :sidekiq
|
|
8
|
+
include SidekiqAdapter
|
|
9
|
+
end
|
|
10
|
+
|
|
5
11
|
self.table_name = 'feature_flags'
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
# validations
|
|
14
|
+
#
|
|
15
|
+
validates :name, presence: true, uniqueness: true, format: { with: /\A[a-zA-Z_\d]+\z/ }
|
|
16
|
+
validates :expires_at, comparison: { greater_than: Time.current.end_of_day }, if: -> { expires_at.present? && expires_at_changed? } # rubocop:disable Layout/LineLength
|
|
17
|
+
|
|
18
|
+
# callbacks
|
|
19
|
+
after_save_commit :handle_change_job, unless: -> { Rails.env.test? }
|
|
20
|
+
before_destroy :destroy_job
|
|
9
21
|
|
|
10
22
|
# the Flag functionality itself
|
|
11
23
|
#
|
|
@@ -39,7 +51,7 @@ module Ffe
|
|
|
39
51
|
milieu[pos] == '1'
|
|
40
52
|
end
|
|
41
53
|
|
|
42
|
-
def readable_milieus
|
|
54
|
+
def readable_milieus
|
|
43
55
|
relevant = milieu.to_s.ljust(Ffe.config.milieus.length, '0').chars.first(Ffe.config.milieus.length)
|
|
44
56
|
return 'all' if relevant.all? { |m| m == '1' }
|
|
45
57
|
return 'no' if relevant.all? { |m| m == '0' }
|
|
@@ -47,5 +59,47 @@ module Ffe
|
|
|
47
59
|
inverted = Ffe.config.milieus.invert
|
|
48
60
|
relevant.each_with_index.filter_map { |m, i| inverted[i] if m == '1' }.join(', ')
|
|
49
61
|
end
|
|
62
|
+
|
|
63
|
+
# ExpiresAt related methods
|
|
64
|
+
#
|
|
65
|
+
# handles changes and decide to update or to destroy the job
|
|
66
|
+
def handle_change_job
|
|
67
|
+
if expires_at.present? && job_id.blank?
|
|
68
|
+
create_job
|
|
69
|
+
elsif expires_at.present? && job_id.present?
|
|
70
|
+
update_job
|
|
71
|
+
elsif expires_at.blank? && job_id.present?
|
|
72
|
+
destroy_job
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 1. create job if expires_at set
|
|
77
|
+
def create_job
|
|
78
|
+
return if expires_at.blank?
|
|
79
|
+
|
|
80
|
+
job = Ffe::ExpiredHandlingJob.set(wait_until: expires_at).perform_later(name)
|
|
81
|
+
update_columns(job_id: job.job_id, touch: true) # rubocop:disable Rails/SkipsModelValidations
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# 2. update job if expires_at changed
|
|
85
|
+
def update_job
|
|
86
|
+
return unless job_id.present? && expires_at.present?
|
|
87
|
+
return create_job unless job
|
|
88
|
+
|
|
89
|
+
adapter_update_job if job
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# 3. delete job if expires_at changed to empty, or FF was destroyed
|
|
93
|
+
def destroy_job
|
|
94
|
+
return if job_id.blank?
|
|
95
|
+
|
|
96
|
+
adapter_destroy_job if job
|
|
97
|
+
|
|
98
|
+
update_columns(job_id: nil, touch: true) unless destroyed? # rubocop:disable Rails/SkipsModelValidations
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def job
|
|
102
|
+
adapter_job.presence
|
|
103
|
+
end
|
|
50
104
|
end
|
|
51
105
|
end
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
|
|
28
28
|
<div>
|
|
29
29
|
<%= form.label :expires_at %>
|
|
30
|
-
<%= form.datetime_local_field :expires_at
|
|
30
|
+
<%= form.datetime_local_field :expires_at %>
|
|
31
|
+
<%= check_box_tag 'feature_flag[clear_expires_at]', "1", false %>
|
|
32
|
+
<%= form.label :clear_expires_at %>
|
|
31
33
|
</div>
|
|
32
34
|
|
|
33
35
|
<div>
|
|
@@ -5,6 +5,7 @@ class CreateFeatureFlags < ActiveRecord::Migration[8.2]
|
|
|
5
5
|
t.string :description
|
|
6
6
|
t.boolean :enabled, default: false
|
|
7
7
|
t.datetime :expires_at
|
|
8
|
+
t.string :job_id
|
|
8
9
|
t.column :milieu, "bit(#{Ffe.config.bitlength})", default: ''.ljust(Ffe.config.bitlength, '0') # only for PG
|
|
9
10
|
t.text :user_ids, array: true, default: []
|
|
10
11
|
|
data/lib/ffe/configuration.rb
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Ffe
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :bitlength, :env_variable, :milieus
|
|
5
|
+
attr_accessor :bitlength, :env_variable, :milieus, :queue_adapter
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
8
|
@bitlength = 4
|
|
9
9
|
@env_variable = 'RAILS_ENV'
|
|
10
10
|
@milieus = { development: 0, staging: 1, production: 2 }
|
|
11
|
+
@queue_adapter = :async
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
end
|
data/lib/ffe/version.rb
CHANGED
|
@@ -23,4 +23,9 @@ Ffe.configure do |config|
|
|
|
23
23
|
|
|
24
24
|
# not all rails apps are using RAILS_ENV variable to set it
|
|
25
25
|
config.env_variable = 'RAILS_ENV'
|
|
26
|
+
|
|
27
|
+
# set the active job adapter,
|
|
28
|
+
# needed if you want to use the Ffe::ExpiredHandlingJob
|
|
29
|
+
# actual supported: :solid_queue, :sidekiq
|
|
30
|
+
config.queue_adapter = ActiveJob::Base.queue_adapter_name.to_sym
|
|
26
31
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ffe
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LeFnord
|
|
@@ -38,7 +38,10 @@ files:
|
|
|
38
38
|
- app/controllers/ffe/feature_flags_controller.rb
|
|
39
39
|
- app/helpers/ffe/application_helper.rb
|
|
40
40
|
- app/jobs/ffe/application_job.rb
|
|
41
|
+
- app/jobs/ffe/expired_handling_job.rb
|
|
41
42
|
- app/mailers/ffe/application_mailer.rb
|
|
43
|
+
- app/models/concerns/sidekiq_adapter.rb
|
|
44
|
+
- app/models/concerns/solid_queue_adapter.rb
|
|
42
45
|
- app/models/ffe/application_record.rb
|
|
43
46
|
- app/models/ffe/feature_flag.rb
|
|
44
47
|
- app/models/ffe/user.rb
|
|
@@ -80,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
80
83
|
- !ruby/object:Gem::Version
|
|
81
84
|
version: '0'
|
|
82
85
|
requirements: []
|
|
83
|
-
rubygems_version: 4.0.
|
|
86
|
+
rubygems_version: 4.0.19
|
|
84
87
|
specification_version: 4
|
|
85
88
|
summary: A minimal Feature Flag Engine for Rails
|
|
86
89
|
test_files: []
|