eight_ball 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +72 -0
- data/LICENSE +21 -0
- data/README.md +105 -0
- data/Rakefile +10 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/eight_ball.gemspec +36 -0
- data/examples/http_provider.rb +8 -0
- data/examples/static_provider.rb +13 -0
- data/lib/eight_ball/conditions/always.rb +10 -0
- data/lib/eight_ball/conditions/base.rb +22 -0
- data/lib/eight_ball/conditions/conditions.rb +16 -0
- data/lib/eight_ball/conditions/list.rb +35 -0
- data/lib/eight_ball/conditions/never.rb +10 -0
- data/lib/eight_ball/conditions/range.rb +48 -0
- data/lib/eight_ball/feature.rb +64 -0
- data/lib/eight_ball/parsers/json.rb +72 -0
- data/lib/eight_ball/providers/http.rb +57 -0
- data/lib/eight_ball/providers/refresh_policies/interval.rb +40 -0
- data/lib/eight_ball/providers/static.rb +23 -0
- data/lib/eight_ball/version.rb +5 -0
- data/lib/eight_ball.rb +97 -0
- metadata +211 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4e72251f479d9b7709d48d560535bb28b72692a7893e2de3d196967eb4915509
|
|
4
|
+
data.tar.gz: 72721d1bb8447908b7241fa1545fe4d59073e2a951982f231de1e32bf11c6d85
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bc07aa302ce37b33da32b8943c4dfae7b7db392d4e028f148500ff95a667999c07cdf0fcd0bd09676e6349f9f20ebc1457d515b72e1dca27c72318e4fb1bc929
|
|
7
|
+
data.tar.gz: e34699163e898d44ad6d7d832470785b9208383b08a089f5badec06b16bd55929d5bc1410ec1748921852ed1c465b84ad60b175ba66a26014a6b13829deb6657
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Each line is a file pattern followed by one or more owners.
|
|
2
|
+
|
|
3
|
+
# These owners will be the default owners for everything in
|
|
4
|
+
# the repo. Unless a later match takes precedence, members of
|
|
5
|
+
# @rewindio/codeowners will be requested for review when someone
|
|
6
|
+
# opens a pull request.
|
|
7
|
+
* @MarcMorinville-Rewind @jhuff078 @smandegar
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
sudo: false
|
|
2
|
+
language: ruby
|
|
3
|
+
cache: bundler
|
|
4
|
+
rvm:
|
|
5
|
+
- 2.5.0
|
|
6
|
+
before_install: gem install bundler -v 1.17.1
|
|
7
|
+
deploy:
|
|
8
|
+
provider: rubygems
|
|
9
|
+
api_key:
|
|
10
|
+
secure: t7jNoz9Ct19jfGikW2lxK39aNPbvbFTwkhvVQ4ZlSAn+0iUWSmcTDFnh6MiSJFi9PEGvH3AUIrs6s7p6vhWUB8ll3S1UpC+34izUQZjUUHssJ+tf+A9q27OtX/O9PUsoOkHiq8/3F8SRT1OqyKb9+DR6i/M9MpHgWKLVEmXq3MXnGin1TeNJ70Hy2hakwbH/LdprrfT0LUohLZYzVFEodBPXRWeplJSAnJlyTrSIfIZV9e716L0F3HbXYY6pE5PJkFUSWwzAyO0Z73PJVV5+VsHt10AbHIJzPqf/qsLtC4K4SICWO7Xtnyuu9erg0HjG+y2D3AHbMklP/vJk4y1J2m6aJEzJ4VUVYfbVbkomPV0ol6alPmd6rCth8FQSZ5IFulwfp3qcsvSxS1BDhKl1yDC1di6n5MH9YNndy6puy6VTuuz4lqCya4WA1aBeummb9WsH2CNIws+rLneoH7PsMz6DCx9YhxbfZkh8h0WXRUgl86Ir1RbKbhR4+b3P1MFart55ipJ/tWMtyWOdJvjgakEyR/QMUEkWFMCOHuQd+Iv3PGT5God9YV6L01X/26BIfiHcTZTi2f5RDSyy6vIaOMROLKKU+heK3CGWvemA24uuJbyH9b3QujypCrR7wLRYvfGHeHunFbFH4HHZj+aedvtxzyJ004kNsia2y1TSw6c=
|
|
11
|
+
gem: eight_ball
|
|
12
|
+
on:
|
|
13
|
+
tags: true
|
|
14
|
+
repo: rewindio/eight_ball
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
eight_ball (1.0.0)
|
|
5
|
+
plissken (~> 1.2)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
ansi (1.5.0)
|
|
11
|
+
builder (3.2.3)
|
|
12
|
+
byebug (10.0.2)
|
|
13
|
+
coderay (1.1.2)
|
|
14
|
+
docile (1.3.1)
|
|
15
|
+
hirb (0.7.3)
|
|
16
|
+
inch (0.8.0)
|
|
17
|
+
pry
|
|
18
|
+
sparkr (>= 0.2.0)
|
|
19
|
+
term-ansicolor
|
|
20
|
+
yard (~> 0.9.12)
|
|
21
|
+
json (2.1.0)
|
|
22
|
+
metaclass (0.0.4)
|
|
23
|
+
method_source (0.9.2)
|
|
24
|
+
minitest (5.11.3)
|
|
25
|
+
minitest-reporters (1.3.5)
|
|
26
|
+
ansi
|
|
27
|
+
builder
|
|
28
|
+
minitest (>= 5.0)
|
|
29
|
+
ruby-progressbar
|
|
30
|
+
mocha (1.7.0)
|
|
31
|
+
metaclass (~> 0.0.1)
|
|
32
|
+
plissken (1.2.0)
|
|
33
|
+
pry (0.12.2)
|
|
34
|
+
coderay (~> 1.1.0)
|
|
35
|
+
method_source (~> 0.9.0)
|
|
36
|
+
pry-byebug (3.6.0)
|
|
37
|
+
byebug (~> 10.0)
|
|
38
|
+
pry (~> 0.10)
|
|
39
|
+
rake (10.5.0)
|
|
40
|
+
ruby-progressbar (1.10.0)
|
|
41
|
+
simplecov (0.16.1)
|
|
42
|
+
docile (~> 1.1)
|
|
43
|
+
json (>= 1.8, < 3)
|
|
44
|
+
simplecov-html (~> 0.10.0)
|
|
45
|
+
simplecov-console (0.4.2)
|
|
46
|
+
ansi
|
|
47
|
+
hirb
|
|
48
|
+
simplecov
|
|
49
|
+
simplecov-html (0.10.2)
|
|
50
|
+
sparkr (0.4.1)
|
|
51
|
+
term-ansicolor (1.7.0)
|
|
52
|
+
tins (~> 1.0)
|
|
53
|
+
tins (1.20.2)
|
|
54
|
+
yard (0.9.16)
|
|
55
|
+
|
|
56
|
+
PLATFORMS
|
|
57
|
+
ruby
|
|
58
|
+
|
|
59
|
+
DEPENDENCIES
|
|
60
|
+
bundler (~> 1.17)
|
|
61
|
+
eight_ball!
|
|
62
|
+
inch (~> 0.8)
|
|
63
|
+
minitest (~> 5.0)
|
|
64
|
+
minitest-reporters (~> 1.3)
|
|
65
|
+
mocha (~> 1.7)
|
|
66
|
+
pry-byebug (~> 3.6)
|
|
67
|
+
rake (~> 10.0)
|
|
68
|
+
simplecov (~> 0.16)
|
|
69
|
+
simplecov-console (~> 0.4)
|
|
70
|
+
|
|
71
|
+
BUNDLED WITH
|
|
72
|
+
1.17.1
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Third Blink Software Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# EightBall
|
|
2
|
+
[](https://travis-ci.com/rewindio/eight_ball)
|
|
3
|
+
|
|
4
|
+
EightBall is a feature toggle querying gem
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Add this line to your application's Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'eight_ball'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
|
|
16
|
+
$ bundle
|
|
17
|
+
|
|
18
|
+
Or install it yourself as:
|
|
19
|
+
|
|
20
|
+
$ gem install eight_ball
|
|
21
|
+
|
|
22
|
+
## Example Usage
|
|
23
|
+
```
|
|
24
|
+
require 'eight_ball'
|
|
25
|
+
|
|
26
|
+
# This could be read from the filesystem or be the response from an external service, etc.
|
|
27
|
+
json_input = %(
|
|
28
|
+
[{
|
|
29
|
+
"name": "Feature1",
|
|
30
|
+
"enabledFor": [{
|
|
31
|
+
"type": "range",
|
|
32
|
+
"parameter": "accountId",
|
|
33
|
+
"min": 1,
|
|
34
|
+
"max": 10
|
|
35
|
+
}],
|
|
36
|
+
"disabledFor": [{
|
|
37
|
+
"type": "list",
|
|
38
|
+
"parameter": "accountId",
|
|
39
|
+
"values": [2, 3]
|
|
40
|
+
}]
|
|
41
|
+
}]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Transform the JSON into a list of Features
|
|
45
|
+
parser = EightBall::Parsers::Json.new
|
|
46
|
+
features = parser.parse json_input
|
|
47
|
+
|
|
48
|
+
# Tell EightBall about these Features
|
|
49
|
+
EightBall.provider = EightBall::Providers::Static.new features
|
|
50
|
+
|
|
51
|
+
# Away you go
|
|
52
|
+
EightBall.enabled? "Feature1", { accountId: 4 } # true
|
|
53
|
+
EightBall.enabled? "Feature1", { accountId: 2 } # false
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
More examples [here](examples)
|
|
57
|
+
|
|
58
|
+
## Concepts
|
|
59
|
+
|
|
60
|
+
### Feature
|
|
61
|
+
A Feature is a part of your application that can be enabled or disabled based on various conditions. It has the following attributes:
|
|
62
|
+
- `name`: The unique name of the Feature.
|
|
63
|
+
- `enabledFor`: An array of Conditions for which the Feature is enabled.
|
|
64
|
+
- `disabledFor`: An array of Conditions for which the Feature is disabled.
|
|
65
|
+
|
|
66
|
+
### Condition
|
|
67
|
+
A Condition must either be `true` or `false`. It describes when a Feature is enabled or disabled.
|
|
68
|
+
|
|
69
|
+
**Supported Conditions**
|
|
70
|
+
- [Always](lib/eight_ball/conditions/always.rb): This condition is always satisfied.
|
|
71
|
+
- [List](lib/eight_ball/conditions/list.rb): This condition is satisfied if the given value belongs to its list of accepted values.
|
|
72
|
+
- [Never](lib/eight_ball/conditions/never.rb): This condition is never satisfied.
|
|
73
|
+
- [Range](lib/eight_ball/conditions/range.rb): This condition is satisfied if the given value is within the specified range (inclusive).
|
|
74
|
+
|
|
75
|
+
### Provider
|
|
76
|
+
A Provider is able to give EightBall the list of Features it needs to answer queries.
|
|
77
|
+
|
|
78
|
+
**Supported Providers**
|
|
79
|
+
- [HTTP](lib/eight_ball/providers/http.rb): Connect to a URL and use the given Parser to convert the response into a list of Features.
|
|
80
|
+
- [Static](lib/eight_ball/providers/static.rb): Once initialized with a list of Features, always provides that same list of Features.
|
|
81
|
+
|
|
82
|
+
#### RefreshPolicies
|
|
83
|
+
Some Providers are able to automatically "refresh" their list of Features using a RefreshPolicy.
|
|
84
|
+
|
|
85
|
+
**Supported RefreshPolicies**
|
|
86
|
+
- [Interval](lib/eight_ball/providers/refresh_policies/interval.rb): The data is considered fresh for a given number of seconds, after which it is considered stale and should be refreshed.
|
|
87
|
+
|
|
88
|
+
### Parser
|
|
89
|
+
A Parser converts the given input to an array of Features.
|
|
90
|
+
|
|
91
|
+
**Supported Parsers**
|
|
92
|
+
- [JSON](lib/eight_ball/parsers/json.rb)
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
97
|
+
|
|
98
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
99
|
+
|
|
100
|
+
### Documenting
|
|
101
|
+
Documentation is written using [yard](https://yardoc.org/) syntax. You can view the generated docs by running `yard server` and going to `http://127.0.0.1:8808/docs/EightBall`
|
|
102
|
+
|
|
103
|
+
## Contributing
|
|
104
|
+
|
|
105
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rewindio/eight_ball.
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'eight_ball'
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
require 'pry-byebug'
|
|
10
|
+
Pry.start
|
data/bin/setup
ADDED
data/eight_ball.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'eight_ball/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'eight_ball'
|
|
9
|
+
spec.version = EightBall::VERSION
|
|
10
|
+
spec.authors = ['Rewind.io']
|
|
11
|
+
spec.email = ['team@rewind.io']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'The most cost efficient way to flag features'
|
|
14
|
+
spec.description = 'Ask questions about flagged features'
|
|
15
|
+
spec.homepage = 'https://github.com/rewindio/eight_ball'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
|
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
19
|
+
spec.bindir = 'exe'
|
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
|
+
spec.require_paths = ['lib']
|
|
22
|
+
|
|
23
|
+
### DEPENDENCIES
|
|
24
|
+
spec.add_dependency 'plissken', '~> 1.2'
|
|
25
|
+
|
|
26
|
+
# Development
|
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.17'
|
|
28
|
+
spec.add_development_dependency 'inch', '~> 0.8'
|
|
29
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
|
30
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.3'
|
|
31
|
+
spec.add_development_dependency 'mocha', '~> 1.7'
|
|
32
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.6'
|
|
33
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
|
34
|
+
spec.add_development_dependency 'simplecov', '~> 0.16'
|
|
35
|
+
spec.add_development_dependency 'simplecov-console', '~> 0.4'
|
|
36
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'eight_ball'
|
|
2
|
+
|
|
3
|
+
# Create a Feature programatically
|
|
4
|
+
enabled_for = EightBall::Conditions::Range.new min: 1, max: 10, parameter: 'account_id'
|
|
5
|
+
disabled_for = EightBall::Conditions::List.new values: [2, 3]
|
|
6
|
+
feature = EightBall::Feature.new 'Feature1', enabled_for, disabled_for
|
|
7
|
+
|
|
8
|
+
# Tell EightBall about the Features
|
|
9
|
+
EightBall.provider = EightBall::Providers::Static.new feature
|
|
10
|
+
|
|
11
|
+
# Away you go
|
|
12
|
+
EightBall.enabled? 'Feature1', account_id: 4 # true
|
|
13
|
+
EightBall.enabled? 'Feature1', account_id: 2 # false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EightBall::Conditions
|
|
4
|
+
class Base
|
|
5
|
+
attr_reader :parameter
|
|
6
|
+
|
|
7
|
+
def initialize(options = [])
|
|
8
|
+
@parameter = nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def satisfied?
|
|
12
|
+
raise 'You can never satisfy the Base condition'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
protected
|
|
16
|
+
|
|
17
|
+
def parameter=(parameter)
|
|
18
|
+
return if parameter.nil?
|
|
19
|
+
@parameter = parameter.gsub(/(.)([A-Z])/,'\1_\2').downcase
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EightBall::Conditions
|
|
4
|
+
# Finds the Condition class based on its name
|
|
5
|
+
# @param [String] name The case insensitive name to find the Condition for
|
|
6
|
+
# @return [EightBall::Conditions] the Condition class represented by the given name
|
|
7
|
+
def self.by_name(name)
|
|
8
|
+
mappings = {
|
|
9
|
+
always: EightBall::Conditions::Always,
|
|
10
|
+
list: EightBall::Conditions::List,
|
|
11
|
+
never: EightBall::Conditions::Never,
|
|
12
|
+
range: EightBall::Conditions::Range
|
|
13
|
+
}
|
|
14
|
+
mappings[name.downcase.to_sym]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EightBall::Conditions
|
|
4
|
+
# The List Condition describes a list of acceptable values.
|
|
5
|
+
# These can be strings, integers, etc.
|
|
6
|
+
class List < Base
|
|
7
|
+
attr_reader :values
|
|
8
|
+
|
|
9
|
+
# Creates a new instance of a List Condition.
|
|
10
|
+
#
|
|
11
|
+
# @param [Hash] options
|
|
12
|
+
#
|
|
13
|
+
# @option options [Array<String>, String] :values
|
|
14
|
+
# The list of acceptable values
|
|
15
|
+
# @option options [String] :parameter
|
|
16
|
+
# The name of the parameter this Condition was created for (eg. "account_id").
|
|
17
|
+
# This value is only used by calling classes as a way to know what to pass
|
|
18
|
+
# into {satisfied?}.
|
|
19
|
+
def initialize(options = {})
|
|
20
|
+
options ||= {}
|
|
21
|
+
|
|
22
|
+
@values = Array(options[:values])
|
|
23
|
+
self.parameter = options[:parameter]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @example
|
|
27
|
+
# condition = new EightBall::Conditions::List.new [1, 'a']
|
|
28
|
+
# condition.satisfied? 1 => true
|
|
29
|
+
# condition.satisfied? 2 => false
|
|
30
|
+
# condition.satisfied? 'a' => true
|
|
31
|
+
def satisfied?(value)
|
|
32
|
+
values.include? value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EightBall::Conditions
|
|
4
|
+
# The Range Condition describes a range of acceptable values by specifying
|
|
5
|
+
# a minimum and a maximum. These can be strings, integers, etc. but mixing
|
|
6
|
+
# data types will probably give you unexpected results.
|
|
7
|
+
class Range < Base
|
|
8
|
+
attr_reader :min, :max
|
|
9
|
+
|
|
10
|
+
# Creates a new instance of a Range Condition.
|
|
11
|
+
#
|
|
12
|
+
# @param [Hash] options
|
|
13
|
+
#
|
|
14
|
+
# @option options :min The minimum acceptable value (inclusive).
|
|
15
|
+
# @option options :max The maximum acceptable value (inclusive).
|
|
16
|
+
# @option options [String] :parameter
|
|
17
|
+
# The name of the parameter this Condition was created for (eg. "account_id").
|
|
18
|
+
# This value is only used by calling classes as a way to know what to pass
|
|
19
|
+
# into {satisfied?}.
|
|
20
|
+
def initialize(options = {})
|
|
21
|
+
options ||= {}
|
|
22
|
+
|
|
23
|
+
raise ArgumentError, 'Missing value for min' if options[:min].nil?
|
|
24
|
+
raise ArgumentError, 'Missing value for max' if options[:max].nil?
|
|
25
|
+
|
|
26
|
+
@min = options[:min]
|
|
27
|
+
|
|
28
|
+
raise ArgumentError, 'Max must be greater or equal to min' if options[:max] < min
|
|
29
|
+
|
|
30
|
+
@max = options[:max]
|
|
31
|
+
|
|
32
|
+
self.parameter = options[:parameter]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @example Using integers
|
|
36
|
+
# condition = new EightBall::Conditions::List.new min: 1, max: 300
|
|
37
|
+
# condition.satisfied? 1 => true
|
|
38
|
+
# condition.satisfied? 301 => false
|
|
39
|
+
#
|
|
40
|
+
# @example Using strings
|
|
41
|
+
# condition = new EightBall::Conditions::List.new min: 'a', max: 'm'
|
|
42
|
+
# condition.satisfied? 'a' => true
|
|
43
|
+
# condition.satisfied? 'z' => false
|
|
44
|
+
def satisfied?(value)
|
|
45
|
+
value >= min && value <= max
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EightBall
|
|
4
|
+
# A Feature is an element of your application that can be enabled or disabled
|
|
5
|
+
# based on various {EightBall::Conditions}.
|
|
6
|
+
class Feature
|
|
7
|
+
attr_reader :name, :enabled_for, :disabled_for
|
|
8
|
+
|
|
9
|
+
# Creates a new instance of an Interval RefreshPolicy.
|
|
10
|
+
#
|
|
11
|
+
# @param name [String] The name of the Feature.
|
|
12
|
+
# @param enabled_for [Array<EightBall::Conditions>, EightBall::Conditions]
|
|
13
|
+
# The Condition(s) that need to be satisfied for the Feature to be enabled.
|
|
14
|
+
# @param disabled_for [Array<EightBall::Conditions>, EightBall::Conditions]
|
|
15
|
+
# The Condition(s) that need to be satisfied for the Feature to be disabled.
|
|
16
|
+
#
|
|
17
|
+
# @example A Feature which is always enabled
|
|
18
|
+
# feature = EightBall::Feature.new 'feature1', EightBall::Conditions::Always
|
|
19
|
+
def initialize(name, enabled_for = [], disabled_for = [])
|
|
20
|
+
@name = name
|
|
21
|
+
@enabled_for = Array enabled_for
|
|
22
|
+
@disabled_for = Array disabled_for
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# "EightBall, is this Feature enabled?"
|
|
26
|
+
#
|
|
27
|
+
# @param parameters [Hash] The parameters the {EightBall::Conditions}
|
|
28
|
+
# of this Feature are concerned with.
|
|
29
|
+
#
|
|
30
|
+
# @return [true] if no {EightBall::Conditions} are set on the Feature.
|
|
31
|
+
# This is equivalent to the {EightBall::Conditions::Always Always} condition.
|
|
32
|
+
# @return [true] if ANY of the {enabled_for} {EightBall::Conditions} are satisfied
|
|
33
|
+
# and NONE of the {disabled_for} {EightBall::Conditions} are satisfied.
|
|
34
|
+
# @return [false ] if ANY of the {disabled_for} {EightBall::Conditions} are satisfied
|
|
35
|
+
#
|
|
36
|
+
# @raise [ArgumentError] if no value is provided for a parameter required
|
|
37
|
+
# by one of the Feature's {EightBall::Conditions}
|
|
38
|
+
#
|
|
39
|
+
# @example The Feature's {EightBall::Conditions} do not require any parameters
|
|
40
|
+
# feature.enabled?
|
|
41
|
+
#
|
|
42
|
+
# @example The Feature's {EightBall::Conditions} require an account ID
|
|
43
|
+
# feature.enabled? account_id: 123
|
|
44
|
+
def enabled?(parameters = {})
|
|
45
|
+
return true if @enabled_for.empty? && @disabled_for.empty?
|
|
46
|
+
return true if @enabled_for.empty? && !any_satisfied?(@disabled_for, parameters)
|
|
47
|
+
|
|
48
|
+
any_satisfied?(@enabled_for, parameters) && !any_satisfied?(@disabled_for, parameters)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def any_satisfied?(conditions, parameters)
|
|
54
|
+
conditions.any? do |condition|
|
|
55
|
+
return condition.satisfied? if condition.parameter.nil?
|
|
56
|
+
|
|
57
|
+
value = parameters[condition.parameter.to_sym]
|
|
58
|
+
raise ArgumentError, "Missing parameter #{condition.parameter}" if value.nil?
|
|
59
|
+
|
|
60
|
+
condition.satisfied? value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'plissken'
|
|
4
|
+
|
|
5
|
+
module EightBall::Parsers
|
|
6
|
+
# A JSON parser will parse JSON into a list of {EightBall::Feature Features}.
|
|
7
|
+
# The top-level JSON element must be an array and must use camel-case;
|
|
8
|
+
# this will be converted to snake-case by EightBall.
|
|
9
|
+
#
|
|
10
|
+
# Below are some examples of valid JSON:
|
|
11
|
+
#
|
|
12
|
+
# @example A single {EightBall::Feature} is enabled for accounts 1-5 as well as region Europe
|
|
13
|
+
# [{
|
|
14
|
+
# "name": "Feature1",
|
|
15
|
+
# "enabledFor": [{
|
|
16
|
+
# "type": "range",
|
|
17
|
+
# "parameter": "accountId",
|
|
18
|
+
# "min": 1,
|
|
19
|
+
# "max": 5
|
|
20
|
+
# }, {
|
|
21
|
+
# "type": "list",
|
|
22
|
+
# "parameter": "regionName",
|
|
23
|
+
# "values": ["Europe"]
|
|
24
|
+
# }]
|
|
25
|
+
# }]
|
|
26
|
+
#
|
|
27
|
+
# @example A single {EightBall::Feature} is disabled completely using the {EightBall::Conditions::Always Always} condition
|
|
28
|
+
# [{
|
|
29
|
+
# "name": "Feature1",
|
|
30
|
+
# "disabledFor": [{
|
|
31
|
+
# "type": "always"
|
|
32
|
+
# }]
|
|
33
|
+
# }]
|
|
34
|
+
class Json
|
|
35
|
+
# Convert the JSON into a list of {EightBall::Feature Features}.
|
|
36
|
+
#
|
|
37
|
+
# @param [String] json The JSON string to parse.
|
|
38
|
+
# @return [Array<EightBall::Feature>] The parsed {EightBall::Feature Features}
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# json_string = <Read from somewhere>
|
|
42
|
+
#
|
|
43
|
+
# parser = EightBall::Parsers::Json.new
|
|
44
|
+
# parser.parse json_string => [Features]
|
|
45
|
+
def parse(json)
|
|
46
|
+
parsed = JSON.parse(json, :symbolize_names => true).to_snake_keys
|
|
47
|
+
|
|
48
|
+
raise ArgumentError, 'JSON input was not an array' unless parsed.is_a? Array
|
|
49
|
+
|
|
50
|
+
parsed.map do |feature|
|
|
51
|
+
enabled_for = create_conditions feature[:enabled_for]
|
|
52
|
+
disabled_for = create_conditions feature[:disabled_for]
|
|
53
|
+
|
|
54
|
+
EightBall::Feature.new feature[:name], enabled_for, disabled_for
|
|
55
|
+
end
|
|
56
|
+
rescue JSON::ParserError => e
|
|
57
|
+
EightBall.logger.error { "Failed to parse JSON: #{e.message}" }
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def create_conditions(json_conditions)
|
|
64
|
+
return [] unless json_conditions && json_conditions.is_a?(Array)
|
|
65
|
+
|
|
66
|
+
json_conditions.map do |condition|
|
|
67
|
+
condition_class = EightBall::Conditions.by_name condition[:type]
|
|
68
|
+
condition_class.new condition
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EightBall::Providers
|
|
4
|
+
# An HTTP Provider will make a GET request to a given URI, and convert
|
|
5
|
+
# the response into an array of {EightBall::Feature Features} using the
|
|
6
|
+
# given {EightBall::Parsers Parser}.
|
|
7
|
+
#
|
|
8
|
+
# The {EightBall::Feature Features} will be automatically kept up to date
|
|
9
|
+
# according to the given {EightBall::Providers::RefreshPolicies RefreshPolicy}.
|
|
10
|
+
class Http
|
|
11
|
+
SUPPORTED_SCHEMES = %w[http https].freeze
|
|
12
|
+
|
|
13
|
+
# @param uri [String] The URI to GET the {EightBall::Feature Features} from.
|
|
14
|
+
# @param options [Hash] The options to create the Provider with.
|
|
15
|
+
#
|
|
16
|
+
# @option options [EightBall::Parsers] :parser
|
|
17
|
+
# The {EightBall::Parsers Parser} used to convert the response to an array
|
|
18
|
+
# of {EightBall::Feature Features}. Defaults to an instance of
|
|
19
|
+
# {EightBall::Parsers::Json}
|
|
20
|
+
#
|
|
21
|
+
# @option options [EightBall::Providers::RefreshPolicies] :refresh_policy
|
|
22
|
+
# The {EightBall::Providers::RefreshPolicies Policy} used to determine
|
|
23
|
+
# when the {EightBall::Feature Features} have gone stale and need to be
|
|
24
|
+
# refreshed. Defaults to an instance of
|
|
25
|
+
# {EightBall::Providers::RefreshPolicies::Interval}
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# provider = EightBall::Providers::Http.new(
|
|
29
|
+
# 'http://www.rewind.io',
|
|
30
|
+
# refresh_policy: EightBall::Providers::RefreshPolicies::Interval.new 120
|
|
31
|
+
# )
|
|
32
|
+
def initialize(uri, options = {})
|
|
33
|
+
raise ArgumentError, 'Invalid HTTP/HTTPS URI provided' unless uri =~ URI.regexp(SUPPORTED_SCHEMES)
|
|
34
|
+
|
|
35
|
+
@uri = URI.parse uri
|
|
36
|
+
|
|
37
|
+
@parser = options[:parser] || EightBall::Parsers::Json.new
|
|
38
|
+
@policy = options[:refresh_policy] || EightBall::Providers::RefreshPolicies::Interval.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the current {EightBall::Feature Features}.
|
|
42
|
+
# @return [Array<{EightBall::Feature}>]
|
|
43
|
+
def features
|
|
44
|
+
@policy.refresh { fetch }
|
|
45
|
+
@features
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def fetch
|
|
51
|
+
@features = @parser.parse Net::HTTP.get(@uri)
|
|
52
|
+
rescue => e
|
|
53
|
+
EightBall.logger.error { "Failed to fetch data from #{@uri}: #{e.message}" }
|
|
54
|
+
@features = []
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module EightBall::Providers::RefreshPolicies
|
|
6
|
+
# An Interval RefreshPolicy states that data is considered fresh for a certain
|
|
7
|
+
# amount of time, after which it is considered stale and should be refreshed.
|
|
8
|
+
class Interval
|
|
9
|
+
SECONDS_IN_A_DAY = 86_400
|
|
10
|
+
|
|
11
|
+
# Creates a new instance of an Interval RefreshPolicy.
|
|
12
|
+
#
|
|
13
|
+
# @param seconds [Integer] The number of seconds the data is considered fresh.
|
|
14
|
+
#
|
|
15
|
+
# @example New data stays fresh for 2 minutes
|
|
16
|
+
# EightBall::Providers::RefreshPolicies::Interval.new 120
|
|
17
|
+
def initialize(seconds = 60)
|
|
18
|
+
@interval = seconds
|
|
19
|
+
@fresh_until = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Yields if the current data is stale, in order to refresh it.
|
|
23
|
+
# Resets the interval once the data is refreshed.
|
|
24
|
+
#
|
|
25
|
+
# @example Load new data if current data is stale
|
|
26
|
+
# policy.refresh { load_new_data }
|
|
27
|
+
def refresh
|
|
28
|
+
return unless block_given? && stale?
|
|
29
|
+
|
|
30
|
+
yield
|
|
31
|
+
@fresh_until = DateTime.now + Rational(@interval, SECONDS_IN_A_DAY)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
protected
|
|
35
|
+
|
|
36
|
+
def stale?
|
|
37
|
+
@fresh_until.nil? || DateTime.now > @fresh_until
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EightBall::Providers
|
|
4
|
+
# A Static provider will always provide the exact list of {EightBall::Features}
|
|
5
|
+
# that were passed in at construction time.
|
|
6
|
+
class Static
|
|
7
|
+
attr_reader :features
|
|
8
|
+
|
|
9
|
+
# Creates a new instance of a Static Provider.
|
|
10
|
+
#
|
|
11
|
+
# @param features [Array<EightBall::Feature>, EightBall::Feature]
|
|
12
|
+
# The {EightBall::Feature Feature(s)} that this provider will return.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# provider = EightBall::Providers::Static.new([
|
|
16
|
+
# EightBall::Feature.new 'feature1',
|
|
17
|
+
# EightBall::Feature.new 'feature2'
|
|
18
|
+
# ])
|
|
19
|
+
def initialize(features = [])
|
|
20
|
+
@features = Array features
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/eight_ball.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'eight_ball/version'
|
|
4
|
+
require 'eight_ball/feature'
|
|
5
|
+
|
|
6
|
+
require 'eight_ball/conditions/conditions'
|
|
7
|
+
require 'eight_ball/conditions/base'
|
|
8
|
+
|
|
9
|
+
require 'eight_ball/conditions/always'
|
|
10
|
+
require 'eight_ball/conditions/list'
|
|
11
|
+
require 'eight_ball/conditions/never'
|
|
12
|
+
require 'eight_ball/conditions/range'
|
|
13
|
+
|
|
14
|
+
require 'eight_ball/parsers/json'
|
|
15
|
+
|
|
16
|
+
require 'eight_ball/providers/http'
|
|
17
|
+
require 'eight_ball/providers/static'
|
|
18
|
+
|
|
19
|
+
require 'eight_ball/providers/refresh_policies/interval'
|
|
20
|
+
|
|
21
|
+
require 'logger'
|
|
22
|
+
|
|
23
|
+
# For all your feature querying needs.
|
|
24
|
+
module EightBall
|
|
25
|
+
# Sets the {EightBall::Providers Provider} instance EightBall
|
|
26
|
+
# will use to obtain your list of {EightBall::Feature Features}.
|
|
27
|
+
#
|
|
28
|
+
# @return [nil]
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# EightBall.provider = EightBall::Providers::Http.new 'http://www.rewind.io'
|
|
32
|
+
def self.provider=(provider)
|
|
33
|
+
@provider = provider
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# "EightBall, is the feature named 'NewFeature' enabled?"
|
|
37
|
+
#
|
|
38
|
+
# @return whether or not the {EightBall::Feature} is enabled.
|
|
39
|
+
# @return [false] if {EightBall::Feature} does not exist.
|
|
40
|
+
#
|
|
41
|
+
# @param name [String] The name of the {EightBall::Feature}.
|
|
42
|
+
# @param parameters [Hash] The parameters the {EightBall::Conditions} of this
|
|
43
|
+
# {EightBall::Feature} are concerned with.
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# EightBall.enabled? 'feature1', account_id: 1
|
|
47
|
+
def self.enabled?(name, parameters = {})
|
|
48
|
+
feature = @provider.features.find { |f| f.name == name }
|
|
49
|
+
return false unless feature
|
|
50
|
+
|
|
51
|
+
feature.enabled? parameters
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# "EightBall, is the feature named 'NewFeature' disabled?"
|
|
55
|
+
#
|
|
56
|
+
# @return whether or not the {EightBall::Feature} is disabled.
|
|
57
|
+
# @return [true] if {EightBall::Feature} does not exist.
|
|
58
|
+
#
|
|
59
|
+
# @param name [String] The name of the {EightBall::Feature}.
|
|
60
|
+
# @param parameters [Hash] The parameters the {EightBall::Conditions} of this
|
|
61
|
+
# {EightBall::Feature} are concerned with.
|
|
62
|
+
#
|
|
63
|
+
# @example
|
|
64
|
+
# EightBall.disabled? 'feature1', account_id: 1
|
|
65
|
+
def self.disabled?(name, parameters = {})
|
|
66
|
+
!enabled? name, parameters
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Yields to the given block of code if the {EightBall::Feature} is enabled.
|
|
70
|
+
#
|
|
71
|
+
# @return [nil] if block is yielded to
|
|
72
|
+
# @return [false] if {EightBall::Feature} is disabled
|
|
73
|
+
#
|
|
74
|
+
# @param name [String] The name of the {EightBall::Feature}.
|
|
75
|
+
# @param parameters [Hash] The parameters the {EightBall::Conditions} of this
|
|
76
|
+
# {EightBall::Feature} are concerned with.
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# EightBall.with 'feature1', account_id: 1 do
|
|
80
|
+
# puts 'Feature is enabled!'
|
|
81
|
+
# end
|
|
82
|
+
def self.with(name, parameters = {})
|
|
83
|
+
return false unless block_given?
|
|
84
|
+
|
|
85
|
+
yield if enabled? name, parameters
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.logger
|
|
89
|
+
@logger ||= Logger.new(STDOUT).tap do |log|
|
|
90
|
+
log.progname = self.name
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.logger=(logger)
|
|
95
|
+
@logger = logger
|
|
96
|
+
end
|
|
97
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eight_ball
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rewind.io
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-12-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: plissken
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.17'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.17'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: inch
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.8'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.8'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: minitest
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '5.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: minitest-reporters
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.3'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.3'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: mocha
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.7'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.7'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: pry-byebug
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.6'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.6'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rake
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '10.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '10.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: simplecov
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0.16'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0.16'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: simplecov-console
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0.4'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0.4'
|
|
153
|
+
description: Ask questions about flagged features
|
|
154
|
+
email:
|
|
155
|
+
- team@rewind.io
|
|
156
|
+
executables: []
|
|
157
|
+
extensions: []
|
|
158
|
+
extra_rdoc_files: []
|
|
159
|
+
files:
|
|
160
|
+
- ".github/CODEOWNERS"
|
|
161
|
+
- ".gitignore"
|
|
162
|
+
- ".travis.yml"
|
|
163
|
+
- CHANGELOG.md
|
|
164
|
+
- Gemfile
|
|
165
|
+
- Gemfile.lock
|
|
166
|
+
- LICENSE
|
|
167
|
+
- README.md
|
|
168
|
+
- Rakefile
|
|
169
|
+
- bin/console
|
|
170
|
+
- bin/setup
|
|
171
|
+
- eight_ball.gemspec
|
|
172
|
+
- examples/http_provider.rb
|
|
173
|
+
- examples/static_provider.rb
|
|
174
|
+
- lib/eight_ball.rb
|
|
175
|
+
- lib/eight_ball/conditions/always.rb
|
|
176
|
+
- lib/eight_ball/conditions/base.rb
|
|
177
|
+
- lib/eight_ball/conditions/conditions.rb
|
|
178
|
+
- lib/eight_ball/conditions/list.rb
|
|
179
|
+
- lib/eight_ball/conditions/never.rb
|
|
180
|
+
- lib/eight_ball/conditions/range.rb
|
|
181
|
+
- lib/eight_ball/feature.rb
|
|
182
|
+
- lib/eight_ball/parsers/json.rb
|
|
183
|
+
- lib/eight_ball/providers/http.rb
|
|
184
|
+
- lib/eight_ball/providers/refresh_policies/interval.rb
|
|
185
|
+
- lib/eight_ball/providers/static.rb
|
|
186
|
+
- lib/eight_ball/version.rb
|
|
187
|
+
homepage: https://github.com/rewindio/eight_ball
|
|
188
|
+
licenses:
|
|
189
|
+
- MIT
|
|
190
|
+
metadata: {}
|
|
191
|
+
post_install_message:
|
|
192
|
+
rdoc_options: []
|
|
193
|
+
require_paths:
|
|
194
|
+
- lib
|
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
196
|
+
requirements:
|
|
197
|
+
- - ">="
|
|
198
|
+
- !ruby/object:Gem::Version
|
|
199
|
+
version: '0'
|
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
201
|
+
requirements:
|
|
202
|
+
- - ">="
|
|
203
|
+
- !ruby/object:Gem::Version
|
|
204
|
+
version: '0'
|
|
205
|
+
requirements: []
|
|
206
|
+
rubyforge_project:
|
|
207
|
+
rubygems_version: 2.7.8
|
|
208
|
+
signing_key:
|
|
209
|
+
specification_version: 4
|
|
210
|
+
summary: The most cost efficient way to flag features
|
|
211
|
+
test_files: []
|