superfeature 0.1.2

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: a3b624d4e0260cabbdebb2675fc22a8236889d92197859336a19700528e23b07
4
+ data.tar.gz: '09256cd31eea44abffe554ad183fb98ce061dcc416259d5b47b55651019edac2'
5
+ SHA512:
6
+ metadata.gz: 02070efdf5658cce58e722464d2932f4908dfa0a848ac50adcc0adda1809566a25cbed61e206a759fd782782e4399639ece9f392d4d631d551c3c36dbb91bbb5
7
+ data.tar.gz: 0304f49be57b23352e1e8857b6a2e75b92c1dc71fe23c44151e85409b9bb2bd9a8b5f04f4bf8cf9ac5f7d0066d0129945412e739c5c7903b23d78fae05828f5b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Brad Gessler
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,129 @@
1
+ # Superfeature
2
+
3
+ Features are simple boolean flags that say whether or not they're enabled, right? Not quite. Features can get quite complicated, as you'll read below in the use cases.
4
+
5
+ This gem makes reasoning through those complexities much more sane by isolating them all into the `app/features` folder as plain 'ol Ruby objects (POROS), that way your team can reason through the features available in an app much better, test them, and do really complicated stuff when needed.
6
+
7
+ ## Use cases
8
+
9
+ Here's why you should use Superfeature:
10
+
11
+ ### Turbo app built by a solopreneur deployed to the Apple App Store
12
+
13
+ If you're deploying a simple Rails Turbo application to the web you might have 20 features that are available for purchase, but when deployed to the Apple App Store, you have to disable certain parts of your website to comply with their draconian app store policies. Superfeature could disable the features that upset Apple, like links to your support and pricing, so that your app can get approved and stay in compliance.
14
+
15
+ ### B2B Rails app built by a 50 person engineering team for multinational enterprises
16
+
17
+ Enterprise use-cases are even more complicated. If a package is sold to a multi-national customer with 200 features, they may want to disable 30 of those features for certain teams/groups within that organization for compliance reasons. You end up with a hierarchy that can get as complicated as, "The Zig Bang feature is available to MegaCorp on the Platimum plan, but only for their US entities if their team administrators turn that feature on because of weird compliance reasons".
18
+
19
+ ## Installation
20
+
21
+ Install the gem by executing the following from your Rails root:
22
+
23
+ ```bash
24
+ $ bundle add superfeature
25
+ ```
26
+
27
+ Then run
28
+
29
+ ```bash
30
+ $ rails generate superfeature:install
31
+ ```
32
+
33
+ Restart your server and it's off to the races!
34
+
35
+ First thing you'll want to checkout is the `./app/plans/application_plan.rb` file:
36
+
37
+ ```ruby
38
+ class ApplicationPlan < Superfeature::Plan
39
+ attr_reader :user, :account
40
+
41
+ def initialize(user)
42
+ @user = user
43
+ @account = user.account
44
+ end
45
+
46
+ def team_size
47
+ hard_limit maximum: 0, quantity: account.users.count
48
+ end
49
+
50
+ def moderation
51
+ enabled account.moderation_enabled?
52
+ end
53
+
54
+ def support
55
+ disabled
56
+ end
57
+ end
58
+ ```
59
+
60
+ Here's what it would look like when you add an enterprise plan to the lign up in the ``./app/plans/application_plan.rb`` file.
61
+
62
+ ```ruby
63
+ class EnerprisePlan < ApplicationPlan
64
+ def support = enabled
65
+ def saml = enabled
66
+ end
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ Then you can do things from controllers like:
72
+
73
+ ```ruby
74
+ class ModerationController < ApplicationController
75
+ def show
76
+ if feature.moderation.enabled?
77
+ render "moderation"
78
+ else
79
+ redirect_to moderation_upgrade_path
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ def plan = ApplicationPlan.new
86
+ def feature = plan.moderation
87
+ end
88
+ ```
89
+
90
+ Or from views:
91
+
92
+ ```erb
93
+ <h1>Moderation</h1>
94
+ <% if feature.enabled? %>
95
+ <p><% render partial: "moderation" %></p>
96
+ <% else %>
97
+ <p>Call sales to upgrade to moderation</p>
98
+ <% end %>
99
+ ```
100
+
101
+ ## Comparable libraries
102
+
103
+ There's a few pretty great feature flag libraries that are worth mentioning so you can better evaluate what's right for you.
104
+
105
+ ### Flipper
106
+
107
+ https://github.com/jnunemaker/flipper
108
+
109
+ Flipper is probably the most extensive and mature feature flag libraries. It even comes with its own cloud service. As a library, it concerns itself with:
110
+
111
+ * Persisting feature flags to Redis, ActiveRecord, or any custom back-end.
112
+ * UI for toggling features flags on/off
113
+ * Controlling feature flags for everybody, specific people, groups of people, or a percentage of people.
114
+
115
+ Superfeature is different in that it:
116
+
117
+ * Feature flags are testable.
118
+ * Features are versioned and tracked as code, which makes it easier to sync between environments if that's a requirement.
119
+ * Can handle reasoning about features beyond a simple true/false, including soft limits, app store limitations, or complex feature cascading required by some enterprises.
120
+
121
+ ### Rollout
122
+
123
+ https://github.com/FetLife/rollout
124
+
125
+ Roll-out is similar to Flipper, but is backed soley by Redis.
126
+
127
+ ## License
128
+
129
+ 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
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/superfeature .css
@@ -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,12 @@
1
+ module Superfeature::Authorization
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ helper_method :current_plan
6
+ end
7
+
8
+ protected
9
+ def current_plan
10
+ ApplicationPlan.new
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module Superfeature
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Superfeature
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Superfeature
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Superfeature
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Superfeature
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Superfeature</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "superfeature/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Superfeature::Engine.routes.draw do
2
+ end
@@ -0,0 +1,5 @@
1
+ module Superfeature
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Superfeature
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Superfeature
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,123 @@
1
+ require "superfeature/version"
2
+ require "superfeature/engine"
3
+
4
+ module Superfeature
5
+ def self.plan(&)
6
+ Class.new(Superfeature::Plan, &)
7
+ end
8
+
9
+ class Feature
10
+ attr_reader :plan, :limit, :name
11
+ delegate :enabled?, :disabled?, to: :limit
12
+ delegate :upgrade, :downgrade, to: :plan
13
+
14
+ def initialize(plan:, name:, limit: Limit::Base.new)
15
+ @plan = plan
16
+ @limit = limit
17
+ @name = name
18
+ end
19
+ end
20
+
21
+ module Limit
22
+ class Base
23
+ def enabled?
24
+ false
25
+ end
26
+
27
+ def disabled?
28
+ not enabled?
29
+ end
30
+ end
31
+
32
+ class Hard < Base
33
+ attr_accessor :quantity, :maximum
34
+
35
+ def initialize(quantity: , maximum: )
36
+ @quantity = quantity
37
+ @maximum = maximum
38
+ end
39
+
40
+ def remaining
41
+ maximum - quantity
42
+ end
43
+
44
+ def exceeded?
45
+ quantity > maximum if quantity and maximum
46
+ end
47
+
48
+ def enabled?
49
+ not exceeded?
50
+ end
51
+ end
52
+
53
+ class Soft < Hard
54
+ attr_accessor :quantity, :soft_limit, :hard_limit
55
+
56
+ def initialize(quantity:, soft_limit:, hard_limit:)
57
+ @quantity = quantity
58
+ @soft_limit = soft_limit
59
+ @hard_limit = hard_limit
60
+ end
61
+
62
+ def maximum
63
+ @soft_limit
64
+ end
65
+ end
66
+
67
+ # Unlimited is treated like a Soft, initialized with infinity values.
68
+ # It is recommended to set a `soft_limit` value based on the technical limitations
69
+ # of your application unless you're running a theoritcal Turing Machine.
70
+ #
71
+ # See https://en.wikipedia.org/wiki/Turing_machine for details.
72
+ class Unlimited < Soft
73
+ INFINITY = Float::INFINITY
74
+
75
+ def initialize(quantity: nil, hard_limit: INFINITY, soft_limit: INFINITY, **)
76
+ super(quantity:, hard_limit:, soft_limit:, **)
77
+ end
78
+ end
79
+
80
+ class Boolean < Base
81
+ def initialize(enabled:)
82
+ @enabled = enabled
83
+ end
84
+
85
+ def enabled?
86
+ @enabled
87
+ end
88
+ end
89
+ end
90
+
91
+ class Plan
92
+ def upgrade
93
+ end
94
+
95
+ def downgrade
96
+ end
97
+
98
+ protected
99
+ def hard_limit(**)
100
+ Limit::Hard.new(**)
101
+ end
102
+
103
+ def soft_limit(**)
104
+ Limit::Soft.new(**)
105
+ end
106
+
107
+ def unlimited(**)
108
+ Limit::Unlimited.new(**)
109
+ end
110
+
111
+ def enabled(value = true, **)
112
+ Limit::Boolean.new enabled: value, **
113
+ end
114
+
115
+ def disabled(value = true)
116
+ enabled !value
117
+ end
118
+
119
+ def feature(name, **)
120
+ Feature.new(plan: self, name:, **)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :superfeature do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: superfeature
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Brad Gessler
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-10-11 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: Features can get really complicated when you have to cascade them from
27
+ global, account, policy, group, and policy levels. Superfeature makes that easy!
28
+ email:
29
+ - bradgessler@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/superfeature_manifest.js
38
+ - app/assets/stylesheets/superfeature/application.css
39
+ - app/controllers/concerns/superfeature/authorization.rb
40
+ - app/controllers/superfeature/application_controller.rb
41
+ - app/helpers/superfeature/application_helper.rb
42
+ - app/jobs/superfeature/application_job.rb
43
+ - app/mailers/superfeature/application_mailer.rb
44
+ - app/models/superfeature/application_record.rb
45
+ - app/views/layouts/superfeature/application.html.erb
46
+ - config/routes.rb
47
+ - lib/superfeature.rb
48
+ - lib/superfeature/engine.rb
49
+ - lib/superfeature/version.rb
50
+ - lib/tasks/superfeature_tasks.rake
51
+ homepage: https://github.com/rubymonolith/superfeature
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ allowed_push_host: https://rubygems.org
56
+ homepage_uri: https://github.com/rubymonolith/superfeature
57
+ source_code_uri: https://github.com/rubymonolith/superfeature
58
+ changelog_uri: https://github.com/rubymonolith/superfeature
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.6.2
74
+ specification_version: 4
75
+ summary: Powerful cascading feature flags in Rails.
76
+ test_files: []