toggles 0.1.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/toggles.svg)](https://badge.fury.io/rb/toggles)
|
4
|
+
[![CI](https://github.com/EasyPost/toggles/workflows/CI/badge.svg)](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
|