ffe 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e4a4f8d7608dddb3df97d2e03b69b815cfd23662ef1f3eb83a934f5336a63b73
4
+ data.tar.gz: fb39e4b322a411d8e93f29b0cdaefc27b12fc3c418efde0a74198e5301f51806
5
+ SHA512:
6
+ metadata.gz: d3cdfbfc53a7015ceefed64e5c6ac7bfe364beea2f33e874ee946b0245ca7f90bc198c45a17fa77ab7be87b8b0e89395b91f6e73f2c2258077669942ea7758cb
7
+ data.tar.gz: 6a7060e5583b18bf30e218663ea19bd7cd6f93fba0541c39d2a0015be3847b57d608befd54313482d3f3e54e0184a6181caa40970334fbd40bcb2712bca30fb0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright LeFnord
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Ffe
2
+
3
+ A minimalistic Feature Flag engine, cause i was bored of unleash and other overloaded ones.
4
+
5
+ ## Installation
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem "ffe"
10
+ ```
11
+
12
+ And then execute:
13
+ ```bash
14
+ $ bundle install
15
+ ```
16
+
17
+ Copy over the initializer with:
18
+ ```bash
19
+ $ bin/rails g ffe:install
20
+ ```
21
+ and adjust it to your needs.
22
+
23
+ Install the migration with:
24
+ ```bash
25
+ $ bin/rails ffe:install:migrations
26
+ ```
27
+ (have an eye on the length of the `milieu` bit mask)
28
+
29
+ Mount the engine in your application's `config/routes.rb` file:
30
+ ```ruby
31
+ mount Ffe::Engine => "/feature_flags"
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ coming soon
37
+
38
+
39
+ ## Contributing
40
+ Contribution directions go here.
41
+
42
+ ## License
43
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ require 'bundler/gem_tasks'
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class FeatureFlagsController < ApplicationController
5
+ before_action :set_ffe, only: %i[show edit update destroy]
6
+
7
+ def index
8
+ @feature_flags = ::Ffe::FeatureFlag.order(created_at: :desc)
9
+ end
10
+
11
+ def show; end
12
+
13
+ def new
14
+ @feature_flag = ::Ffe::FeatureFlag.new
15
+ end
16
+
17
+ def edit; end
18
+
19
+ def create
20
+ @feature_flag = ::Ffe::FeatureFlag.new(feature_flag_params.except(:milieus))
21
+
22
+ if @feature_flag.save
23
+ redirect_to feature_flag_path(@feature_flag), notice: 'FFE wurde erstellt.'
24
+ else
25
+ render :new, status: :unprocessable_content
26
+ end
27
+ end
28
+
29
+ def update
30
+ if @feature_flag.update(feature_flag_params.except(:milieus))
31
+ redirect_to feature_flag_path(@feature_flag), notice: 'FFE wurde aktualisiert.'
32
+ else
33
+ render :edit, status: :unprocessable_content
34
+ end
35
+ end
36
+
37
+ def destroy
38
+ @feature_flag.destroy
39
+ redirect_to feature_flags_path, notice: 'FFE wurde gelöscht.'
40
+ end
41
+
42
+ private
43
+
44
+ def set_ffe
45
+ @feature_flag = ::Ffe::FeatureFlag.find(params.expect(:id))
46
+ end
47
+
48
+ def feature_flag_params
49
+ params.expect(feature_flag: [:name, :description, :enabled, :expires_at, { milieus: {} }]).tap do |params|
50
+ params[:milieu] = params[:milieus].values.join.ljust(Ffe.config.bitlength, '0') if params[:milieus].present?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ module ApplicationHelper
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: 'from@example.com'
6
+ layout 'mailer'
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class FeatureFlag < ApplicationRecord
5
+ self.table_name = 'feature_flags'
6
+
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? }
9
+
10
+ # the Flag functionality itself
11
+ #
12
+ # main methods
13
+ def self.enabled?(flag)
14
+ feature = find_by(name: flag)
15
+
16
+ feature.enabled? && feature.allowed_milieu?
17
+ end
18
+
19
+ def self.disabled?(flag)
20
+ !enabled?(flag)
21
+ end
22
+
23
+ def self.enabled_for?(flag, user: nil)
24
+ return enabled?(flag) if user.blank?
25
+
26
+ feature = find_by(name: flag)
27
+ return false unless feature.allowed_milieu?
28
+
29
+ feature.enabled? && feature.user_ids.include?(user.id.to_s)
30
+ end
31
+
32
+ # instance methods
33
+ #
34
+ def allowed_milieu?
35
+ actual_milieu = ENV.fetch(Ffe.config.env_variable, false)
36
+ return false unless actual_milieu
37
+
38
+ pos = Ffe.config.milieus[actual_milieu.to_sym]
39
+ milieu[pos] == '1'
40
+ end
41
+
42
+ def readable_milieus # rubocop:disable Metrics/AbcSize
43
+ relevant = milieu.to_s.ljust(Ffe.config.milieus.length, '0').chars.first(Ffe.config.milieus.length)
44
+ return 'all' if relevant.all? { |m| m == '1' }
45
+ return 'no' if relevant.all? { |m| m == '0' }
46
+
47
+ inverted = Ffe.config.milieus.invert
48
+ relevant.each_with_index.filter_map { |m, i| inverted[i] if m == '1' }.join(', ')
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # only a dummy model, it will be expected,
4
+ # that the host app already has a users model and table
5
+ module Ffe
6
+ class User < ApplicationRecord
7
+ self.table_name = 'users'
8
+ end
9
+ end
@@ -0,0 +1,47 @@
1
+ <%= form_with model: feature_flag do |form| %>
2
+ <% if feature_flag.errors.any? %>
3
+ <div>
4
+ <h2><%= pluralize(feature_flag.errors.count, "Fehler") %> verhindert das Speichern:</h2>
5
+ <ul>
6
+ <% feature_flag.errors.full_messages.each do |message| %>
7
+ <li><%= message %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
12
+
13
+ <div>
14
+ <%= form.label :name %>
15
+ <%= form.text_field :name %>
16
+ </div>
17
+
18
+ <div>
19
+ <%= form.label :description %>
20
+ <%= form.text_field :description %>
21
+ </div>
22
+
23
+ <div>
24
+ <%= form.label :enabled %>
25
+ <%= form.check_box :enabled %>
26
+ </div>
27
+
28
+ <div>
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') %>
31
+ </div>
32
+
33
+ <div>
34
+ <%= form.fields_for :milieus do |milieu| %>
35
+ <%- Ffe.config.milieus.each do |(env, pos)| %>
36
+ <div>
37
+ <%= milieu.label env %>
38
+ <%= milieu.check_box env, checked: feature_flag.milieu[pos] == '1' %>
39
+ </div>
40
+ <% end %>
41
+ <% end %>
42
+ </div>
43
+
44
+ <div>
45
+ <%= form.submit %>
46
+ </div>
47
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <h1>FFE bearbeiten</h1>
2
+
3
+ <%= render "form", feature_flag: @feature_flag %>
4
+
5
+ <%= link_to "Zurück", feature_flags_path %>
@@ -0,0 +1,31 @@
1
+ <h1>Feature Flags</h1>
2
+
3
+ <p><%= link_to "Neues Feature Flag anlegen", new_feature_flag_path %></p>
4
+
5
+ <table>
6
+ <thead>
7
+ <tr>
8
+ <th>Name</th>
9
+ <th>Description</th>
10
+ <th>Enabled</th>
11
+ <th>Expires At</th>
12
+ <th>Environments</th>
13
+ <th>Actions</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @feature_flags.each do |feature_flag| %>
18
+ <tr>
19
+ <td><%= link_to feature_flag.name, feature_flag_path(feature_flag) %></td>
20
+ <td><%= feature_flag.description %></td>
21
+ <td><%= feature_flag.enabled %></td>
22
+ <td><%= feature_flag.expires_at&.strftime('%Y-%m-%dT%H:%M') %></td>
23
+ <td><%= feature_flag.readable_milieus %></td>
24
+ <td>
25
+ <%= link_to "Bearbeiten", edit_feature_flag_path(feature_flag) %>
26
+ <%= button_to "Löschen", feature_flag_path(feature_flag), method: :delete %>
27
+ </td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
@@ -0,0 +1,5 @@
1
+ <h1>Neues FFE</h1>
2
+
3
+ <%= render "form", feature_flag: @feature_flag %>
4
+
5
+ <%= link_to "Zurück", feature_flags_path %>
@@ -0,0 +1,21 @@
1
+ <h1><%= @feature_flag.name %></h1>
2
+
3
+ <div>
4
+ <strong>Description:</strong>
5
+ <%= @feature_flag.description %>
6
+ </div>
7
+ <div>
8
+ <strong>Enabled:</strong>
9
+ <%= @feature_flag.enabled %>
10
+ </div>
11
+ <div>
12
+ <strong>Expires at:</strong>
13
+ <%= @feature_flag.expires_at&.strftime('%Y-%m-%dT%H:%M') %>
14
+ </div>
15
+ <div>
16
+ <strong>Milieus:</strong>
17
+ <%= @feature_flag.readable_milieus %>
18
+ </div>
19
+
20
+ <p><%= link_to "Bearbeiten", edit_feature_flag_path(@feature_flag) %></p>
21
+ <p><%= link_to "Zurück", feature_flags_path %></p>
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Ffe</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= yield :head %>
9
+
10
+ <%= stylesheet_link_tag "ffe/application", media: "all" %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= yield %>
15
+
16
+ </body>
17
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Ffe::Engine.routes.draw do
2
+ resources :feature_flags
3
+ end
@@ -0,0 +1,17 @@
1
+ class CreateFeatureFlags < ActiveRecord::Migration[8.2]
2
+ def change
3
+ create_table :feature_flags do |t|
4
+ t.string :name, null: false
5
+ t.string :description
6
+ t.boolean :enabled, default: false
7
+ t.datetime :expires_at
8
+ t.column :milieu, "bit(#{Ffe.config.bitlength})", default: ''.ljust(Ffe.config.bitlength, '0') # only for PG
9
+ t.text :user_ids, array: true, default: []
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :feature_flags, :name, unique: true
15
+ add_index :feature_flags, :user_ids, using: :gin
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # remove this migration, if you app already has a users model and table
2
+ class CreateUsers < ActiveRecord::Migration[8.2]
3
+ def change
4
+ create_table :users do |t|
5
+ t.string :name
6
+ t.string :email
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class Configuration
5
+ attr_accessor :bitlength, :env_variable, :milieus
6
+
7
+ def initialize
8
+ @bitlength = 4
9
+ @env_variable = 'RAILS_ENV'
10
+ @milieus = { development: 0, staging: 1, production: 2 }
11
+ end
12
+ end
13
+ end
data/lib/ffe/engine.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Ffe
6
+
7
+ config.ffe = Configuration.new
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ffe
4
+ VERSION = '0.1.0'
5
+ end
data/lib/ffe.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffe/version'
4
+ require 'ffe/configuration'
5
+ require 'ffe/engine'
6
+
7
+ module Ffe
8
+ class << self
9
+ def configure
10
+ yield config
11
+ end
12
+
13
+ def config
14
+ Rails.application.config.ffe
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module Ffe
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('../templates', __dir__)
9
+
10
+ desc 'Creates an initializer for the Ffe engine'
11
+
12
+ def copy_initializer
13
+ template 'ffe_initializer.rb.tt', 'config/initializers/ffe.rb'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ Ffe.configure do |config|
4
+ # Configure the Ffe engine here.
5
+ # (given values are the defaults)
6
+
7
+ # configure the length of the bitmask, used to store the milieus,
8
+ # the minimum length should be length of milieus
9
+ config.bitlength = 4
10
+
11
+ # define the environments you want to use,
12
+ # the count of items maps directly to the length of the milieu bit mask above,
13
+ # so change it accordingly
14
+ #
15
+ # for example: add a fourth environment
16
+ # config.milieus = { development: 0, staging: 1, production: 2, testing: 3}
17
+ # the minimum bitlength becomes:
18
+ # config.bitlength = 4
19
+ #
20
+ # (later changes are bit more complicated, so increase the bit length before)
21
+ #
22
+ config.milieus = { development: 0, staging: 1, production: 2 }
23
+
24
+ # not all rails apps are using RAILS_ENV variable to set it
25
+ config.env_variable = 'RAILS_ENV'
26
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :ffe do
5
+ # # Task goes here
6
+ # end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - LeFnord
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ description: A minimal Feature Flag Engine for Rails.
27
+ email:
28
+ - pscholz.le@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - MIT-LICENSE
34
+ - README.md
35
+ - Rakefile
36
+ - app/assets/stylesheets/ffe/application.css
37
+ - app/controllers/ffe/application_controller.rb
38
+ - app/controllers/ffe/feature_flags_controller.rb
39
+ - app/helpers/ffe/application_helper.rb
40
+ - app/jobs/ffe/application_job.rb
41
+ - app/mailers/ffe/application_mailer.rb
42
+ - app/models/ffe/application_record.rb
43
+ - app/models/ffe/feature_flag.rb
44
+ - app/models/ffe/user.rb
45
+ - app/views/ffe/feature_flags/_form.html.erb
46
+ - app/views/ffe/feature_flags/edit.html.erb
47
+ - app/views/ffe/feature_flags/index.html.erb
48
+ - app/views/ffe/feature_flags/new.html.erb
49
+ - app/views/ffe/feature_flags/show.html.erb
50
+ - app/views/layouts/ffe/application.html.erb
51
+ - config/routes.rb
52
+ - db/migrate/20260810150000_create_feature_flags.rb
53
+ - db/migrate/20260814160209_create_users.rb
54
+ - lib/ffe.rb
55
+ - lib/ffe/configuration.rb
56
+ - lib/ffe/engine.rb
57
+ - lib/ffe/version.rb
58
+ - lib/generators/ffe/install/install_generator.rb
59
+ - lib/generators/ffe/templates/ffe_initializer.rb.tt
60
+ - lib/tasks/ffe_tasks.rake
61
+ homepage: https://github.com/LeFnord/ffe
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ homepage_uri: https://github.com/LeFnord/ffe
66
+ source_code_uri: https://github.com/LeFnord/ffe
67
+ changelog_uri: https://github.com/LeFnord/ffe/blob/master/CHANGELOG.md
68
+ rubygems_mfa_required: 'true'
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '4.0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 4.0.18
84
+ specification_version: 4
85
+ summary: A minimal Feature Flag Engine for Rails
86
+ test_files: []