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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4a4f8d7608dddb3df97d2e03b69b815cfd23662ef1f3eb83a934f5336a63b73
4
- data.tar.gz: fb39e4b322a411d8e93f29b0cdaefc27b12fc3c418efde0a74198e5301f51806
3
+ metadata.gz: cd421a2265cc4254c4f11d652831e595994412dc411adbb8d6082b0cebd354de
4
+ data.tar.gz: a93f928f0810704b1fb5837719753a58d23567220065fb15e41ef705e24b1bc1
5
5
  SHA512:
6
- metadata.gz: d3cdfbfc53a7015ceefed64e5c6ac7bfe364beea2f33e874ee946b0245ca7f90bc198c45a17fa77ab7be87b8b0e89395b91f6e73f2c2258077669942ea7758cb
7
- data.tar.gz: 6a7060e5583b18bf30e218663ea19bd7cd6f93fba0541c39d2a0015be3847b57d608befd54313482d3f3e54e0184a6181caa40970334fbd40bcb2712bca30fb0
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
- coming soon
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(feature_flag: [:name, :description, :enabled, :expires_at, { milieus: {} }]).tap do |params|
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class ExpiredHandlingJob < ApplicationJob
5
+ queue_as :ffe
6
+
7
+ def perform(flag)
8
+ feature_flag = FeatureFlag.find_by(name: flag)
9
+ feature_flag.update!(enabled: false)
10
+ end
11
+ end
12
+ 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
- validates :name, presence: true, uniqueness: true, format: { with: /\A[a-zA-Z_]+\z/ }
8
- validates :expires_at, comparison: { greater_than: Time.current.end_of_day }, if: -> { expires_at.present? }
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 # rubocop:disable Metrics/AbcSize
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, min: Time.current.tomorrow.beginning_of_day, value: feature_flag.expires_at&.strftime('%Y-%m-%dT%H:%M') %>
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ffe
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -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.1.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.18
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: []