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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +24 -0
  3. data/CHANGELOG.md +83 -0
  4. data/Gemfile +5 -0
  5. data/README.md +7 -10
  6. data/features/abbreviations_cn22.yml +3 -0
  7. data/features/file_s3.yml +3 -0
  8. data/features/nested_foo/bar_baz.yml +3 -0
  9. data/features/s3_file.yml +3 -0
  10. data/lib/toggles/constant_lookup.rb +50 -0
  11. data/lib/toggles/feature/attribute.rb +15 -0
  12. data/lib/toggles/feature/operation/and.rb +0 -15
  13. data/lib/toggles/feature/operation/in.rb +1 -1
  14. data/lib/toggles/feature/operation/range.rb +0 -2
  15. data/lib/toggles/feature/permissions.rb +34 -29
  16. data/lib/toggles/feature.rb +76 -16
  17. data/lib/toggles/version.rb +5 -0
  18. data/lib/toggles.rb +17 -55
  19. data/spec/spec_helper.rb +2 -0
  20. data/spec/toggles/feature/acceptance/type_spec.rb +19 -3
  21. data/spec/toggles/feature/attribute_spec.rb +9 -0
  22. data/spec/toggles/feature/operation_spec.rb +69 -0
  23. data/spec/toggles/feature/permissions_spec.rb +1 -1
  24. data/spec/toggles/feature_spec.rb +37 -0
  25. data/spec/toggles/init_spec.rb +9 -4
  26. data/toggles.gemspec +19 -16
  27. metadata +36 -87
  28. data/lib/toggles/feature/base.rb +0 -27
  29. data/lib/toggles/feature/operation/attribute.rb +0 -19
  30. data/lib/toggles/feature/operation/lt.rb +0 -9
  31. data/lib/toggles/feature/operation/not.rb +0 -15
  32. data/lib/toggles/feature/operation/or.rb +0 -15
  33. data/lib/toggles/feature/operation.rb +0 -8
  34. data/spec/toggles/feature/base_spec.rb +0 -10
  35. data/spec/toggles/feature/operation/and_spec.rb +0 -9
  36. data/spec/toggles/feature/operation/attribute_spec.rb +0 -9
  37. data/spec/toggles/feature/operation/gt_spec.rb +0 -6
  38. data/spec/toggles/feature/operation/in_spec.rb +0 -7
  39. data/spec/toggles/feature/operation/lt_spec.rb +0 -6
  40. data/spec/toggles/feature/operation/not_spec.rb +0 -6
  41. data/spec/toggles/feature/operation/or_spec.rb +0 -6
  42. data/spec/toggles/feature/operation/range_spec.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3fc507d16102054690297319ce1e47e61c1ef408
4
- data.tar.gz: 482eb06c1c41bd52afd536d0aad227e57ac7deab
2
+ SHA256:
3
+ metadata.gz: ece49487f49d27e3a10944d318e3b2c1b9a0e9a8a3c231581ebf5341e7abc4f2
4
+ data.tar.gz: d278a07f9e9e5e379936e02fa27e4a4cd7270ef2e7933181e5948c30d88bb4fd
5
5
  SHA512:
6
- metadata.gz: 97b6de15c5f8abff3dc5fac12dd1429163ebe7208a1b68c4666af82d130cb6aa2ca3a44c20a87e959ee6df3bf42964b046658a0355816d3b799aa1292bd340bd
7
- data.tar.gz: 85553a0544a5c6cb9f8929fe9f9c66d540451d6df8e2996af56af9bb3a16ea28ff9495f39dcb250e6396c8cf74bc48a30d18979a7e41c9ea9dc532cefc607321
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
@@ -1,3 +1,8 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ group :test do
6
+ gem "pry"
7
+ gem "pry-nav"
8
+ end
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Toggles
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/toggles.svg)](https://badge.fury.io/rb/toggles)
4
- [![CircleCI](https://circleci.com/gh/EasyPost/toggles.svg?style=svg)](https://circleci.com/gh/EasyPost/toggles)
4
+ [![CI](https://github.com/EasyPost/toggles/workflows/CI/badge.svg)](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::NewFeature::AvailableForPresentation.enabled_for?(user: OpenStruct.new(id: 12345)) # true
69
- Feature::NewFeature::AvailableForPresentation.enabled_for?(user: OpenStruct.new(id: 54321)) # true
70
- Feature::NewFeature::AvailableForPresentation.enabled_for?(user: OpenStruct.new(id: 7)) # false
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::NewFeature::AvailableForPresentation.disabled_for?(user: OpenStruct.new(id: 12345)) # false
73
- Feature::NewFeature::AvailableForPresentation.disabled_for?(user: OpenStruct.new(id: 54321)) # false
74
- Feature::NewFeature::AvailableForPresentation.disabled_for?(user: OpenStruct.new(id: 7)) # true
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,3 @@
1
+ ---
2
+ user:
3
+ id: 1
@@ -0,0 +1,3 @@
1
+ ---
2
+ user:
3
+ id: 1
@@ -0,0 +1,3 @@
1
+ id:
2
+ lt:
3
+ 5
@@ -0,0 +1,3 @@
1
+ ---
2
+ user:
3
+ id: 1
@@ -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
@@ -4,7 +4,7 @@ module Feature
4
4
  def self.call(entity, attr_name, expected)
5
5
  if expected.kind_of? Hash
6
6
  expected = expected.reduce([]) do |list, (operation, args)|
7
- OPERATIONS[operation.to_sym].call(args)
7
+ Feature.operations[operation.to_sym].call(args)
8
8
  end
9
9
  end
10
10
 
@@ -2,8 +2,6 @@ module Feature
2
2
  module Operation
3
3
  class Range
4
4
  def self.call(args)
5
- raise StandardError, "Invalid range operation" if args.size != 2
6
- (args.first..args.last)
7
5
  end
8
6
  end
9
7
  end
@@ -1,43 +1,48 @@
1
- require "ostruct"
2
- require "forwardable"
1
+ require 'ostruct'
2
+ require 'forwardable'
3
+ require 'yaml'
3
4
 
4
- module Feature
5
- class Permissions
6
- extend Forwardable
5
+ Feature::Permissions = Struct.new(:rules) do
6
+ extend Forwardable
7
7
 
8
- attr_reader :rules
8
+ def_delegators :rules, :all?, :keys
9
9
 
10
- def_delegators :rules, :all?, :keys
10
+ def self.from_yaml(path)
11
+ new(
12
+ YAML.safe_load(File.read(path), permitted_classes: [Symbol])
13
+ )
14
+ end
11
15
 
12
- def initialize(path)
13
- @rules = YAML.load(File.read(path))
14
- end
16
+ def subjects
17
+ @subjects ||= keys.map(&:to_sym)
18
+ end
15
19
 
16
- def subjects
17
- @subjects ||= keys.map(&:to_sym)
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
- def valid_for?(entities)
21
- unless subjects == entities.keys
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
- if entity.nil?
29
- return false
30
- end
28
+ return false if entity.nil?
31
29
 
32
- if entity.class.ancestors.find { |ancestor| ancestor == Comparable }
33
- entity = OpenStruct.new(name => entity)
34
- rule = {name => rule}
35
- end
30
+ if entity.class.ancestors.find { |ancestor| ancestor == Comparable }
31
+ entity = OpenStruct.new(name => entity)
32
+ rule = { name => rule }
33
+ end
36
34
 
37
- rule.all? do |key, value|
38
- OPERATIONS.fetch(key.to_sym, Operation::Attribute).call(entity, key, value)
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
@@ -1,24 +1,84 @@
1
- require "toggles/feature/base"
2
- require "toggles/feature/operation"
3
- require "toggles/feature/permissions"
4
- require "toggles/feature/subject"
5
-
1
+ # frozen_string_literal: true
2
+
6
3
  module Feature
7
- OPERATIONS = {and: Operation::And,
8
- gt: Operation::GreaterThan,
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
- @@tree = Module.new
7
+ def self.features
8
+ @features ||= {}
9
+ end
16
10
 
17
- def self.set_tree(tree)
18
- @@tree = tree
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
- @@tree.const_get(sym, inherit: false)
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
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toggles
4
+ VERSION = '0.4.0'
5
+ end
data/lib/toggles.rb CHANGED
@@ -1,8 +1,11 @@
1
- require "find"
2
- require "pathname"
1
+ require 'find'
2
+ require 'pathname'
3
+ require 'set'
3
4
 
4
- require "toggles/configuration"
5
- require "toggles/feature"
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.exists? configuration.features_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
- Find.find(top_level) do |path|
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
- previous.const_set(filename.split("_").map(&:capitalize).join.to_sym, cls)
72
- end
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.exists? configuration.features_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
@@ -3,6 +3,8 @@ require 'rspec/temp_dir'
3
3
 
4
4
  require "toggles"
5
5
 
6
+ Bundler.require(:test)
7
+
6
8
  RSpec.configure do |config|
7
9
  config.order = "random"
8
10
 
@@ -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
- expect(Feature::Type.enabled_for?(user_id: 1)).to eq true
4
- expect(Feature::Type.enabled_for?(user_id: 25)).to eq false
5
- expect(Feature::Type.enabled_for?(user_id: nil)).to eq false
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.new(path) }
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
@@ -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::Foo::Children.enabled_for?(id: 1) }.to raise_error(NameError)
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
- Dir.mkdir("#{temp_dir}/features")
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 = "toggles"
3
- s.version = "0.1.2"
4
- s.authors = ["Andrew Tribone", "James Brown"]
5
- s.summary = "YAML backed feature toggles"
6
- s.email = "oss@easypost.com"
7
- s.homepage = "https://github.com/EasyPost/toggles"
8
- s.license = "ISC"
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(/^(spec)\//)
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 "bundler"
19
- s.add_development_dependency "pry"
20
- s.add_development_dependency "pry-nav"
21
- s.add_development_dependency "pry-remote"
22
- s.add_development_dependency "rake"
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.1.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Tribone
8
8
  - James Brown
9
- autorequire:
9
+ - Josh Lane
10
+ autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2016-11-08 00:00:00.000000000 Z
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
- - .gitignore
137
- - .rspec
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/base.rb
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/base_spec.rb
172
- - spec/toggles/feature/operation/and_spec.rb
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
- rubyforge_project:
204
- rubygems_version: 2.0.14.1
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/base_spec.rb
218
- - spec/toggles/feature/operation/and_spec.rb
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
@@ -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,9 +0,0 @@
1
- module Feature
2
- module Operation
3
- class LessThan
4
- def self.call(entity, attr_name, expected)
5
- entity.send(attr_name) < expected
6
- end
7
- end
8
- end
9
- 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::And do
2
- specify do
3
- [[60, true], [40, false], [80, false]].each do |(id, expected)|
4
- expect(
5
- described_class.call(double(id: id), :id, {"gt" => 50, "lt" => 70})
6
- ).to eq expected
7
- end
8
- end
9
- 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,6 +0,0 @@
1
- describe Feature::Operation::GreaterThan do
2
- specify do
3
- expect(described_class.call(double(id: 50), :id, 40)).to eq true
4
- expect(described_class.call(double(id: 50), :id, 60)).to eq false
5
- end
6
- 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
@@ -1,6 +0,0 @@
1
- describe Feature::Operation::LessThan do
2
- specify do
3
- expect(described_class.call(double(id: 50), :id, 60)).to eq true
4
- expect(described_class.call(double(id: 50), :id, 40)).to eq false
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Feature::Operation::Not do
2
- specify do
3
- expect(described_class.call(double(id: 50), :id, {"in" => (10..20)})).to eq true
4
- expect(described_class.call(double(id: 50), :id, 50)).to eq false
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- describe Feature::Operation::Or do
2
- specify do
3
- expect(described_class.call(double(id: 16), :id, {"in" => (10..20), "not" => 15})).to eq true
4
- expect(described_class.call(double(id: 15), :id, {"in" => (10..20), "not" => 15})).to eq true
5
- end
6
- end
@@ -1,5 +0,0 @@
1
- describe Feature::Operation::Range do
2
- specify do
3
- expect(described_class.call([10, 20])).to eq (10..20)
4
- end
5
- end