toggles 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4c0bf3043c4b85c32062fa22750cc20c88af4cd4
4
- data.tar.gz: f8aeb5d080385e546815ccc57d8401e8abc1c9e4
3
+ metadata.gz: 3fc507d16102054690297319ce1e47e61c1ef408
4
+ data.tar.gz: 482eb06c1c41bd52afd536d0aad227e57ac7deab
5
5
  SHA512:
6
- metadata.gz: 2b7f5e471ba5456ee1ecc659f902b2826fecd2de532c1a4aef9b49e4791985df6cbdf73260e9643cd340711c3ab8f63c6daefb2691bcbc761fade903d3eacf93
7
- data.tar.gz: 08d1d0b300525bde3f0166b3fced8bc426b11f383eeb0ef4a6a60a1530e4e52f1e67522ea96472f2546a4f98d35ffece5642b3997569fb00777639626e9b14e0
6
+ metadata.gz: 97b6de15c5f8abff3dc5fac12dd1429163ebe7208a1b68c4666af82d130cb6aa2ca3a44c20a87e959ee6df3bf42964b046658a0355816d3b799aa1292bd340bd
7
+ data.tar.gz: 85553a0544a5c6cb9f8929fe9f9c66d540451d6df8e2996af56af9bb3a16ea28ff9495f39dcb250e6396c8cf74bc48a30d18979a7e41c9ea9dc532cefc607321
@@ -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.
@@ -0,0 +1,79 @@
1
+ # Toggles
2
+
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)
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
+ The classes `Feature::Test`, `Feature::Thing::One` and `Feature::Thing::Two` will be available for use within
45
+ your application.
46
+
47
+ You can call the `Toggles.init` method to force re-parsing the configuration and re-initializing all Features
48
+ structures at any time. The `Toggles.reinit_if_necessary` method is a convenience helper which will only
49
+ re-initialize of the top-level features directory has changed. Note that, in general, this will only detect
50
+ changes if you use a system where you swap out the entire features directory on changes and do not edit
51
+ individual files within the directory.
52
+
53
+ ## Usage
54
+
55
+ Create a file in `features_dir`:
56
+
57
+ ```yaml
58
+ user:
59
+ id:
60
+ in:
61
+ - 12345
62
+ - 54321
63
+ ```
64
+
65
+ Check if the feature is enabled or disabled:
66
+
67
+ ```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
71
+
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
75
+ ```
76
+
77
+ ## License
78
+
79
+ This project is licensed under the ISC License, the contents of which can be found at [LICENSE.txt](LICENSE.txt).
@@ -1,10 +1,16 @@
1
+ require "find"
2
+ require "pathname"
3
+
1
4
  require "toggles/configuration"
2
5
  require "toggles/feature"
3
6
 
4
7
  module Toggles
5
8
  extend self
6
9
 
10
+ StatResult = Struct.new(:inode, :mtime)
11
+
7
12
  def configure
13
+ @stat_tuple ||= StatResult.new(0, 0)
8
14
  yield configuration
9
15
  init
10
16
  end
@@ -30,11 +36,25 @@ module Toggles
30
36
  def init
31
37
  return unless Dir.exists? configuration.features_dir
32
38
 
33
- Find.find(configuration.features_dir) do |path|
39
+ new_tree = Module.new
40
+
41
+ top_level = File.realpath(configuration.features_dir)
42
+ top_level_p = Pathname.new(top_level)
43
+
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
34
48
  if path.match(/\.ya?ml\Z/)
35
- _, *directories, filename = path.chomp(File.extname(path)).split("/")
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
36
57
 
37
- previous = Feature
38
58
  directories.each do |directory|
39
59
  module_name = directory.split("_").map(&:capitalize).join.to_sym
40
60
  previous = if previous.constants.include? module_name
@@ -45,11 +65,30 @@ module Toggles
45
65
  end
46
66
 
47
67
  cls = Class.new(Feature::Base) do |c|
48
- c.const_set(:PERMISSIONS, Feature::Permissions.new(path))
68
+ c.const_set(:PERMISSIONS, Feature::Permissions.new(abspath))
49
69
  end
50
70
 
51
71
  previous.const_set(filename.split("_").map(&:capitalize).join.to_sym, cls)
52
72
  end
53
73
  end
74
+
75
+ stbuf = File.stat(top_level)
76
+ @stat_tuple = StatResult.new(stbuf.ino, stbuf.mtime)
77
+
78
+ Feature.set_tree(new_tree)
79
+ end
80
+
81
+ def reinit_if_changed
82
+ # Reload the configuration if the top-level directory has changed.
83
+ # Does not detect changes to files inside that directory unless your
84
+ # filesystem propagates mtimes.
85
+ return unless Dir.exists? configuration.features_dir
86
+ top_level = File.realpath(configuration.features_dir)
87
+ stbuf = File.stat(top_level)
88
+ stat_tuple = StatResult.new(stbuf.ino, stbuf.mtime)
89
+
90
+ if @stat_tuple != stat_tuple
91
+ init
92
+ end
54
93
  end
55
94
  end
@@ -1,5 +1,3 @@
1
- require "find"
2
-
3
1
  require "toggles/feature/base"
4
2
  require "toggles/feature/operation"
5
3
  require "toggles/feature/permissions"
@@ -13,4 +11,14 @@ module Feature
13
11
  not: Operation::Not,
14
12
  or: Operation::Or,
15
13
  range: Operation::Range}
14
+
15
+ @@tree = Module.new
16
+
17
+ def self.set_tree(tree)
18
+ @@tree = tree
19
+ end
20
+
21
+ def self.const_missing(sym)
22
+ @@tree.const_get(sym, inherit: false)
23
+ end
16
24
  end
@@ -1,11 +1,14 @@
1
1
  require "rspec/its"
2
+ require 'rspec/temp_dir'
2
3
 
3
4
  require "toggles"
4
5
 
5
- Toggles.configure do |config|
6
- config.features_dir = "features"
7
- end
8
-
9
6
  RSpec.configure do |config|
10
7
  config.order = "random"
8
+
9
+ config.before(:each) do
10
+ Toggles.configure do |c|
11
+ c.features_dir = "features"
12
+ end
13
+ end
11
14
  end
@@ -1,4 +1,4 @@
1
- describe Feature::Collection do
1
+ describe "Feature::Collection" do
2
2
  specify do
3
3
  expect(Feature::Collection.enabled_for?(user: double(id: 1))).to eq true
4
4
  expect(Feature::Collection.enabled_for?(user: double(id: 5))).to eq true
@@ -1,4 +1,4 @@
1
- describe Feature::ComplexAnd do
1
+ describe "Feature::ComplexAnd" do
2
2
  specify do
3
3
  expect(Feature::ComplexAnd.enabled_for?(data: double(id: 1))).to eq true
4
4
  expect(Feature::ComplexAnd.enabled_for?(data: double(id: 2, timestamp: 0))).to eq true
@@ -1,4 +1,4 @@
1
- describe Feature::MultipleSubjects do
1
+ describe "Feature::MultipleSubjects" do
2
2
  specify do
3
3
  expect(Feature::MultipleSubjects.enabled_for?(
4
4
  user: double(id: 1, logged_in?: true), widget: double(id: 2))).to eq true
@@ -1,4 +1,4 @@
1
- describe Feature::NestedAttributes do
1
+ describe "Feature::NestedAttributes" do
2
2
  specify do
3
3
  expect(Feature::NestedAttributes.enabled_for?(
4
4
  foo: double(bar: :two, baz: double(id: 51)))).to eq true
@@ -1,4 +1,4 @@
1
- describe Feature::OrAttributes do
1
+ describe "Feature::OrAttributes" do
2
2
  specify do
3
3
  expect(Feature::OrAttributes.enabled_for?(
4
4
  user: double(foo: 20, bar: 10))).to eq true
@@ -1,4 +1,4 @@
1
- describe Feature::Type do
1
+ describe "Feature::Type" do
2
2
  specify do
3
3
  expect(Feature::Type.enabled_for?(user_id: 1)).to eq true
4
4
  expect(Feature::Type.enabled_for?(user_id: 25)).to eq false
@@ -0,0 +1,84 @@
1
+ describe Toggles do
2
+ describe "#init" do
3
+ include_context "uses temp dir"
4
+
5
+ it "correctly loads configuration" do
6
+ Dir.mkdir("#{temp_dir}/foo")
7
+ Dir.mkdir("#{temp_dir}/bar")
8
+
9
+ File.open("#{temp_dir}/foo/users.yml", "w") do |f|
10
+ f.write("{\"id\": {\"in\": [1, 2]}}")
11
+ end
12
+
13
+ File.open("#{temp_dir}/bar/users.yml", "w") do |f|
14
+ f.write("{\"id\": {\"in\": [3, 4]}}")
15
+ end
16
+
17
+ Toggles.configure do |c|
18
+ c.features_dir = temp_dir
19
+ end
20
+
21
+ expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(true)
22
+ expect(Feature::Bar::Users.enabled_for?(id: 3)).to eq(true)
23
+ end
24
+
25
+ it "reloads configuration when #init is called" do
26
+ Dir.mkdir("#{temp_dir}/foo")
27
+
28
+ File.open("#{temp_dir}/foo/users.yml", "w") do |f|
29
+ f.write("{\"id\": {\"in\": [1, 2]}}")
30
+ end
31
+ File.open("#{temp_dir}/foo/children.yml", "w") do |f|
32
+ f.write("{\"id\": {\"in\": [1, 2]}}")
33
+ end
34
+
35
+
36
+ Toggles.configure do |c|
37
+ c.features_dir = temp_dir
38
+ end
39
+
40
+ expect(Feature::Foo::Users.enabled_for?(id: 1)).to eq(true)
41
+ expect(Feature::Foo::Children.enabled_for?(id: 1)).to eq(true)
42
+
43
+ File.open("#{temp_dir}/foo/users.yml", "w") do |f|
44
+ f.write("{\"id\": {\"in\": [2]}}")
45
+ end
46
+
47
+ File.unlink("#{temp_dir}/foo/children.yml")
48
+
49
+ Toggles.init
50
+
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)
53
+ end
54
+ end
55
+
56
+ describe "#reinit_if_changed" do
57
+ include_context "uses temp dir"
58
+
59
+ it "reloads when the contents have changed" do
60
+ Dir.mkdir("#{temp_dir}/features")
61
+
62
+ Toggles.configure do |c|
63
+ c.features_dir = "#{temp_dir}/features"
64
+ end
65
+
66
+ Dir.delete("#{temp_dir}/features")
67
+ Dir.mkdir("#{temp_dir}/features")
68
+
69
+ expect(Toggles).to receive("init")
70
+ Toggles.reinit_if_changed
71
+ end
72
+
73
+ it "does not reload when the contents have not changed" do
74
+ Dir.mkdir("#{temp_dir}/features")
75
+
76
+ Toggles.configure do |c|
77
+ c.features_dir = "#{temp_dir}/features"
78
+ end
79
+
80
+ expect(Toggles).not_to receive("init")
81
+ Toggles.reinit_if_changed
82
+ end
83
+ end
84
+ end
@@ -1,13 +1,19 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "toggles"
3
- s.version = "0.1.1"
4
- s.authors = ["Andrew Tribone"]
3
+ s.version = "0.1.2"
4
+ s.authors = ["Andrew Tribone", "James Brown"]
5
5
  s.summary = "YAML backed feature toggles"
6
- s.email = "tribone@easypost.com"
7
- s.homepage = "https://github.com/att14/toggles"
8
- s.license = ""
6
+ s.email = "oss@easypost.com"
7
+ s.homepage = "https://github.com/EasyPost/toggles"
8
+ s.license = "ISC"
9
9
  s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
10
10
  s.test_files = s.files.grep(/^(spec)\//)
11
+ s.description = <<-EOF
12
+ YAML-backed implementation of the feature flags pattern. Build a
13
+ hierarchy of features in YAML files in the filesystem, apply various
14
+ conditions using boolean logic and a selection of filters, and easily
15
+ check whether a given feature should be applied.
16
+ EOF
11
17
 
12
18
  s.add_development_dependency "bundler"
13
19
  s.add_development_dependency "pry"
@@ -16,4 +22,5 @@ Gem::Specification.new do |s|
16
22
  s.add_development_dependency "rake"
17
23
  s.add_development_dependency "rspec"
18
24
  s.add_development_dependency "rspec-its"
25
+ s.add_development_dependency "rspec-temp_dir"
19
26
  end
metadata CHANGED
@@ -1,122 +1,143 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toggles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Tribone
8
+ - James Brown
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2015-08-25 00:00:00.000000000 Z
12
+ date: 2016-11-08 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: bundler
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
- - - ">="
18
+ - - '>='
18
19
  - !ruby/object:Gem::Version
19
20
  version: '0'
20
21
  type: :development
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
- - - ">="
25
+ - - '>='
25
26
  - !ruby/object:Gem::Version
26
27
  version: '0'
27
28
  - !ruby/object:Gem::Dependency
28
29
  name: pry
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
- - - ">="
32
+ - - '>='
32
33
  - !ruby/object:Gem::Version
33
34
  version: '0'
34
35
  type: :development
35
36
  prerelease: false
36
37
  version_requirements: !ruby/object:Gem::Requirement
37
38
  requirements:
38
- - - ">="
39
+ - - '>='
39
40
  - !ruby/object:Gem::Version
40
41
  version: '0'
41
42
  - !ruby/object:Gem::Dependency
42
43
  name: pry-nav
43
44
  requirement: !ruby/object:Gem::Requirement
44
45
  requirements:
45
- - - ">="
46
+ - - '>='
46
47
  - !ruby/object:Gem::Version
47
48
  version: '0'
48
49
  type: :development
49
50
  prerelease: false
50
51
  version_requirements: !ruby/object:Gem::Requirement
51
52
  requirements:
52
- - - ">="
53
+ - - '>='
53
54
  - !ruby/object:Gem::Version
54
55
  version: '0'
55
56
  - !ruby/object:Gem::Dependency
56
57
  name: pry-remote
57
58
  requirement: !ruby/object:Gem::Requirement
58
59
  requirements:
59
- - - ">="
60
+ - - '>='
60
61
  - !ruby/object:Gem::Version
61
62
  version: '0'
62
63
  type: :development
63
64
  prerelease: false
64
65
  version_requirements: !ruby/object:Gem::Requirement
65
66
  requirements:
66
- - - ">="
67
+ - - '>='
67
68
  - !ruby/object:Gem::Version
68
69
  version: '0'
69
70
  - !ruby/object:Gem::Dependency
70
71
  name: rake
71
72
  requirement: !ruby/object:Gem::Requirement
72
73
  requirements:
73
- - - ">="
74
+ - - '>='
74
75
  - !ruby/object:Gem::Version
75
76
  version: '0'
76
77
  type: :development
77
78
  prerelease: false
78
79
  version_requirements: !ruby/object:Gem::Requirement
79
80
  requirements:
80
- - - ">="
81
+ - - '>='
81
82
  - !ruby/object:Gem::Version
82
83
  version: '0'
83
84
  - !ruby/object:Gem::Dependency
84
85
  name: rspec
85
86
  requirement: !ruby/object:Gem::Requirement
86
87
  requirements:
87
- - - ">="
88
+ - - '>='
88
89
  - !ruby/object:Gem::Version
89
90
  version: '0'
90
91
  type: :development
91
92
  prerelease: false
92
93
  version_requirements: !ruby/object:Gem::Requirement
93
94
  requirements:
94
- - - ">="
95
+ - - '>='
95
96
  - !ruby/object:Gem::Version
96
97
  version: '0'
97
98
  - !ruby/object:Gem::Dependency
98
99
  name: rspec-its
99
100
  requirement: !ruby/object:Gem::Requirement
100
101
  requirements:
101
- - - ">="
102
+ - - '>='
102
103
  - !ruby/object:Gem::Version
103
104
  version: '0'
104
105
  type: :development
105
106
  prerelease: false
106
107
  version_requirements: !ruby/object:Gem::Requirement
107
108
  requirements:
108
- - - ">="
109
+ - - '>='
109
110
  - !ruby/object:Gem::Version
110
111
  version: '0'
111
- description:
112
- email: tribone@easypost.com
112
+ - !ruby/object:Gem::Dependency
113
+ name: rspec-temp_dir
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: |2
127
+ YAML-backed implementation of the feature flags pattern. Build a
128
+ hierarchy of features in YAML files in the filesystem, apply various
129
+ conditions using boolean logic and a selection of filters, and easily
130
+ check whether a given feature should be applied.
131
+ email: oss@easypost.com
113
132
  executables: []
114
133
  extensions: []
115
134
  extra_rdoc_files: []
116
135
  files:
117
- - ".gitignore"
118
- - ".rspec"
136
+ - .gitignore
137
+ - .rspec
119
138
  - Gemfile
139
+ - LICENSE.txt
140
+ - README.md
120
141
  - Rakefile
121
142
  - features/collection.yml
122
143
  - features/complex_and.yml
@@ -158,10 +179,11 @@ files:
158
179
  - spec/toggles/feature/operation/range_spec.rb
159
180
  - spec/toggles/feature/permissions_spec.rb
160
181
  - spec/toggles/feature/subject_spec.rb
182
+ - spec/toggles/init_spec.rb
161
183
  - toggles.gemspec
162
- homepage: https://github.com/att14/toggles
184
+ homepage: https://github.com/EasyPost/toggles
163
185
  licenses:
164
- - ''
186
+ - ISC
165
187
  metadata: {}
166
188
  post_install_message:
167
189
  rdoc_options: []
@@ -169,17 +191,17 @@ require_paths:
169
191
  - lib
170
192
  required_ruby_version: !ruby/object:Gem::Requirement
171
193
  requirements:
172
- - - ">="
194
+ - - '>='
173
195
  - !ruby/object:Gem::Version
174
196
  version: '0'
175
197
  required_rubygems_version: !ruby/object:Gem::Requirement
176
198
  requirements:
177
- - - ">="
199
+ - - '>='
178
200
  - !ruby/object:Gem::Version
179
201
  version: '0'
180
202
  requirements: []
181
203
  rubyforge_project:
182
- rubygems_version: 2.2.2
204
+ rubygems_version: 2.0.14.1
183
205
  signing_key:
184
206
  specification_version: 4
185
207
  summary: YAML backed feature toggles
@@ -203,3 +225,4 @@ test_files:
203
225
  - spec/toggles/feature/operation/range_spec.rb
204
226
  - spec/toggles/feature/permissions_spec.rb
205
227
  - spec/toggles/feature/subject_spec.rb
228
+ - spec/toggles/init_spec.rb