eight_ball 1.0.0

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