toggles 0.1.2 → 0.4.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 +5 -5
- data/.github/workflows/ci.yml +24 -0
- data/CHANGELOG.md +83 -0
- data/Gemfile +5 -0
- data/README.md +7 -10
- data/features/abbreviations_cn22.yml +3 -0
- data/features/file_s3.yml +3 -0
- data/features/nested_foo/bar_baz.yml +3 -0
- data/features/s3_file.yml +3 -0
- data/lib/toggles/constant_lookup.rb +50 -0
- data/lib/toggles/feature/attribute.rb +15 -0
- data/lib/toggles/feature/operation/and.rb +0 -15
- data/lib/toggles/feature/operation/in.rb +1 -1
- data/lib/toggles/feature/operation/range.rb +0 -2
- data/lib/toggles/feature/permissions.rb +34 -29
- data/lib/toggles/feature.rb +76 -16
- data/lib/toggles/version.rb +5 -0
- data/lib/toggles.rb +17 -55
- data/spec/spec_helper.rb +2 -0
- data/spec/toggles/feature/acceptance/type_spec.rb +19 -3
- data/spec/toggles/feature/attribute_spec.rb +9 -0
- data/spec/toggles/feature/operation_spec.rb +69 -0
- data/spec/toggles/feature/permissions_spec.rb +1 -1
- data/spec/toggles/feature_spec.rb +37 -0
- data/spec/toggles/init_spec.rb +9 -4
- data/toggles.gemspec +19 -16
- metadata +36 -87
- data/lib/toggles/feature/base.rb +0 -27
- data/lib/toggles/feature/operation/attribute.rb +0 -19
- data/lib/toggles/feature/operation/lt.rb +0 -9
- data/lib/toggles/feature/operation/not.rb +0 -15
- data/lib/toggles/feature/operation/or.rb +0 -15
- data/lib/toggles/feature/operation.rb +0 -8
- data/spec/toggles/feature/base_spec.rb +0 -10
- data/spec/toggles/feature/operation/and_spec.rb +0 -9
- data/spec/toggles/feature/operation/attribute_spec.rb +0 -9
- data/spec/toggles/feature/operation/gt_spec.rb +0 -6
- data/spec/toggles/feature/operation/in_spec.rb +0 -7
- data/spec/toggles/feature/operation/lt_spec.rb +0 -6
- data/spec/toggles/feature/operation/not_spec.rb +0 -6
- data/spec/toggles/feature/operation/or_spec.rb +0 -6
- data/spec/toggles/feature/operation/range_spec.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ece49487f49d27e3a10944d318e3b2c1b9a0e9a8a3c231581ebf5341e7abc4f2
|
4
|
+
data.tar.gz: d278a07f9e9e5e379936e02fa27e4a4cd7270ef2e7933181e5948c30d88bb4fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e80d24bb41c699217fa865e8ac2c346c4fbdc411d93eec98dc0d4ff11953f1c757a778606d62de664602a4320788b63389beca5f831431d7bc90bbee24eea0d
|
7
|
+
data.tar.gz: 4c4f3ea7f4d06f1a465f53e3114e5a456eed9c7de889e940910a01ed300823852e2b2cb823c89152685ed489800cf406d51efc876179feda4ad204077305ba72
|
@@ -0,0 +1,24 @@
|
|
1
|
+
name: 'CI'
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request: ~
|
7
|
+
schedule:
|
8
|
+
- cron: "0 16 * * 1"
|
9
|
+
|
10
|
+
jobs:
|
11
|
+
run-tests:
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
strategy:
|
14
|
+
matrix:
|
15
|
+
rubyversion: ['2.7', '3.0', '3.1']
|
16
|
+
steps:
|
17
|
+
- uses: actions/checkout@v2
|
18
|
+
- name: set up ruby
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{ matrix.rubyversion }}
|
22
|
+
bundler-cache: true
|
23
|
+
- name: run tests
|
24
|
+
run: bundle exec rspec
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [v0.4.0](https://github.com/EasyPost/toggles/tree/v0.4.0)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.3.0...v0.4.0)
|
6
|
+
|
7
|
+
**Merged pull requests:**
|
8
|
+
|
9
|
+
- refactor: remove Feature::Base, just use Feature::Permissions [\#13](https://github.com/EasyPost/toggles/pull/13) ([lanej](https://github.com/lanej))
|
10
|
+
- refactor: collapse some hierarchies, model operations as functions [\#12](https://github.com/EasyPost/toggles/pull/12) ([lanej](https://github.com/lanej))
|
11
|
+
- fix: handle abbreviations and numbers in feature name [\#11](https://github.com/EasyPost/toggles/pull/11) ([lanej](https://github.com/lanej))
|
12
|
+
- Deprecate constant lookups via .const\_missing [\#10](https://github.com/EasyPost/toggles/pull/10) ([lanej](https://github.com/lanej))
|
13
|
+
|
14
|
+
## [v0.3.0](https://github.com/EasyPost/toggles/tree/v0.3.0) (2021-08-16)
|
15
|
+
|
16
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.1.2...v0.3.0)
|
17
|
+
|
18
|
+
**Merged pull requests:**
|
19
|
+
|
20
|
+
- add github actions integration [\#9](https://github.com/EasyPost/toggles/pull/9) ([Roguelazer](https://github.com/Roguelazer))
|
21
|
+
- point build status link at travis-ci.com instead of travis-ci.org [\#8](https://github.com/EasyPost/toggles/pull/8) ([Roguelazer](https://github.com/Roguelazer))
|
22
|
+
- fix tests [\#7](https://github.com/EasyPost/toggles/pull/7) ([Roguelazer](https://github.com/Roguelazer))
|
23
|
+
- run tests on travis [\#6](https://github.com/EasyPost/toggles/pull/6) ([Roguelazer](https://github.com/Roguelazer))
|
24
|
+
|
25
|
+
## [v0.1.2](https://github.com/EasyPost/toggles/tree/v0.1.2) (2016-11-08)
|
26
|
+
|
27
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.1.1...v0.1.2)
|
28
|
+
|
29
|
+
**Merged pull requests:**
|
30
|
+
|
31
|
+
- store the actual feature configuration in a variable instead of in real constants under the Feature module [\#4](https://github.com/EasyPost/toggles/pull/4) ([Roguelazer](https://github.com/Roguelazer))
|
32
|
+
|
33
|
+
## [v0.1.1](https://github.com/EasyPost/toggles/tree/v0.1.1) (2015-08-24)
|
34
|
+
|
35
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.1.0...v0.1.1)
|
36
|
+
|
37
|
+
## [v0.1.0](https://github.com/EasyPost/toggles/tree/v0.1.0) (2015-08-24)
|
38
|
+
|
39
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.0.8...v0.1.0)
|
40
|
+
|
41
|
+
**Closed issues:**
|
42
|
+
|
43
|
+
- Handle when properties are nil [\#2](https://github.com/EasyPost/toggles/issues/2)
|
44
|
+
|
45
|
+
## [v0.0.8](https://github.com/EasyPost/toggles/tree/v0.0.8) (2015-02-25)
|
46
|
+
|
47
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.0.7...v0.0.8)
|
48
|
+
|
49
|
+
**Merged pull requests:**
|
50
|
+
|
51
|
+
- Features automatically disabled\_for nil input [\#3](https://github.com/EasyPost/toggles/pull/3) ([victoryftw](https://github.com/victoryftw))
|
52
|
+
|
53
|
+
## [v0.0.7](https://github.com/EasyPost/toggles/tree/v0.0.7) (2014-12-24)
|
54
|
+
|
55
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.0.6...v0.0.7)
|
56
|
+
|
57
|
+
**Closed issues:**
|
58
|
+
|
59
|
+
- Allow Fixnums etc. as top-level subjects [\#1](https://github.com/EasyPost/toggles/issues/1)
|
60
|
+
|
61
|
+
## [v0.0.6](https://github.com/EasyPost/toggles/tree/v0.0.6) (2014-12-22)
|
62
|
+
|
63
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.0.5...v0.0.6)
|
64
|
+
|
65
|
+
## [v0.0.5](https://github.com/EasyPost/toggles/tree/v0.0.5) (2014-12-17)
|
66
|
+
|
67
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.0.4...v0.0.5)
|
68
|
+
|
69
|
+
## [v0.0.4](https://github.com/EasyPost/toggles/tree/v0.0.4) (2014-12-10)
|
70
|
+
|
71
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.0.3...v0.0.4)
|
72
|
+
|
73
|
+
## [v0.0.3](https://github.com/EasyPost/toggles/tree/v0.0.3) (2014-12-10)
|
74
|
+
|
75
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/v0.0.2...v0.0.3)
|
76
|
+
|
77
|
+
## [v0.0.2](https://github.com/EasyPost/toggles/tree/v0.0.2) (2014-12-10)
|
78
|
+
|
79
|
+
[Full Changelog](https://github.com/EasyPost/toggles/compare/b19564d6ef5eb5d9c3b44d4eafe2fbac9d8a8938...v0.0.2)
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Toggles
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/toggles)
|
4
|
-
[](https://github.com/EasyPost/toggles/actions?query=workflow%3ACI)
|
5
5
|
|
6
6
|
YAML backed feature toggles
|
7
7
|
|
@@ -41,9 +41,6 @@ features
|
|
41
41
|
└── test.yml
|
42
42
|
```
|
43
43
|
|
44
|
-
The classes `Feature::Test`, `Feature::Thing::One` and `Feature::Thing::Two` will be available for use within
|
45
|
-
your application.
|
46
|
-
|
47
44
|
You can call the `Toggles.init` method to force re-parsing the configuration and re-initializing all Features
|
48
45
|
structures at any time. The `Toggles.reinit_if_necessary` method is a convenience helper which will only
|
49
46
|
re-initialize of the top-level features directory has changed. Note that, in general, this will only detect
|
@@ -65,13 +62,13 @@ user:
|
|
65
62
|
Check if the feature is enabled or disabled:
|
66
63
|
|
67
64
|
```ruby
|
68
|
-
Feature
|
69
|
-
Feature
|
70
|
-
Feature
|
65
|
+
Feature.enabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # true
|
66
|
+
Feature.enabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # true
|
67
|
+
Feature.enabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # false
|
71
68
|
|
72
|
-
Feature
|
73
|
-
Feature
|
74
|
-
Feature
|
69
|
+
Feature.disabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 12345)) # false
|
70
|
+
Feature.disabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 54321)) # false
|
71
|
+
Feature.disabled?(:new_feature, :available_for_presentation, user: OpenStruct.new(id: 7)) # true
|
75
72
|
```
|
76
73
|
|
77
74
|
## License
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Feature::ConstantLookup
|
2
|
+
Error = Class.new(NameError) do
|
3
|
+
attr_reader :sym
|
4
|
+
|
5
|
+
def initialize(sym)
|
6
|
+
@sym = sym
|
7
|
+
super(sym.join('::'))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return a tree walker that translates Module#const_missing(sym) into the next child node
|
12
|
+
#
|
13
|
+
# So Features::Cat::BearDog walks as:
|
14
|
+
# * next_features = Feature.features # root
|
15
|
+
# * const_missing(:Cat) => next_features = next_features['cat']
|
16
|
+
# * const_missing(:BearDog) => next_features['bear_dog']
|
17
|
+
# * const_missing(:CN22) => next_features['c_n_22']
|
18
|
+
#
|
19
|
+
# Defined at Toggles.features_dir + "/cat/bear_dog.yaml"
|
20
|
+
#
|
21
|
+
# @raise [Error] if constant cannot be resolved
|
22
|
+
def self.from(features, path)
|
23
|
+
Class.new {
|
24
|
+
class << self
|
25
|
+
attr_accessor :features, :path
|
26
|
+
|
27
|
+
def const_missing(sym)
|
28
|
+
# translate class name into path part i.e :BearDog #=> 'bear_dog'
|
29
|
+
key = sym.to_s
|
30
|
+
key.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
|
31
|
+
key.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
|
32
|
+
key.downcase!
|
33
|
+
|
34
|
+
subtree_or_feature = features.fetch(key.to_sym)
|
35
|
+
|
36
|
+
if subtree_or_feature.is_a?(Hash)
|
37
|
+
Feature::ConstantLookup.from(subtree_or_feature, path + [sym])
|
38
|
+
else
|
39
|
+
subtree_or_feature
|
40
|
+
end
|
41
|
+
rescue KeyError
|
42
|
+
raise Error, path + [sym]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
}.tap do |resolver|
|
46
|
+
resolver.features = features
|
47
|
+
resolver.path = path
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Feature::Attribute
|
2
|
+
def self.call(entity, attr_name, expected)
|
3
|
+
if expected.kind_of? Hash
|
4
|
+
expected.all? do |operation, rules|
|
5
|
+
if Feature.operations.include? operation.to_sym
|
6
|
+
Feature.operations[operation.to_sym].call(entity, attr_name, rules)
|
7
|
+
else
|
8
|
+
call(entity.send(attr_name), operation, rules)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
else
|
12
|
+
entity.send(attr_name) == expected
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
module Feature
|
2
|
-
module Operation
|
3
|
-
class And
|
4
|
-
def self.call(entity, attr_name, expected)
|
5
|
-
expected.all? do |operation, value|
|
6
|
-
if OPERATIONS.include? operation.to_sym
|
7
|
-
OPERATIONS[operation.to_sym].call(entity, attr_name, value)
|
8
|
-
else
|
9
|
-
Operation::Attribute.call(entity, operation, value)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,43 +1,48 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'ostruct'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'yaml'
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
extend Forwardable
|
5
|
+
Feature::Permissions = Struct.new(:rules) do
|
6
|
+
extend Forwardable
|
7
7
|
|
8
|
-
|
8
|
+
def_delegators :rules, :all?, :keys
|
9
9
|
|
10
|
-
|
10
|
+
def self.from_yaml(path)
|
11
|
+
new(
|
12
|
+
YAML.safe_load(File.read(path), permitted_classes: [Symbol])
|
13
|
+
)
|
14
|
+
end
|
11
15
|
|
12
|
-
|
13
|
-
|
14
|
-
|
16
|
+
def subjects
|
17
|
+
@subjects ||= keys.map(&:to_sym)
|
18
|
+
end
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
def valid_for?(entities)
|
21
|
+
unless subjects == entities.keys
|
22
|
+
raise Feature::Subject::Invalid, Feature::Subject.difference(subjects, entities.keys)
|
18
23
|
end
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
raise Subject::Invalid, Subject.difference(subjects, entities.keys)
|
23
|
-
end
|
24
|
-
|
25
|
-
rules.all? do |name, rule|
|
26
|
-
entity = entities[name.to_sym]
|
25
|
+
rules.all? do |name, rule|
|
26
|
+
entity = entities[name.to_sym]
|
27
27
|
|
28
|
-
|
29
|
-
return false
|
30
|
-
end
|
28
|
+
return false if entity.nil?
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
if entity.class.ancestors.find { |ancestor| ancestor == Comparable }
|
31
|
+
entity = OpenStruct.new(name => entity)
|
32
|
+
rule = { name => rule }
|
33
|
+
end
|
36
34
|
|
37
|
-
|
38
|
-
|
39
|
-
end
|
35
|
+
rule.all? do |key, value|
|
36
|
+
Feature.operations.fetch(key.to_sym, Feature::Attribute).call(entity, key, value)
|
40
37
|
end
|
41
38
|
end
|
42
39
|
end
|
40
|
+
|
41
|
+
def enabled_for?(subjects = {})
|
42
|
+
valid_for?(subjects)
|
43
|
+
end
|
44
|
+
|
45
|
+
def disabled_for?(subjects = {})
|
46
|
+
!valid_for?(subjects)
|
47
|
+
end
|
43
48
|
end
|
data/lib/toggles/feature.rb
CHANGED
@@ -1,24 +1,84 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require "toggles/feature/permissions"
|
4
|
-
require "toggles/feature/subject"
|
5
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
6
3
|
module Feature
|
7
|
-
|
8
|
-
|
9
|
-
in: Operation::In,
|
10
|
-
lt: Operation::LessThan,
|
11
|
-
not: Operation::Not,
|
12
|
-
or: Operation::Or,
|
13
|
-
range: Operation::Range}
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
Unknown = Class.new(Error)
|
14
6
|
|
15
|
-
|
7
|
+
def self.features
|
8
|
+
@features ||= {}
|
9
|
+
end
|
16
10
|
|
17
|
-
def self.
|
18
|
-
|
11
|
+
def self.operations
|
12
|
+
@operations ||= {}
|
19
13
|
end
|
20
14
|
|
15
|
+
# @deprecated This is an abuse of lazy dispatch that creates cryptic errors
|
21
16
|
def self.const_missing(sym)
|
22
|
-
|
17
|
+
ConstantLookup.from(features, [:Feature]).const_missing(sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.enabled?(*sym, **criteria)
|
21
|
+
sym
|
22
|
+
.inject(features) { |a, e| a.fetch(e) }
|
23
|
+
.enabled_for?(criteria)
|
24
|
+
rescue KeyError
|
25
|
+
raise Unknown, sym.inspect
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.disabled?(*sym, **criteria)
|
29
|
+
!enabled?(*sym, **criteria)
|
23
30
|
end
|
24
31
|
end
|
32
|
+
|
33
|
+
require 'toggles/constant_lookup'
|
34
|
+
require "toggles/feature/attribute"
|
35
|
+
require "toggles/feature/permissions"
|
36
|
+
require "toggles/feature/subject"
|
37
|
+
|
38
|
+
Feature.operations[:and] = lambda { |entity, attr_name, expected|
|
39
|
+
expected.all? do |operation, value|
|
40
|
+
if Feature.operations.include? operation.to_sym
|
41
|
+
Feature.operations[operation.to_sym].call(entity, attr_name, value)
|
42
|
+
else
|
43
|
+
Feature::Attribute.call(entity, operation, value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
}
|
47
|
+
|
48
|
+
Feature.operations[:in] = lambda { |entity, attr_name, expected|
|
49
|
+
if expected.is_a? Hash
|
50
|
+
expected = expected.reduce([]) do |_list, (operation, args)|
|
51
|
+
Feature.operations[operation.to_sym].call(args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
expected.include? entity.send(attr_name.to_sym)
|
56
|
+
}
|
57
|
+
|
58
|
+
Feature.operations[:not] = lambda { |entity, attr_name, expected|
|
59
|
+
if expected.is_a? Hash
|
60
|
+
expected.none? do |operation, value|
|
61
|
+
Feature.operations[operation.to_sym].call(entity, attr_name, value)
|
62
|
+
end
|
63
|
+
else
|
64
|
+
entity.send(attr_name) != expected
|
65
|
+
end
|
66
|
+
}
|
67
|
+
|
68
|
+
Feature.operations[:or] = lambda { |entity, attr_name, expected|
|
69
|
+
expected.any? do |operation, value|
|
70
|
+
if Feature.operations.include? operation.to_sym
|
71
|
+
Feature.operations[operation.to_sym].call(entity, attr_name, value)
|
72
|
+
else
|
73
|
+
Feature::Attribute.call(entity, operation, value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
}
|
77
|
+
|
78
|
+
Feature.operations[:gt] = ->(entity, attr_name, expected) { entity.send(attr_name) > expected }
|
79
|
+
Feature.operations[:lt] = ->(entity, attr_name, expected) { entity.send(attr_name) < expected }
|
80
|
+
Feature.operations[:range] = lambda { |args|
|
81
|
+
raise StandardError, 'Invalid range operation' if args.size != 2
|
82
|
+
|
83
|
+
(args.first..args.last)
|
84
|
+
}
|
data/lib/toggles.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'find'
|
2
|
+
require 'pathname'
|
3
|
+
require 'set'
|
3
4
|
|
4
|
-
require
|
5
|
-
|
5
|
+
require 'toggles/version'
|
6
|
+
|
7
|
+
require 'toggles/configuration'
|
8
|
+
require 'toggles/feature'
|
6
9
|
|
7
10
|
module Toggles
|
8
11
|
extend self
|
@@ -19,76 +22,35 @@ module Toggles
|
|
19
22
|
@configuration ||= Configuration.new
|
20
23
|
end
|
21
24
|
|
22
|
-
# Dynamically create modules and classes within the `Feature` module based on
|
23
|
-
# the directory structure of `features`.
|
24
|
-
#
|
25
|
-
# For example if the `features` directory has the structure:
|
26
|
-
#
|
27
|
-
# features
|
28
|
-
# ├── thing
|
29
|
-
# | ├── one.yml
|
30
|
-
# | └── two.yml
|
31
|
-
# └── test.yml
|
32
|
-
#
|
33
|
-
# `Feature::Test`, `Feature::Thing::One`, `Feature::Thing::Two` would be
|
34
|
-
# available by default.
|
35
|
-
#
|
36
25
|
def init
|
37
|
-
return unless Dir.
|
38
|
-
|
39
|
-
new_tree = Module.new
|
26
|
+
return unless Dir.exist? configuration.features_dir
|
40
27
|
|
41
28
|
top_level = File.realpath(configuration.features_dir)
|
42
29
|
top_level_p = Pathname.new(top_level)
|
43
30
|
|
44
|
-
|
45
|
-
previous = new_tree
|
46
|
-
abspath = path
|
47
|
-
path = Pathname.new(path).relative_path_from(top_level_p).to_s
|
48
|
-
if path.match(/\.ya?ml\Z/)
|
49
|
-
base = path.chomp(File.extname(path)).split("/")
|
50
|
-
if base.size > 1
|
51
|
-
directories = base[0...-1]
|
52
|
-
filename = base[-1]
|
53
|
-
else
|
54
|
-
directories = []
|
55
|
-
filename = base[0]
|
56
|
-
end
|
57
|
-
|
58
|
-
directories.each do |directory|
|
59
|
-
module_name = directory.split("_").map(&:capitalize).join.to_sym
|
60
|
-
previous = if previous.constants.include? module_name
|
61
|
-
previous.const_get(module_name)
|
62
|
-
else
|
63
|
-
previous.const_set(module_name, Module.new)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
cls = Class.new(Feature::Base) do |c|
|
68
|
-
c.const_set(:PERMISSIONS, Feature::Permissions.new(abspath))
|
69
|
-
end
|
31
|
+
Feature.features.clear
|
70
32
|
|
71
|
-
|
72
|
-
|
33
|
+
Dir[File.join(top_level, '**/*.{yaml,yml}')].each do |abspath|
|
34
|
+
path = Pathname.new(abspath).relative_path_from(top_level_p).to_s
|
35
|
+
features = path.split('/')[0..-2].inject(Feature.features) { |a, e| a[e.to_sym] ||= {} }
|
36
|
+
feature_key = File.basename(path, File.extname(path)).to_sym
|
37
|
+
features[feature_key] = Feature::Permissions.from_yaml(abspath)
|
73
38
|
end
|
74
39
|
|
75
40
|
stbuf = File.stat(top_level)
|
76
41
|
@stat_tuple = StatResult.new(stbuf.ino, stbuf.mtime)
|
77
|
-
|
78
|
-
Feature.set_tree(new_tree)
|
79
42
|
end
|
80
43
|
|
81
44
|
def reinit_if_changed
|
82
45
|
# Reload the configuration if the top-level directory has changed.
|
83
46
|
# Does not detect changes to files inside that directory unless your
|
84
47
|
# filesystem propagates mtimes.
|
85
|
-
return unless Dir.
|
48
|
+
return unless Dir.exist? configuration.features_dir
|
49
|
+
|
86
50
|
top_level = File.realpath(configuration.features_dir)
|
87
51
|
stbuf = File.stat(top_level)
|
88
52
|
stat_tuple = StatResult.new(stbuf.ino, stbuf.mtime)
|
89
53
|
|
90
|
-
if @stat_tuple != stat_tuple
|
91
|
-
init
|
92
|
-
end
|
54
|
+
init if @stat_tuple != stat_tuple
|
93
55
|
end
|
94
56
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,23 @@
|
|
1
1
|
describe "Feature::Type" do
|
2
|
+
specify 'deprecated' do
|
3
|
+
aggregate_failures do
|
4
|
+
expect(Feature::Type.enabled_for?(user_id: 1)).to eq true
|
5
|
+
expect(Feature::Type.enabled_for?(user_id: 25)).to eq false
|
6
|
+
expect(Feature::Type.enabled_for?(user_id: nil)).to eq false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
2
10
|
specify do
|
3
|
-
|
4
|
-
|
5
|
-
|
11
|
+
aggregate_failures do
|
12
|
+
expect(Feature.enabled?(:type, user_id: 1)).to eq(true)
|
13
|
+
expect(Feature.disabled?(:type, user_id: 1)).to eq(false)
|
14
|
+
expect(Feature.enabled?(:type, user_id: 25)).to eq(false)
|
15
|
+
expect(Feature.enabled?(:nested_foo, :bar_baz, id: 25)).to eq(false)
|
16
|
+
expect(Feature.enabled?(:nested_foo, :bar_baz, id: 1)).to eq(true)
|
17
|
+
expect(Feature.disabled?(:type, user_id: 25)).to eq(true)
|
18
|
+
expect(Feature.disabled?(:nested_foo, :bar_baz, id: 25)).to eq(true)
|
19
|
+
expect(Feature.disabled?(:nested_foo, :bar_baz, id: 1)).to eq(false)
|
20
|
+
expect { Feature.disabled?(:nested_foo, :bar_boz, id: 1) }.to raise_error(Feature::Unknown)
|
21
|
+
end
|
6
22
|
end
|
7
23
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
RSpec.describe Feature::Attribute do
|
2
|
+
specify do
|
3
|
+
expect(described_class.call(double(id: 50), :id, 50)).to eq true
|
4
|
+
expect(described_class.call(double(id: 50), :id, 51)).to eq false
|
5
|
+
|
6
|
+
expect(described_class.call(double(id: 50), :id, { 'in' => (0..50), 'not' => 49 })).to eq true
|
7
|
+
expect(described_class.call(double(id: 49), :id, { 'in' => (0..50), 'not' => 49 })).to eq false
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe 'operations' do
|
4
|
+
context 'and' do
|
5
|
+
subject { Feature.operations[:and] }
|
6
|
+
|
7
|
+
specify do
|
8
|
+
[[60, true], [40, false], [80, false]].each do |(id, expected)|
|
9
|
+
expect(
|
10
|
+
subject.call(double(id: id), :id, {"gt" => 50, "lt" => 70})
|
11
|
+
).to eq expected
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'gt' do
|
17
|
+
subject { Feature.operations[:gt] }
|
18
|
+
|
19
|
+
specify do
|
20
|
+
expect(subject.call(double(id: 50), :id, 40)).to eq true
|
21
|
+
expect(subject.call(double(id: 50), :id, 60)).to eq false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'in' do
|
26
|
+
subject { Feature.operations[:in] }
|
27
|
+
|
28
|
+
specify do
|
29
|
+
expect(subject.call(double(id: 50), :id, {"range" => [40, 60]})).to eq true
|
30
|
+
expect(subject.call(double(id: 50), :id, [1])).to eq false
|
31
|
+
expect(subject.call(double(id: nil), :id, [1])).to eq false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'lt' do
|
36
|
+
subject { Feature.operations[:lt] }
|
37
|
+
|
38
|
+
specify do
|
39
|
+
expect(subject.call(double(id: 50), :id, 60)).to eq true
|
40
|
+
expect(subject.call(double(id: 50), :id, 40)).to eq false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'not' do
|
45
|
+
subject { Feature.operations[:not] }
|
46
|
+
|
47
|
+
specify do
|
48
|
+
expect(subject.call(double(id: 50), :id, {"in" => (10..20)})).to eq true
|
49
|
+
expect(subject.call(double(id: 50), :id, 50)).to eq false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'or' do
|
54
|
+
subject { Feature.operations[:or] }
|
55
|
+
|
56
|
+
specify do
|
57
|
+
expect(subject.call(double(id: 16), :id, {"in" => (10..20), "not" => 15})).to eq true
|
58
|
+
expect(subject.call(double(id: 15), :id, {"in" => (10..20), "not" => 15})).to eq true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'range' do
|
63
|
+
subject { Feature.operations[:range] }
|
64
|
+
|
65
|
+
specify do
|
66
|
+
expect(subject.call([10, 20])).to eq((10..20))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
describe Feature::Permissions do
|
2
2
|
let(:path) { "features/multiple_subjects.yml" }
|
3
3
|
|
4
|
-
subject { Feature::Permissions.
|
4
|
+
subject { Feature::Permissions.from_yaml(path) }
|
5
5
|
|
6
6
|
its(:rules) { is_expected.to eq({"user"=>{"id"=>1, "logged_in?"=>true},
|
7
7
|
"widget"=>{"id"=>2}}) }
|
@@ -0,0 +1,37 @@
|
|
1
|
+
describe 'features' do
|
2
|
+
let(:user) { double(id: 1, logged_in?: true) }
|
3
|
+
let(:widget) { double(id: 2) }
|
4
|
+
|
5
|
+
context 'multiple subjects' do
|
6
|
+
specify { expect(Feature).to be_enabled(:multiple_subjects, user: user, widget: widget) }
|
7
|
+
specify { expect(Feature::MultipleSubjects).to be_enabled_for(user: user, widget: widget) }
|
8
|
+
specify { expect(Feature).not_to be_disabled(:multiple_subjects, user: user, widget: widget) }
|
9
|
+
specify { expect(Feature::MultipleSubjects).not_to be_disabled_for(user: user, widget: widget) }
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'abbreviation with numbers' do
|
13
|
+
subject { Feature::AbbreviationsCN22.new(user: user) }
|
14
|
+
|
15
|
+
specify { expect(Feature).to be_enabled(:abbreviations_cn22, user: user) }
|
16
|
+
specify { expect(Feature::AbbreviationsCN22).to be_enabled_for(user: user) }
|
17
|
+
specify { expect(Feature).not_to be_disabled(:abbreviations_cn22, user: user) }
|
18
|
+
specify { expect(Feature::AbbreviationsCN22).not_to be_disabled_for(user: user) }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'irregular capitalization' do
|
22
|
+
specify { expect(Feature).to be_enabled(:s3_file, user: user) }
|
23
|
+
specify { expect(Feature::S3File).to be_enabled_for(user: user) }
|
24
|
+
specify { expect(Feature).not_to be_disabled(:s3_file, user: user) }
|
25
|
+
specify { expect(Feature).to be_disabled(:s3_file, user: widget) }
|
26
|
+
specify { expect(Feature::S3File).not_to be_disabled_for(user: user) }
|
27
|
+
specify { expect(Feature::S3File).to be_disabled_for(user: widget) }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'irregular capitalization' do
|
31
|
+
specify { expect(Feature).to be_enabled(:file_s3, user: user) }
|
32
|
+
specify { expect(Feature::FileS3).to be_enabled_for(user: user) }
|
33
|
+
specify { expect(Feature).not_to be_disabled(:file_s3, user: user) }
|
34
|
+
specify { expect(Feature::FileS3).not_to be_disabled_for(user: user) }
|
35
|
+
specify { expect(Feature).to be_disabled(:file_s3, user: widget) }
|
36
|
+
end
|
37
|
+
end
|
data/spec/toggles/init_spec.rb
CHANGED
@@ -17,7 +17,7 @@ describe Toggles do
|
|
17
17
|
Toggles.configure do |c|
|
18
18
|
c.features_dir = temp_dir
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(true)
|
22
22
|
expect(Feature::Bar::Users.enabled_for?(id: 3)).to eq(true)
|
23
23
|
end
|
@@ -36,7 +36,7 @@ describe Toggles do
|
|
36
36
|
Toggles.configure do |c|
|
37
37
|
c.features_dir = temp_dir
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(true)
|
41
41
|
expect(Feature::Foo::Children.enabled_for?(id: 1)).to eq(true)
|
42
42
|
|
@@ -49,7 +49,10 @@ describe Toggles do
|
|
49
49
|
Toggles.init
|
50
50
|
|
51
51
|
expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(false)
|
52
|
-
expect { Feature::
|
52
|
+
expect { Feature::Bar::Children.enabled_for?(id: 1) }
|
53
|
+
.to raise_error(Feature::ConstantLookup::Error, 'Feature::Bar')
|
54
|
+
expect { Feature::Foo::Children.enabled_for?(id: 1) }
|
55
|
+
.to raise_error(Feature::ConstantLookup::Error, 'Feature::Foo::Children')
|
53
56
|
end
|
54
57
|
end
|
55
58
|
|
@@ -63,8 +66,10 @@ describe Toggles do
|
|
63
66
|
c.features_dir = "#{temp_dir}/features"
|
64
67
|
end
|
65
68
|
|
69
|
+
# the OS might reuse the inode if we do this in the obvious order
|
70
|
+
Dir.mkdir("#{temp_dir}/features2")
|
66
71
|
Dir.delete("#{temp_dir}/features")
|
67
|
-
|
72
|
+
File.rename("#{temp_dir}/features2", "#{temp_dir}/features")
|
68
73
|
|
69
74
|
expect(Toggles).to receive("init")
|
70
75
|
Toggles.reinit_if_changed
|
data/toggles.gemspec
CHANGED
@@ -1,13 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'toggles/version'
|
6
|
+
|
1
7
|
Gem::Specification.new do |s|
|
2
|
-
s.name =
|
3
|
-
s.version =
|
4
|
-
s.authors = [
|
5
|
-
s.summary =
|
6
|
-
s.email =
|
7
|
-
s.homepage =
|
8
|
-
s.license =
|
8
|
+
s.name = 'toggles'
|
9
|
+
s.version = Toggles::VERSION
|
10
|
+
s.authors = ['Andrew Tribone', 'James Brown', 'Josh Lane']
|
11
|
+
s.summary = 'YAML backed feature toggles'
|
12
|
+
s.email = 'oss@easypost.com'
|
13
|
+
s.homepage = 'https://github.com/EasyPost/toggles'
|
14
|
+
s.license = 'ISC'
|
9
15
|
s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
10
|
-
s.test_files = s.files.grep(
|
16
|
+
s.test_files = s.files.grep(%r{^(spec)/})
|
11
17
|
s.description = <<-EOF
|
12
18
|
YAML-backed implementation of the feature flags pattern. Build a
|
13
19
|
hierarchy of features in YAML files in the filesystem, apply various
|
@@ -15,12 +21,9 @@ Gem::Specification.new do |s|
|
|
15
21
|
check whether a given feature should be applied.
|
16
22
|
EOF
|
17
23
|
|
18
|
-
s.add_development_dependency
|
19
|
-
s.add_development_dependency
|
20
|
-
s.add_development_dependency
|
21
|
-
s.add_development_dependency
|
22
|
-
s.add_development_dependency
|
23
|
-
s.add_development_dependency "rspec"
|
24
|
-
s.add_development_dependency "rspec-its"
|
25
|
-
s.add_development_dependency "rspec-temp_dir"
|
24
|
+
s.add_development_dependency 'bundler'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_development_dependency 'rspec'
|
27
|
+
s.add_development_dependency 'rspec-its'
|
28
|
+
s.add_development_dependency 'rspec-temp_dir'
|
26
29
|
end
|
metadata
CHANGED
@@ -1,126 +1,85 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toggles
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Tribone
|
8
8
|
- James Brown
|
9
|
-
|
9
|
+
- Josh Lane
|
10
|
+
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date:
|
13
|
+
date: 2023-03-16 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: bundler
|
16
17
|
requirement: !ruby/object:Gem::Requirement
|
17
18
|
requirements:
|
18
|
-
- -
|
19
|
+
- - ">="
|
19
20
|
- !ruby/object:Gem::Version
|
20
21
|
version: '0'
|
21
22
|
type: :development
|
22
23
|
prerelease: false
|
23
24
|
version_requirements: !ruby/object:Gem::Requirement
|
24
25
|
requirements:
|
25
|
-
- -
|
26
|
-
- !ruby/object:Gem::Version
|
27
|
-
version: '0'
|
28
|
-
- !ruby/object:Gem::Dependency
|
29
|
-
name: pry
|
30
|
-
requirement: !ruby/object:Gem::Requirement
|
31
|
-
requirements:
|
32
|
-
- - '>='
|
33
|
-
- !ruby/object:Gem::Version
|
34
|
-
version: '0'
|
35
|
-
type: :development
|
36
|
-
prerelease: false
|
37
|
-
version_requirements: !ruby/object:Gem::Requirement
|
38
|
-
requirements:
|
39
|
-
- - '>='
|
40
|
-
- !ruby/object:Gem::Version
|
41
|
-
version: '0'
|
42
|
-
- !ruby/object:Gem::Dependency
|
43
|
-
name: pry-nav
|
44
|
-
requirement: !ruby/object:Gem::Requirement
|
45
|
-
requirements:
|
46
|
-
- - '>='
|
47
|
-
- !ruby/object:Gem::Version
|
48
|
-
version: '0'
|
49
|
-
type: :development
|
50
|
-
prerelease: false
|
51
|
-
version_requirements: !ruby/object:Gem::Requirement
|
52
|
-
requirements:
|
53
|
-
- - '>='
|
54
|
-
- !ruby/object:Gem::Version
|
55
|
-
version: '0'
|
56
|
-
- !ruby/object:Gem::Dependency
|
57
|
-
name: pry-remote
|
58
|
-
requirement: !ruby/object:Gem::Requirement
|
59
|
-
requirements:
|
60
|
-
- - '>='
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
version: '0'
|
63
|
-
type: :development
|
64
|
-
prerelease: false
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
66
|
-
requirements:
|
67
|
-
- - '>='
|
26
|
+
- - ">="
|
68
27
|
- !ruby/object:Gem::Version
|
69
28
|
version: '0'
|
70
29
|
- !ruby/object:Gem::Dependency
|
71
30
|
name: rake
|
72
31
|
requirement: !ruby/object:Gem::Requirement
|
73
32
|
requirements:
|
74
|
-
- -
|
33
|
+
- - ">="
|
75
34
|
- !ruby/object:Gem::Version
|
76
35
|
version: '0'
|
77
36
|
type: :development
|
78
37
|
prerelease: false
|
79
38
|
version_requirements: !ruby/object:Gem::Requirement
|
80
39
|
requirements:
|
81
|
-
- -
|
40
|
+
- - ">="
|
82
41
|
- !ruby/object:Gem::Version
|
83
42
|
version: '0'
|
84
43
|
- !ruby/object:Gem::Dependency
|
85
44
|
name: rspec
|
86
45
|
requirement: !ruby/object:Gem::Requirement
|
87
46
|
requirements:
|
88
|
-
- -
|
47
|
+
- - ">="
|
89
48
|
- !ruby/object:Gem::Version
|
90
49
|
version: '0'
|
91
50
|
type: :development
|
92
51
|
prerelease: false
|
93
52
|
version_requirements: !ruby/object:Gem::Requirement
|
94
53
|
requirements:
|
95
|
-
- -
|
54
|
+
- - ">="
|
96
55
|
- !ruby/object:Gem::Version
|
97
56
|
version: '0'
|
98
57
|
- !ruby/object:Gem::Dependency
|
99
58
|
name: rspec-its
|
100
59
|
requirement: !ruby/object:Gem::Requirement
|
101
60
|
requirements:
|
102
|
-
- -
|
61
|
+
- - ">="
|
103
62
|
- !ruby/object:Gem::Version
|
104
63
|
version: '0'
|
105
64
|
type: :development
|
106
65
|
prerelease: false
|
107
66
|
version_requirements: !ruby/object:Gem::Requirement
|
108
67
|
requirements:
|
109
|
-
- -
|
68
|
+
- - ">="
|
110
69
|
- !ruby/object:Gem::Version
|
111
70
|
version: '0'
|
112
71
|
- !ruby/object:Gem::Dependency
|
113
72
|
name: rspec-temp_dir
|
114
73
|
requirement: !ruby/object:Gem::Requirement
|
115
74
|
requirements:
|
116
|
-
- -
|
75
|
+
- - ">="
|
117
76
|
- !ruby/object:Gem::Version
|
118
77
|
version: '0'
|
119
78
|
type: :development
|
120
79
|
prerelease: false
|
121
80
|
version_requirements: !ruby/object:Gem::Requirement
|
122
81
|
requirements:
|
123
|
-
- -
|
82
|
+
- - ">="
|
124
83
|
- !ruby/object:Gem::Version
|
125
84
|
version: '0'
|
126
85
|
description: |2
|
@@ -133,33 +92,36 @@ executables: []
|
|
133
92
|
extensions: []
|
134
93
|
extra_rdoc_files: []
|
135
94
|
files:
|
136
|
-
- .
|
137
|
-
- .
|
95
|
+
- ".github/workflows/ci.yml"
|
96
|
+
- ".gitignore"
|
97
|
+
- ".rspec"
|
98
|
+
- CHANGELOG.md
|
138
99
|
- Gemfile
|
139
100
|
- LICENSE.txt
|
140
101
|
- README.md
|
141
102
|
- Rakefile
|
103
|
+
- features/abbreviations_cn22.yml
|
142
104
|
- features/collection.yml
|
143
105
|
- features/complex_and.yml
|
106
|
+
- features/file_s3.yml
|
144
107
|
- features/multiple_subjects.yml
|
145
108
|
- features/nested_attributes.yml
|
109
|
+
- features/nested_foo/bar_baz.yml
|
146
110
|
- features/or_attributes.yml
|
111
|
+
- features/s3_file.yml
|
147
112
|
- features/type.yml
|
148
113
|
- lib/toggles.rb
|
149
114
|
- lib/toggles/configuration.rb
|
115
|
+
- lib/toggles/constant_lookup.rb
|
150
116
|
- lib/toggles/feature.rb
|
151
|
-
- lib/toggles/feature/
|
152
|
-
- lib/toggles/feature/operation.rb
|
117
|
+
- lib/toggles/feature/attribute.rb
|
153
118
|
- lib/toggles/feature/operation/and.rb
|
154
|
-
- lib/toggles/feature/operation/attribute.rb
|
155
119
|
- lib/toggles/feature/operation/gt.rb
|
156
120
|
- lib/toggles/feature/operation/in.rb
|
157
|
-
- lib/toggles/feature/operation/lt.rb
|
158
|
-
- lib/toggles/feature/operation/not.rb
|
159
|
-
- lib/toggles/feature/operation/or.rb
|
160
121
|
- lib/toggles/feature/operation/range.rb
|
161
122
|
- lib/toggles/feature/permissions.rb
|
162
123
|
- lib/toggles/feature/subject.rb
|
124
|
+
- lib/toggles/version.rb
|
163
125
|
- spec/spec_helper.rb
|
164
126
|
- spec/toggles/configuration_spec.rb
|
165
127
|
- spec/toggles/feature/acceptance/collection_spec.rb
|
@@ -168,41 +130,34 @@ files:
|
|
168
130
|
- spec/toggles/feature/acceptance/nested_attributes_spec.rb
|
169
131
|
- spec/toggles/feature/acceptance/or_attributes_spec.rb
|
170
132
|
- spec/toggles/feature/acceptance/type_spec.rb
|
171
|
-
- spec/toggles/feature/
|
172
|
-
- spec/toggles/feature/
|
173
|
-
- spec/toggles/feature/operation/attribute_spec.rb
|
174
|
-
- spec/toggles/feature/operation/gt_spec.rb
|
175
|
-
- spec/toggles/feature/operation/in_spec.rb
|
176
|
-
- spec/toggles/feature/operation/lt_spec.rb
|
177
|
-
- spec/toggles/feature/operation/not_spec.rb
|
178
|
-
- spec/toggles/feature/operation/or_spec.rb
|
179
|
-
- spec/toggles/feature/operation/range_spec.rb
|
133
|
+
- spec/toggles/feature/attribute_spec.rb
|
134
|
+
- spec/toggles/feature/operation_spec.rb
|
180
135
|
- spec/toggles/feature/permissions_spec.rb
|
181
136
|
- spec/toggles/feature/subject_spec.rb
|
137
|
+
- spec/toggles/feature_spec.rb
|
182
138
|
- spec/toggles/init_spec.rb
|
183
139
|
- toggles.gemspec
|
184
140
|
homepage: https://github.com/EasyPost/toggles
|
185
141
|
licenses:
|
186
142
|
- ISC
|
187
143
|
metadata: {}
|
188
|
-
post_install_message:
|
144
|
+
post_install_message:
|
189
145
|
rdoc_options: []
|
190
146
|
require_paths:
|
191
147
|
- lib
|
192
148
|
required_ruby_version: !ruby/object:Gem::Requirement
|
193
149
|
requirements:
|
194
|
-
- -
|
150
|
+
- - ">="
|
195
151
|
- !ruby/object:Gem::Version
|
196
152
|
version: '0'
|
197
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
198
154
|
requirements:
|
199
|
-
- -
|
155
|
+
- - ">="
|
200
156
|
- !ruby/object:Gem::Version
|
201
157
|
version: '0'
|
202
158
|
requirements: []
|
203
|
-
|
204
|
-
|
205
|
-
signing_key:
|
159
|
+
rubygems_version: 3.3.26
|
160
|
+
signing_key:
|
206
161
|
specification_version: 4
|
207
162
|
summary: YAML backed feature toggles
|
208
163
|
test_files:
|
@@ -214,15 +169,9 @@ test_files:
|
|
214
169
|
- spec/toggles/feature/acceptance/nested_attributes_spec.rb
|
215
170
|
- spec/toggles/feature/acceptance/or_attributes_spec.rb
|
216
171
|
- spec/toggles/feature/acceptance/type_spec.rb
|
217
|
-
- spec/toggles/feature/
|
218
|
-
- spec/toggles/feature/
|
219
|
-
- spec/toggles/feature/operation/attribute_spec.rb
|
220
|
-
- spec/toggles/feature/operation/gt_spec.rb
|
221
|
-
- spec/toggles/feature/operation/in_spec.rb
|
222
|
-
- spec/toggles/feature/operation/lt_spec.rb
|
223
|
-
- spec/toggles/feature/operation/not_spec.rb
|
224
|
-
- spec/toggles/feature/operation/or_spec.rb
|
225
|
-
- spec/toggles/feature/operation/range_spec.rb
|
172
|
+
- spec/toggles/feature/attribute_spec.rb
|
173
|
+
- spec/toggles/feature/operation_spec.rb
|
226
174
|
- spec/toggles/feature/permissions_spec.rb
|
227
175
|
- spec/toggles/feature/subject_spec.rb
|
176
|
+
- spec/toggles/feature_spec.rb
|
228
177
|
- spec/toggles/init_spec.rb
|
data/lib/toggles/feature/base.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
require "yaml"
|
2
|
-
|
3
|
-
module Feature
|
4
|
-
class Base
|
5
|
-
attr_reader :subjects
|
6
|
-
|
7
|
-
def self.enabled_for?(subjects = {})
|
8
|
-
new(subjects).enabled?
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.disabled_for?(subjects = {})
|
12
|
-
!enabled_for? subjects
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize(subjects)
|
16
|
-
@subjects = subjects
|
17
|
-
end
|
18
|
-
|
19
|
-
def permissions
|
20
|
-
@permissions ||= self.class::PERMISSIONS
|
21
|
-
end
|
22
|
-
|
23
|
-
def enabled?
|
24
|
-
permissions.valid_for? subjects
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module Feature
|
2
|
-
module Operation
|
3
|
-
class Attribute
|
4
|
-
def self.call(entity, attr_name, expected)
|
5
|
-
if expected.kind_of? Hash
|
6
|
-
expected.all? do |operation, rules|
|
7
|
-
if OPERATIONS.include? operation.to_sym
|
8
|
-
OPERATIONS[operation.to_sym].call(entity, attr_name, rules)
|
9
|
-
else
|
10
|
-
Operation::Attribute.call(entity.send(attr_name), operation, rules)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
else
|
14
|
-
entity.send(attr_name) == expected
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
module Feature
|
2
|
-
module Operation
|
3
|
-
class Not
|
4
|
-
def self.call(entity, attr_name, expected)
|
5
|
-
if expected.kind_of? Hash
|
6
|
-
expected.none? do |operation, value|
|
7
|
-
OPERATIONS[operation.to_sym].call(entity, attr_name, value)
|
8
|
-
end
|
9
|
-
else
|
10
|
-
entity.send(attr_name) != expected
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
module Feature
|
2
|
-
module Operation
|
3
|
-
class Or
|
4
|
-
def self.call(entity, attr_name, expected)
|
5
|
-
expected.any? do |operation, value|
|
6
|
-
if OPERATIONS.include? operation.to_sym
|
7
|
-
OPERATIONS[operation.to_sym].call(entity, attr_name, value)
|
8
|
-
else
|
9
|
-
Operation::Attribute.call(entity, operation, value)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,8 +0,0 @@
|
|
1
|
-
require "toggles/feature/operation/and"
|
2
|
-
require "toggles/feature/operation/attribute"
|
3
|
-
require "toggles/feature/operation/gt"
|
4
|
-
require "toggles/feature/operation/in"
|
5
|
-
require "toggles/feature/operation/lt"
|
6
|
-
require "toggles/feature/operation/not"
|
7
|
-
require "toggles/feature/operation/or"
|
8
|
-
require "toggles/feature/operation/range"
|
@@ -1,10 +0,0 @@
|
|
1
|
-
describe Feature::Base do
|
2
|
-
let(:user) { double(id: 1, logged_in?: true) }
|
3
|
-
let(:widget) { double(id: 2) }
|
4
|
-
|
5
|
-
subject { Feature::MultipleSubjects.new(user: user, widget: widget) }
|
6
|
-
|
7
|
-
its(:enabled?) { is_expected.to eq true }
|
8
|
-
its(:subjects) { is_expected.to eq user: user, widget: widget }
|
9
|
-
its("permissions.subjects") { is_expected.to eq [:user, :widget] }
|
10
|
-
end
|
@@ -1,9 +0,0 @@
|
|
1
|
-
describe Feature::Operation::Attribute do
|
2
|
-
specify do
|
3
|
-
expect(described_class.call(double(id: 50), :id, 50)).to eq true
|
4
|
-
expect(described_class.call(double(id: 50), :id, 51)).to eq false
|
5
|
-
|
6
|
-
expect(described_class.call(double(id: 50), :id, {"in" => (0..50), "not" => 49})).to eq true
|
7
|
-
expect(described_class.call(double(id: 49), :id, {"in" => (0..50), "not" => 49})).to eq false
|
8
|
-
end
|
9
|
-
end
|
@@ -1,7 +0,0 @@
|
|
1
|
-
describe Feature::Operation::In do
|
2
|
-
specify do
|
3
|
-
expect(described_class.call(double(id: 50), :id, {"range" => [40, 60]})).to eq true
|
4
|
-
expect(described_class.call(double(id: 50), :id, [1])).to eq false
|
5
|
-
expect(described_class.call(double(id: nil), :id, [1])).to eq false
|
6
|
-
end
|
7
|
-
end
|