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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +129 -0
- data/Rakefile +8 -0
- data/app/assets/config/superfeature_manifest.js +1 -0
- data/app/assets/stylesheets/superfeature/application.css +15 -0
- data/app/controllers/concerns/superfeature/authorization.rb +12 -0
- data/app/controllers/superfeature/application_controller.rb +4 -0
- data/app/helpers/superfeature/application_helper.rb +4 -0
- data/app/jobs/superfeature/application_job.rb +4 -0
- data/app/mailers/superfeature/application_mailer.rb +6 -0
- data/app/models/superfeature/application_record.rb +5 -0
- data/app/views/layouts/superfeature/application.html.erb +15 -0
- data/config/routes.rb +2 -0
- data/lib/superfeature/engine.rb +5 -0
- data/lib/superfeature/version.rb +3 -0
- data/lib/superfeature.rb +123 -0
- data/lib/tasks/superfeature_tasks.rake +4 -0
- metadata +76 -0
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 @@
|
|
|
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
|
+
*/
|
data/config/routes.rb
ADDED
data/lib/superfeature.rb
ADDED
|
@@ -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
|
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: []
|