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 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
@@ -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
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
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
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0]
4
+ Initial release!
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in eight_ball.gemspec
6
+ gemspec
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
+ [![Build Status](https://travis-ci.com/rewindio/eight_ball.svg?token=2toDwh2UkVcJs8RE5coA&branch=dev)](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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,8 @@
1
+ require 'eight_ball'
2
+
3
+ # Tell EightBall about the Features
4
+ EightBall.provider = EightBall::Providers::Http.new '<YOUR URL HERE>'
5
+
6
+ # Away you go
7
+ EightBall.enabled? 'Feature1', account_id: 4 # true
8
+ EightBall.enabled? 'Feature1', account_id: 2 # false
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EightBall::Conditions
4
+ # The Always Condition is always satisfied
5
+ class Always < Base
6
+ def satisfied?
7
+ true
8
+ end
9
+ end
10
+ end
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EightBall::Conditions
4
+ # The Never Condition is never satisfied
5
+ class Never < Base
6
+ def satisfied?
7
+ false
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EightBall
4
+ VERSION = '1.0.0'
5
+ 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: []