toggles 0.1.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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