toggles 0.1.1 → 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/LICENSE.txt +13 -0
- data/README.md +76 -0
- 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 +80 -12
- data/lib/toggles/version.rb +5 -0
- data/lib/toggles.rb +39 -38
- data/spec/spec_helper.rb +8 -3
- data/spec/toggles/feature/acceptance/collection_spec.rb +1 -1
- data/spec/toggles/feature/acceptance/complex_and_spec.rb +1 -1
- data/spec/toggles/feature/acceptance/multiple_subjects_spec.rb +1 -1
- data/spec/toggles/feature/acceptance/nested_attributes_spec.rb +1 -1
- data/spec/toggles/feature/acceptance/or_attributes_spec.rb +1 -1
- data/spec/toggles/feature/acceptance/type_spec.rb +20 -4
- 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 +89 -0
- data/toggles.gemspec +25 -15
- metadata +39 -67
- 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/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2016-2016, Simpler Postage, Inc. <oss@easypost.com>
|
2
|
+
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
4
|
+
purpose with or without fee is hereby granted, provided that the above
|
5
|
+
copyright notice and this permission notice appear in all copies.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
8
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
9
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
10
|
+
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
11
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
12
|
+
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
13
|
+
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Toggles
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/toggles)
|
4
|
+
[](https://github.com/EasyPost/toggles/actions?query=workflow%3ACI)
|
5
|
+
|
6
|
+
YAML backed feature toggles
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add the following to your Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem "toggles"
|
14
|
+
```
|
15
|
+
|
16
|
+
and run `bundle install` from your shell.
|
17
|
+
|
18
|
+
To install the gem manually from your shell, run:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem install toggles
|
22
|
+
```
|
23
|
+
|
24
|
+
## Configuration
|
25
|
+
|
26
|
+
Configure `toggles`:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
Toggles.configure do |config|
|
30
|
+
config.features_dir = "features"
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
You can now express conditional logic within `features_dir`. The structure of the `features_dir` determines the structure of the classes within the `Feature` module. For example if the `features_dir` has the structure:
|
35
|
+
|
36
|
+
```
|
37
|
+
features
|
38
|
+
├── thing
|
39
|
+
| ├── one.yml
|
40
|
+
| └── two.yml
|
41
|
+
└── test.yml
|
42
|
+
```
|
43
|
+
|
44
|
+
You can call the `Toggles.init` method to force re-parsing the configuration and re-initializing all Features
|
45
|
+
structures at any time. The `Toggles.reinit_if_necessary` method is a convenience helper which will only
|
46
|
+
re-initialize of the top-level features directory has changed. Note that, in general, this will only detect
|
47
|
+
changes if you use a system where you swap out the entire features directory on changes and do not edit
|
48
|
+
individual files within the directory.
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
Create a file in `features_dir`:
|
53
|
+
|
54
|
+
```yaml
|
55
|
+
user:
|
56
|
+
id:
|
57
|
+
in:
|
58
|
+
- 12345
|
59
|
+
- 54321
|
60
|
+
```
|
61
|
+
|
62
|
+
Check if the feature is enabled or disabled:
|
63
|
+
|
64
|
+
```ruby
|
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
|
68
|
+
|
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
|
72
|
+
```
|
73
|
+
|
74
|
+
## License
|
75
|
+
|
76
|
+
This project is licensed under the ISC License, the contents of which can be found at [LICENSE.txt](LICENSE.txt).
|
@@ -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,16 +1,84 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feature
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
Unknown = Class.new(Error)
|
6
|
+
|
7
|
+
def self.features
|
8
|
+
@features ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.operations
|
12
|
+
@operations ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# @deprecated This is an abuse of lazy dispatch that creates cryptic errors
|
16
|
+
def self.const_missing(sym)
|
17
|
+
ConstantLookup.from(features, [:Feature]).const_missing(sym)
|
18
|
+
end
|
2
19
|
|
3
|
-
|
4
|
-
|
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)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'toggles/constant_lookup'
|
34
|
+
require "toggles/feature/attribute"
|
5
35
|
require "toggles/feature/permissions"
|
6
36
|
require "toggles/feature/subject"
|
7
37
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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,10 +1,19 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'find'
|
2
|
+
require 'pathname'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'toggles/version'
|
6
|
+
|
7
|
+
require 'toggles/configuration'
|
8
|
+
require 'toggles/feature'
|
3
9
|
|
4
10
|
module Toggles
|
5
11
|
extend self
|
6
12
|
|
13
|
+
StatResult = Struct.new(:inode, :mtime)
|
14
|
+
|
7
15
|
def configure
|
16
|
+
@stat_tuple ||= StatResult.new(0, 0)
|
8
17
|
yield configuration
|
9
18
|
init
|
10
19
|
end
|
@@ -13,43 +22,35 @@ module Toggles
|
|
13
22
|
@configuration ||= Configuration.new
|
14
23
|
end
|
15
24
|
|
16
|
-
# Dynamically create modules and classes within the `Feature` module based on
|
17
|
-
# the directory structure of `features`.
|
18
|
-
#
|
19
|
-
# For example if the `features` directory has the structure:
|
20
|
-
#
|
21
|
-
# features
|
22
|
-
# ├── thing
|
23
|
-
# | ├── one.yml
|
24
|
-
# | └── two.yml
|
25
|
-
# └── test.yml
|
26
|
-
#
|
27
|
-
# `Feature::Test`, `Feature::Thing::One`, `Feature::Thing::Two` would be
|
28
|
-
# available by default.
|
29
|
-
#
|
30
25
|
def init
|
31
|
-
return unless Dir.
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
previous.const_set(module_name, Module.new)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
cls = Class.new(Feature::Base) do |c|
|
48
|
-
c.const_set(:PERMISSIONS, Feature::Permissions.new(path))
|
49
|
-
end
|
50
|
-
|
51
|
-
previous.const_set(filename.split("_").map(&:capitalize).join.to_sym, cls)
|
52
|
-
end
|
26
|
+
return unless Dir.exist? configuration.features_dir
|
27
|
+
|
28
|
+
top_level = File.realpath(configuration.features_dir)
|
29
|
+
top_level_p = Pathname.new(top_level)
|
30
|
+
|
31
|
+
Feature.features.clear
|
32
|
+
|
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)
|
53
38
|
end
|
39
|
+
|
40
|
+
stbuf = File.stat(top_level)
|
41
|
+
@stat_tuple = StatResult.new(stbuf.ino, stbuf.mtime)
|
42
|
+
end
|
43
|
+
|
44
|
+
def reinit_if_changed
|
45
|
+
# Reload the configuration if the top-level directory has changed.
|
46
|
+
# Does not detect changes to files inside that directory unless your
|
47
|
+
# filesystem propagates mtimes.
|
48
|
+
return unless Dir.exist? configuration.features_dir
|
49
|
+
|
50
|
+
top_level = File.realpath(configuration.features_dir)
|
51
|
+
stbuf = File.stat(top_level)
|
52
|
+
stat_tuple = StatResult.new(stbuf.ino, stbuf.mtime)
|
53
|
+
|
54
|
+
init if @stat_tuple != stat_tuple
|
54
55
|
end
|
55
56
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
require "rspec/its"
|
2
|
+
require 'rspec/temp_dir'
|
2
3
|
|
3
4
|
require "toggles"
|
4
5
|
|
5
|
-
|
6
|
-
config.features_dir = "features"
|
7
|
-
end
|
6
|
+
Bundler.require(:test)
|
8
7
|
|
9
8
|
RSpec.configure do |config|
|
10
9
|
config.order = "random"
|
10
|
+
|
11
|
+
config.before(:each) do
|
12
|
+
Toggles.configure do |c|
|
13
|
+
c.features_dir = "features"
|
14
|
+
end
|
15
|
+
end
|
11
16
|
end
|