featureflow 0.5.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 14f14953f195ceeb742c570f0bf89c40436dc068
4
- data.tar.gz: 4036b8cc67e22251bfcaf7d83d2cc412f0e50dbb
2
+ SHA256:
3
+ metadata.gz: 2865ebc3fed1dfffb613a9a459ce6e0ac8d6e5cd05d758b4e93944cbc8d8d372
4
+ data.tar.gz: 1b5a7d4b01a2ff98501ca2818e3d00a4d4523525e9e71cd4cda1a847ef009867
5
5
  SHA512:
6
- metadata.gz: d75e26abe09444b7ecdc9e35a281be54fa14bf4398a358a99d32e94774d8b25abd81a558425c584532e0431942863e64c223bfd553680ee869a11802c6786bb5
7
- data.tar.gz: 7238f9574262b630d372f97ddf1d18b6d7ec01e000f3d4fd27a7191ca8969d09036484c3bed01bce690dcad32296d82f14739a1e973cb741e91c996ed105358a
6
+ metadata.gz: 8c1fb252dcd6beac50457f7d8328198b7512d4bad761824d665490dc0a8fa59026aa55901adf0555dbfce12599ecbf1b611695801d150b29a76ee747abb1aadd
7
+ data.tar.gz: 712e342d59b58327b4ec1bf98770946ef255f39dbecc2093dc50fa786252b82f6aed33b06a1faacbd5c7e9f60aa65f750a37e454c4ec6f03f8a8cb9f96bb87c2
data/CHANGELOG.md CHANGED
@@ -1,4 +1,30 @@
1
1
  # Change log
2
+ ## [0.7.0] - 2026-08-11
3
+ ### Fixed
4
+ - Condition operators fail closed instead of raising into the host application: an
5
+ invalid `matches` regex, a non-string attribute against a string operator, or a
6
+ string/number type mismatch on a comparison operator is now a no-match, never an
7
+ exception.
8
+ - `before`/`after` compare parsed instants rather than strings, so rule values with
9
+ arbitrary UTC offsets compare correctly; date-only values are read as UTC midnight
10
+ on every host, not local midnight.
11
+ - Rollout bucketing hashed every user to the same bucket (`user['id']` vs
12
+ `user[:id]`); percentage splits now distribute correctly.
13
+ - `RailsClient#evaluate` and `UserBuilder#with_attributes` were broken; both work
14
+ again, and the Rails generator accepts current `sdk-srv-env-` keys.
15
+ ### Added
16
+ - `Featureflow::Client.new(api_key: '...')` accepts a configuration Hash; unknown
17
+ options raise rather than being silently dropped.
18
+ - Implicit `featureflow.date` / `featureflow.hourofday` attributes are injected at
19
+ evaluation time, matching the other server SDKs.
20
+ ### Changed
21
+ - Requires Ruby >= 3.1 (excon ~> 1.5).
22
+ - The packaged gem now contains only `lib/`, README, CHANGELOG and LICENSE.
23
+
24
+ ## [0.6.0] - 2018-12-17
25
+ ### Fixed
26
+ - Use User over Context matching all other SDKs
27
+ - Use events.featureflow.io endpoint
2
28
  ## [0.5.1] - 2017-07-17
3
29
  ### Fixed
4
30
  - Feature registration now compatible with Ruby 2.4
data/README.md CHANGED
@@ -15,7 +15,7 @@ The easiest way to get started is to follow the [Featureflow quick start guides]
15
15
 
16
16
  ## Change Log
17
17
 
18
- Please see [CHANGELOG](https://github.com/featureflow/featureflow-node-sdk/blob/master/CHANGELOG.md).
18
+ Please see [CHANGELOG](https://github.com/featureflow/featureflow-ruby-sdk/blob/master/CHANGELOG.md).
19
19
 
20
20
  ## Usage
21
21
 
@@ -30,7 +30,7 @@ gem 'featureflow'
30
30
  ```
31
31
 
32
32
  Requiring `featureflow` in your ruby application will expose the classes
33
- `Featureflow::Client`, `Featuerflow::ContextBuilder` and `Featureflow::Feature`.
33
+ `Featureflow::Client`, `Featuerflow::UserBuilder` and `Featureflow::Feature`.
34
34
 
35
35
  The usage of each class is documented below.
36
36
 
@@ -79,45 +79,45 @@ end
79
79
 
80
80
 
81
81
 
82
- #### Defining Context
82
+ #### Defining a User
83
83
 
84
- Before evaluating a feature you must define a context for the current user.
85
- Featureflow uses context to target different user groups to specific feature variants.
86
- A featureflow context has a `key`, which should uniquely identify the current user, and optionally additional `values`.
87
- Featureflow requires the context `key` to be unique per user for gradual rollout of features.
84
+ Before evaluating a feature you must define a user.
85
+ Featureflow uses users to target different user groups to specific feature variants.
86
+ A featureflow user has an `id`, which should uniquely identify the current user, and optionally additional `attributes`.
87
+ Featureflow requires the user `id` to be unique per user for gradual rollout of features.
88
88
 
89
- There are two ways to define context:
89
+ There are two ways to define a user:
90
90
  ```ruby
91
91
  require 'featureflow'
92
- context_key = '<unique_user_identifier>'
92
+ user_id = '<unique_user_identifier>'
93
93
 
94
- # option 1, use the context builder
95
- context = Featureflow::ContextBuilder.new(context_key)
96
- .with_values(country: 'US',
94
+ # option 1, use the user builder
95
+ user = Featureflow::UserBuilder.new(user_id)
96
+ .with_attributes(country: 'US',
97
97
  roles: %w[USER_ADMIN, BETA_CUSTOMER])
98
98
  .build
99
99
 
100
100
  # option 2, use just a string
101
- context = context_key
101
+ user = user_id
102
102
  ```
103
103
 
104
104
  #### Evaluating Features
105
105
 
106
106
  In your code, you can test the value of your feature using something similar to below
107
- For these examples below, assume the feature `my-feature-key` is equal to `'on'` for the current `context`
107
+ For these examples below, assume the feature `my-feature-key` is equal to `'on'` for the current `user`
108
108
  ```ruby
109
- if featureflow.evaluate('my-feature-key', context).is? 'on'
110
- # this code will be run because 'my-feature-key' is set to 'on' for the given context
109
+ if featureflow.evaluate('my-feature-key', user).is? 'on'
110
+ # this code will be run because 'my-feature-key' is set to 'on' for the given user
111
111
  end
112
112
  ```
113
113
  Because the most common variants for a feature are `'on'` and `'off'`, we have provided two helper methods `.on?` and `.off?`
114
114
 
115
115
  ```ruby
116
- if featureflow.evaluate('my-feature-key', context).on?
116
+ if featureflow.evaluate('my-feature-key', user).on?
117
117
  # this feature code will be run because 'my-feature-key' is set to 'on'
118
118
  end
119
119
 
120
- if featureflow.evaluate('my-feature-key', context).off?
120
+ if featureflow.evaluate('my-feature-key', user).off?
121
121
  # this feature code won't be run because 'my-feature-key' is not set to 'off'
122
122
  end
123
123
  ```
@@ -147,9 +147,9 @@ featureflow = Featureflow::Client.new(api_key: FEATUREFLOW_SERVER_KEY,
147
147
  ])
148
148
 
149
149
  # ... app has been started offline
150
- featureflow.evaluate('key-one', context).on? # == true
151
- featureflow.evaluate('key-two', context).off? # == true
152
- featureflow.evaluate('key-three', context).is? 'custom' # == true
150
+ featureflow.evaluate('key-one', user).on? # == true
151
+ featureflow.evaluate('key-two', user).off? # == true
152
+ featureflow.evaluate('key-three', user).is? 'custom' # == true
153
153
 
154
154
  ```
155
155
 
@@ -170,4 +170,25 @@ Apache-2.0
170
170
  [rubygems-img]: https://badge.fury.io/rb/featureflow.png
171
171
 
172
172
  [dependency-url]: https://www.featureflow.io
173
- [dependency-img]: https://www.featureflow.io/wp-content/uploads/2016/12/featureflow-web.png
173
+ [dependency-img]: https://www.featureflow.io/wp-content/uploads/2016/12/featureflow-web.png
174
+
175
+ #Developer documentation
176
+
177
+ To build and test the SDK
178
+
179
+ ```
180
+ rvm install 2.5.1
181
+ rvm use --default 2.5.1
182
+ bundle install
183
+ ruby test.rb
184
+ ```
185
+
186
+ ## Manual test harness (example server)
187
+
188
+ To try the SDK against a real Featureflow environment without writing any code, run the small local harness in [examples/harness](examples/harness):
189
+
190
+ ```
191
+ FEATUREFLOW_SERVER_KEY=sdk-srv-env-<your_key> bundle exec ruby examples/harness/server.rb
192
+ ```
193
+
194
+ Then open `http://127.0.0.1:3456/` to evaluate features in the browser, or hit `GET /api/evaluate?feature=<key>&userId=<id>` directly. See [examples/harness/README.md](examples/harness/README.md) for all endpoints and environment variables (including pointing at staging via `FEATUREFLOW_BASE_URL`).
@@ -1,83 +1,107 @@
1
- require 'logger'
2
1
  require 'time'
3
- require 'pp'
4
2
  require 'featureflow/evaluate'
5
3
  require 'featureflow/polling_client'
6
4
  require 'featureflow/events_client'
7
5
 
8
6
  module Featureflow
9
7
  class Client
10
- def initialize(config = {})
11
- api_key = config[:api_key] || ENV['FEATUREFLOW_SERVER_KEY']
12
- raise ArgumentError, "You have not defined either config[:api_key] or ENV[:FEATUREFLOW_SERVER_KEY]" unless api_key
13
- Featureflow.logger.info 'initializing client'
8
+ def initialize(configuration = nil)
9
+ @configuration = build_configuration(configuration)
10
+ @configuration.validate!
14
11
  @features = {}
15
- @config = {
16
- api_key: api_key,
17
- url: 'https://app.featureflow.io',
18
- path: '/api/sdk/v1/features',
19
- with_features: [],
20
- disable_events: false
21
- }.merge(config)
22
-
23
- unless with_features_valid? @config[:with_features]
24
- raise ArgumentError, 'config[:with_features] must be an array of Feature hashes. Use Featureflow::Feature.create(key, failover_variant)'
25
- end
12
+
13
+ Featureflow.logger.info 'initializing client'
26
14
 
27
15
  @failover_variants = {}
28
- @config[:with_features].each do |feature|
16
+ @configuration.with_features.each do |feature|
29
17
  Featureflow.logger.info "Registering feature with key #{feature[:key]}"
30
18
  failover = feature[:failover_variant];
31
19
  @failover_variants[feature[:key]] = failover if failover.is_a?(String) && !failover.empty?
32
20
  end
33
21
 
34
- unless @config[:disable_events]
35
- @events_client = EventsClient.new @config[:url], @config[:api_key]
36
- @events_client.register_features @config[:with_features]
22
+ unless @configuration.disable_events
23
+ @events_client = EventsClient.new @configuration.event_endpoint, @configuration.api_key
24
+ @events_client.register_features @configuration.with_features
37
25
  end
38
26
 
39
- PollingClient.new(@config[:url] + @config[:path],
40
- @config[:api_key],
41
- delay: 30,
42
- timeout: 30) {|features| update_features(features)}
27
+ reload
43
28
 
44
29
  Featureflow.logger.info 'client initialized'
45
30
  end
46
31
 
32
+ def reload
33
+ @polling_client.finish if @polling_client
34
+ @polling_client = build_polling_client
35
+ end
36
+
37
+ def build_polling_client
38
+ PollingClient.new(
39
+ @configuration.endpoint,
40
+ @configuration.api_key,
41
+ poll_interval: 10,
42
+ timeout: 30
43
+ )
44
+ end
45
+
47
46
  def feature(key)
48
- @features[key]
47
+ @polling_client.feature(key)
49
48
  end
50
49
 
51
- def evaluate(key, context)
50
+ def failover_variant(key)
51
+ @failover_variants[key]
52
+ end
53
+
54
+ def evaluate(key, user)
52
55
  raise ArgumentError, 'key must be a string' unless key.is_a?(String)
53
- raise ArgumentError, 'context is required' unless context
54
- unless context.is_a?(String) || context.is_a?(Hash)
55
- raise ArgumentError, 'context must be either a string context key,' + \
56
- ' or a Hash built using Featureflow::ContextBuilder)'
56
+ raise ArgumentError, 'user is required' unless user
57
+ unless user.is_a?(String) || user.is_a?(Hash)
58
+ raise ArgumentError, 'user must be either a string user id,' + \
59
+ ' or a Hash built using Featureflow::UserBuilder)'
57
60
  end
58
61
 
59
- context = ContextBuilder.new(context).build if context.is_a?(String)
60
-
61
- context = context.dup
62
- context[:values] = context[:values].merge('featureflow.key' => context[:key],
63
- 'featureflow.date' => Time.now.iso8601)
62
+ user = UserBuilder.new(user).build if user.is_a?(String)
64
63
 
65
- Evaluate.new key, feature(key), failover_variant(key), context, '1', @events_client
66
- end
64
+ user = user.dup
65
+ # Implicit attributes injected at evaluation time (matching the Node SDK's
66
+ # EvaluateHelpers): featureflow.date/hourofday let rules target the current
67
+ # moment; sdk-server's partial evaluation defers these to the JS client, but
68
+ # server SDKs evaluate them locally against "now".
69
+ now = Time.now
70
+ user[:attributes] = user[:attributes].merge(
71
+ 'featureflow.user.id' => user[:id],
72
+ 'featureflow.date' => now.utc.iso8601,
73
+ 'featureflow.hourofday' => now.hour
74
+ )
67
75
 
68
- private def failover_variant(key)
69
- @failover_variants[key]
76
+ Evaluate.new(
77
+ feature_key: key,
78
+ feature: feature(key),
79
+ failover_variant: failover_variant(key),
80
+ user: user,
81
+ salt: '1',
82
+ events_client: @events_client
83
+ )
70
84
  end
71
85
 
72
- private def update_features(features)
73
- Featureflow.logger.info "updating features"
74
- @features = features
75
- end
86
+ # Accepts either a Featureflow::Configuration, or a Hash of configuration
87
+ # options — `Featureflow::Client.new(api_key: '...')` is the form the README
88
+ # leads with and the natural Ruby idiom. Passing nothing keeps using the
89
+ # process-wide Featureflow.configuration (which reads FEATUREFLOW_SERVER_KEY).
90
+ private def build_configuration(configuration)
91
+ return Featureflow.configuration if configuration.nil?
92
+ return configuration unless configuration.is_a?(Hash)
76
93
 
77
- private def with_features_valid?(features)
78
- features.all? { |feature|
79
- feature[:key].is_a?(String) && feature[:failover_variant].is_a?(String) && feature[:variants].is_a?(Array)
80
- }
94
+ config = Configuration.new
95
+ configuration.each do |option, value|
96
+ setter = "#{option}="
97
+ # Fail loudly rather than silently dropping a mistyped option — a
98
+ # misspelt `endpoint:` would otherwise quietly poll production.
99
+ unless config.respond_to?(setter)
100
+ raise ArgumentError, "Unknown Featureflow configuration option: #{option}"
101
+ end
102
+ config.public_send(setter, value)
103
+ end
104
+ config
81
105
  end
82
106
  end
83
107
  end
@@ -1,33 +1,104 @@
1
+ require 'date'
2
+ require 'time'
3
+
1
4
  module Featureflow
2
5
  class Conditions
6
+ # Mirrors the Node SDK's dateParse: dates compare by instant, not by string,
7
+ # so ISO values with different UTC offsets (e.g. "+04:00" vs "Z") compare correctly.
8
+ #
9
+ # A value carrying no timezone information is read as UTC, per the SDK contract
10
+ # ("A date-only value denotes UTC midnight"): the dashboard's date picker emits
11
+ # date-only values like "2026-07-03", and reading those as *local* midnight would
12
+ # make a scheduled rollout fire at a different instant on every host in a fleet.
13
+ # Values with an explicit offset or "Z" keep their instant, unshifted.
14
+ def self.to_epoch(value)
15
+ case value
16
+ when Time then value.to_f
17
+ when Numeric then value.to_f
18
+ when String
19
+ begin
20
+ parsed = Time.parse(value)
21
+ # Time.parse silently falls back to the process's timezone when the value
22
+ # carries no zone; adding back the offset it applied re-reads the same
23
+ # wall-clock components as UTC. Date._parse only reports :offset when the
24
+ # value actually stated a zone.
25
+ parsed += parsed.utc_offset unless Date._parse(value).key?(:offset)
26
+ parsed.to_f
27
+ rescue ArgumentError, TypeError, RangeError
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ # Both operands must be strings for a string operator to mean anything.
34
+ def self.both_strings?(a, b)
35
+ a.is_a?(String) && b.is_a?(String)
36
+ end
37
+
38
+ # Numeric-to-numeric or string-to-string only. Mixing the two raises in Ruby, and
39
+ # coercing would answer version comparisons wrongly.
40
+ def self.comparable?(a, b)
41
+ (a.is_a?(Numeric) && b.is_a?(Numeric)) || (a.is_a?(String) && b.is_a?(String))
42
+ end
43
+
3
44
  def self.test(op, a, b)
4
45
  case op
5
46
  when 'equals'
6
47
  a.eql? b[0]
48
+ # The string operators assume both sides are strings. A numeric attribute against
49
+ # `contains` raised NoMethodError straight into the host application — the same
50
+ # fail-closed violation as the invalid regex below, and reachable the same way,
51
+ # from a rule somebody typed into the dashboard. Python already guards these with
52
+ # an explicit type check; this brings Ruby into line. A type mismatch is a
53
+ # no-match, not an exception.
7
54
  when 'contains'
8
- a.include? b[0]
55
+ both_strings?(a, b[0]) && a.include?(b[0])
9
56
  when 'startsWith'
10
- a.start_with? b[0]
57
+ both_strings?(a, b[0]) && a.start_with?(b[0])
11
58
  when 'endsWith'
12
- a.end_with? b[0]
59
+ both_strings?(a, b[0]) && a.end_with?(b[0])
13
60
  when 'matches'
14
- a.match? Regexp.new(b[0])
61
+ # The pattern is untrusted configuration, not code: it comes from a targeting
62
+ # rule somebody typed into the dashboard, so a malformed one like "[unclosed"
63
+ # is a user's mistake rather than a bug in the SDK. The contract
64
+ # (featureflow-client-sdk-testbed/CONTRACT.md, "Operators") therefore requires
65
+ # an invalid regex to return false — a flag SDK must degrade rather than take
66
+ # the host application down with it.
67
+ #
68
+ # Only the regex failure is rescued, never everything: RegexpError for a
69
+ # pattern that will not compile, TypeError for a value that is not a string at
70
+ # all. A genuine bug anywhere else still surfaces.
71
+ begin
72
+ a.match? Regexp.new(b[0])
73
+ rescue RegexpError, TypeError
74
+ false
75
+ end
15
76
  when 'in'
16
77
  b.include? a
17
78
  when 'notIn'
18
79
  !b.include? a
80
+ # Comparing a String with a Numeric raises ArgumentError in Ruby, so an attribute
81
+ # whose type does not match the rule's took the host application down. The
82
+ # contract requires a numeric operator to return false on non-numeric input
83
+ # rather than coercing — "1.10.0" > "1.9.0" must not silently succeed — so a
84
+ # mismatch is simply a no-match. String-to-string comparison is kept, since Ruby
85
+ # orders strings meaningfully and the server behaves the same way.
19
86
  when 'greaterThan'
20
- a > b[0]
87
+ comparable?(a, b[0]) && a > b[0]
21
88
  when 'greaterThanOrEqual'
22
- a >= b[0]
89
+ comparable?(a, b[0]) && a >= b[0]
23
90
  when 'lessThan'
24
- a < b[0]
91
+ comparable?(a, b[0]) && a < b[0]
25
92
  when 'lessThanOrEqual'
26
- a <= b[0]
93
+ comparable?(a, b[0]) && a <= b[0]
27
94
  when 'before'
28
- a < b[0]
95
+ a_time = to_epoch(a)
96
+ b_time = to_epoch(b[0])
97
+ !!(a_time && b_time && a_time < b_time)
29
98
  when 'after'
30
- a > b[0]
99
+ a_time = to_epoch(a)
100
+ b_time = to_epoch(b[0])
101
+ !!(a_time && b_time && a_time > b_time)
31
102
  else
32
103
  false
33
104
  end
@@ -0,0 +1,36 @@
1
+ class Featureflow::Configuration
2
+ attr_accessor :api_key
3
+ attr_accessor :endpoint
4
+ attr_accessor :event_endpoint
5
+ attr_accessor :disable_events
6
+ attr_accessor :with_features
7
+ attr_accessor :logger
8
+
9
+ DEFAULT_ENDPOINT = 'https://app.featureflow.io'
10
+ DEFAULT_EVENT_ENDPOINT = 'https://events.featureflow.io'
11
+
12
+ def initialize
13
+ self.api_key = ENV["FEATUREFLOW_SERVER_KEY"]
14
+ self.endpoint = DEFAULT_ENDPOINT
15
+ self.event_endpoint = DEFAULT_EVENT_ENDPOINT
16
+ self.disable_events = false
17
+ self.with_features = []
18
+
19
+ self.logger = Logger.new(STDOUT)
20
+ self.logger.level = Logger::WARN
21
+ end
22
+
23
+ def validate!
24
+ unless with_features_valid? @with_features
25
+ raise ArgumentError, 'with_features must be an array of Feature hashes. Use Featureflow::Feature.create(key, failover_variant)'
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def with_features_valid?(features)
32
+ features.all? do |feature|
33
+ feature[:key].is_a?(String) && feature[:failover_variant].is_a?(String) && feature[:variants].is_a?(Array)
34
+ end
35
+ end
36
+ end
@@ -2,15 +2,22 @@ require 'featureflow/evaluate_helpers'
2
2
 
3
3
  module Featureflow
4
4
  class Evaluate
5
- def initialize(feature_key, feature, failover_variant, context, salt, events_client = nil)
6
- @evaluated_variant = calculate_variant feature_key, feature, failover_variant, context, salt
7
- @events_client = events_client
8
- @context = context
5
+ def initialize(feature_key:, feature:, failover_variant:, user:, salt:, events_client: nil)
9
6
  @key = feature_key
7
+ @feature = feature
8
+ @failover_variant = failover_variant
9
+ @user = user
10
+ @salt = salt
11
+ @events_client = events_client
12
+
13
+ @has_failover = @failover_variant.is_a?(String)
14
+ @failover_variant = 'off' unless @has_failover
15
+
16
+ @evaluated_variant = evaluate_variant
10
17
  end
11
18
 
12
19
  def is?(value)
13
- @events_client.evaluate @key, @evaluated_variant, value, @context unless @events_client.is_a?(NilClass)
20
+ @events_client.evaluate(@key, @evaluated_variant, value, @user) if @events_client
14
21
  @evaluated_variant == value
15
22
  end
16
23
 
@@ -28,22 +35,23 @@ module Featureflow
28
35
  @evaluated_variant
29
36
  end
30
37
 
31
- private def calculate_variant(feature_key, feature, failover_variant, context = {}, salt = '1')
32
- unless feature
33
- has_failover = failover_variant.is_a?(String)
34
- failover_variant = 'off' unless has_failover
35
- Featureflow.logger.info "Evaluating nil feature '#{feature_key}' using the "\
36
- "#{has_failover ? 'provided' : 'default'} failover '#{failover_variant}'"
37
- return failover_variant
38
+ private
39
+
40
+ def evaluate_variant
41
+ unless @feature
42
+ Featureflow.logger.info "Evaluating nil feature '#{@feature_key}' using the "\
43
+ "#{@has_failover ? 'provided' : 'default'} failover '#{@failover_variant}'"
44
+ return @failover_variant
38
45
  end
39
46
 
40
- return feature['offVariantKey'] unless feature['enabled']
41
- feature['rules'].each do |rule|
42
- next unless EvaluateHelpers.rule_matches rule, context
43
- hash = EvaluateHelpers.calculate_hash salt, feature['key'], context['key']
44
- variant_value = EvaluateHelpers.get_variant_value hash
45
- return EvaluateHelpers.get_variant_split_key rule['variantSplits'], variant_value
47
+ return @feature['offVariantKey'] unless @feature['enabled']
48
+
49
+ @feature['rules'].each do |rule|
50
+ next unless EvaluateHelpers.rule_matches(rule, @user)
51
+ hash = EvaluateHelpers.calculate_hash(@salt, @feature['key'], @user[:id])
52
+ variant_value = EvaluateHelpers.get_variant_value(hash)
53
+ return EvaluateHelpers.get_variant_split_key(rule['variantSplits'], variant_value)
46
54
  end
47
55
  end
48
56
  end
49
- end
57
+ end
@@ -3,15 +3,15 @@ require 'digest/sha1'
3
3
 
4
4
  module Featureflow
5
5
  class EvaluateHelpers
6
- def self.rule_matches(rule, context)
6
+ def self.rule_matches(rule, user)
7
7
  if rule['defaultRule']
8
8
  true # the default rule will always match true
9
9
  else
10
10
  rule['audience']['conditions'].all? do |condition|
11
- context_values = context[:values][condition['target']]
11
+ user_attributes = user[:attributes][condition['target']]
12
12
  # convert to array to work with test
13
- Array(context_values).any? do |value|
14
- Conditions.test condition['operator'], value, condition['values']
13
+ Array(user_attributes).any? do |attribute|
14
+ Conditions.test condition['operator'], attribute, condition['values']
15
15
  end
16
16
  end
17
17
  end
@@ -1,13 +1,31 @@
1
1
  require 'excon'
2
2
  require 'json'
3
+ require 'thread'
4
+ require 'time'
3
5
 
4
6
  module Featureflow
7
+ #LOCK = Mutex.new
8
+
5
9
  class EventsClient
6
10
  def initialize(url, api_key)
7
11
  @url = url
8
12
  @api_key = api_key
13
+ @eventsQueue = Queue.new
14
+ @scheduler = start_scheduler
15
+ end
16
+
17
+ def start_scheduler()
18
+ Thread.new do
19
+ loop do
20
+ begin
21
+ sleep 10
22
+ send_queue
23
+ end
24
+ end
25
+ end
9
26
  end
10
27
 
28
+ #register features are not queued and go straight out
11
29
  def register_features(with_features)
12
30
  Thread.new do
13
31
  features = []
@@ -16,28 +34,63 @@ module Featureflow
16
34
  variants: feature[:variants],
17
35
  failoverVariant: feature[:failover_variant])
18
36
  end
19
- send_event 'Register Features', :put, 'api/sdk/v1/register', features
37
+ send_event 'Register Features', :put, '/api/sdk/v1/register', features
20
38
  end
21
39
  end
22
40
 
23
- def evaluate(key, evaluated_variant, expected_variant, context)
41
+ def evaluate(key, evaluated_variant, expected_variant, user)
24
42
  Thread.new do
25
- send_event 'Evaluate Variant', :post, 'api/sdk/v1/events', [{
26
- featureKey: key,
27
- evaluatedVariant: evaluated_variant,
28
- expectedVaraint: expected_variant,
29
- context: context
30
- }]
43
+ timestamp = Time.now.iso8601
44
+ queue_event ({
45
+ featureKey: key,
46
+ evaluatedVariant: evaluated_variant,
47
+ expectedVaraint: expected_variant,
48
+ user: user,
49
+ timestamp: timestamp
50
+ })
31
51
  end
32
52
  end
33
53
 
34
- private def send_event(event_name, method, path, body)
54
+ def queue_event(event)
55
+ #add to queue
56
+
57
+ @eventsQueue.push(event)
58
+
59
+ if !@scheduler.alive?
60
+ @scheduler = start_scheduler
61
+ end
62
+
63
+ if @eventsQueue.length >= 10000
64
+ send_queue
65
+ end
66
+ #id queue = 10000 then send_queue
67
+ end
68
+
69
+ def send_queue()
70
+ events =[]
71
+ begin
72
+ loop do
73
+ events << @eventsQueue.pop(true)
74
+ end
75
+ rescue ThreadError
76
+ end
77
+
78
+ if !events.empty?
79
+ send_event 'Evaluate Variant', :post, '/api/sdk/v1/events', events
80
+ end
81
+
82
+ end
83
+
84
+ =begin
85
+ private def send_queue()
86
+
35
87
  connection = Excon.new(@url)
36
88
  response = connection.request(method: method,
37
89
  path: path,
38
90
  headers: {
39
91
  'Authorization' => "Bearer #{@api_key}",
40
- 'Content-Type' => 'application/json;charset=UTF-8'
92
+ 'Content-Type' => 'application/json;charset=UTF-8',
93
+ 'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
41
94
  },
42
95
  omit_default_port: true,
43
96
  body: JSON.generate(body))
@@ -48,5 +101,27 @@ module Featureflow
48
101
  rescue => e
49
102
  Featureflow.logger.error e.inspect
50
103
  end
104
+ =end
105
+
106
+ private def send_event(event_name, method, path, body)
107
+ connection = Excon.new(@url)
108
+ response = connection.request(method: method,
109
+ path: path,
110
+ headers: {
111
+ 'Authorization' => "Bearer #{@api_key}",
112
+ 'Content-Type' => 'application/json;charset=UTF-8',
113
+ 'Accept' => 'Application/Json',
114
+ 'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
115
+ },
116
+ omit_default_port: true,
117
+ body: JSON.generate(body))
118
+ if response.status >= 400
119
+ Featureflow.logger.error "unable to send event #{event_name} to #{@url+path}. Failed with response status #{response.status}"
120
+ Featureflow.logger.error response.to_s
121
+ end
122
+ rescue => e
123
+ Featureflow.logger.error e.inspect
124
+ end
125
+
51
126
  end
52
- end
127
+ end
@@ -7,15 +7,18 @@ module Featureflow
7
7
  poll_interval: 30,
8
8
  timeout: 30
9
9
  }.freeze
10
- def initialize(url, api_key, options = {}, &set_features)
10
+ LOCK = Mutex.new
11
+
12
+ def initialize(url, api_key, options = {})
11
13
  @etag = ''
12
14
  @url = url
13
15
  @api_key = api_key
14
16
  @options = DEFAULT_OPTIONS.merge(options)
15
- @set_features = set_features
17
+ @features = {}
16
18
 
17
19
  load_features
18
- Thread.new do
20
+
21
+ @thread = Thread.new do
19
22
  loop do
20
23
  sleep @options[:poll_interval]
21
24
  load_features
@@ -23,14 +26,30 @@ module Featureflow
23
26
  end
24
27
  end
25
28
 
29
+ def finish
30
+ @thread.exit
31
+ end
32
+
33
+ def feature(key)
34
+ LOCK.synchronize { @features[key] }
35
+ end
36
+
26
37
  def load_features
27
- response = Excon.get(@url, headers: {
38
+ response = Excon.get(@url + + '/api/sdk/v1/features', headers: {
28
39
  'Authorization' => "Bearer #{@api_key}",
29
- 'If-None-Match' => @etag
40
+ 'Accept' => 'Application/Json',
41
+ 'If-None-Match' => @etag,
42
+ 'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
30
43
  }, omit_default_port: true, read_timeout: @options[:timeout])
44
+
31
45
  if response.status == 200
46
+ Featureflow.logger.debug "updating features"
47
+
32
48
  @etag = response.headers['ETag']
33
- @set_features.call(JSON.parse(response.body))
49
+
50
+ features = JSON.parse(response.body)
51
+
52
+ LOCK.synchronize { @features = features }
34
53
  elsif response.status >= 400
35
54
  Featureflow.logger.error "request for features failed with response status #{response.status}"
36
55
  Featureflow.logger.error response.to_s
@@ -1,19 +1,37 @@
1
1
  require 'featureflow/client'
2
+ require 'featureflow/user_builder'
2
3
 
3
4
  module Featureflow
4
5
  class RailsClient
5
6
  def initialize(request)
6
7
  @request = request
7
8
  end
8
- def evaluate(key, context)
9
- request_context_values = {
9
+
10
+ # `user` is whatever Client#evaluate accepts: a String user id, or a user
11
+ # Hash built with Featureflow::UserBuilder ({id:, attributes:}). The current
12
+ # request's ip and url are merged in as implicit attributes so targeting
13
+ # rules can match on them; anything the caller set explicitly wins.
14
+ def evaluate(key, user)
15
+ Featureflow.evaluate(key, with_request_attributes(user))
16
+ end
17
+
18
+ private
19
+
20
+ def with_request_attributes(user)
21
+ user = UserBuilder.new(user).build if user.is_a?(String)
22
+ # Anything else is left alone so Client#evaluate raises its own ArgumentError.
23
+ return user unless user.is_a?(Hash)
24
+
25
+ user.merge(attributes: request_attributes.merge(user[:attributes] || {}))
26
+ end
27
+
28
+ # Attribute keys are Strings throughout the evaluation path (UserBuilder
29
+ # normalises them), so the request attributes are keyed as Strings too.
30
+ def request_attributes
31
+ {
10
32
  'featureflow.ip' => @request.remote_ip,
11
33
  'featureflow.url' => @request.original_url
12
34
  }
13
- context = {key: context, values: {}} if context.is_a?(String)
14
- Featureflow.featureflow.evaluate(key,
15
- key: context[:key],
16
- values: request_context_values.merge(context[:values]))
17
35
  end
18
36
  end
19
- end
37
+ end
@@ -1,42 +1,12 @@
1
1
  require 'featureflow/rails/rails_client'
2
- module Featureflow
3
- LOCK = Mutex.new
4
-
5
- class << self
6
- def configure(config_hash=nil)
7
- if config_hash
8
- configuration.merge!(config_hash)
9
- end
10
-
11
- yield(configuration) if block_given?
12
- yield(featureflow) if block_given?
13
- end
14
-
15
- # Configuration getters
16
- def configuration
17
- @configuration = nil unless defined?(@configuration)
18
- @configuration || LOCK.synchronize { @configuration ||= {} }
19
- end
20
-
21
- # Configuration getters
22
- def featureflow
23
- @featureflow = nil unless defined?(@featureflow)
24
- @featureflow || LOCK.synchronize { @featureflow ||= Featureflow::Client.new(configuration) }
25
- end
26
- end
27
-
28
- class FeatureflowRailtie < Rails::Railtie
29
-
30
- config.before_initialize do
31
- ActiveSupport.on_load(:action_controller) do
32
- # @client =
33
- ActionController::Base.class_eval do
34
- def featureflow
35
- Featureflow::RailsClient.new(request)
36
- end
2
+ class Featureflow::FeatureflowRailtie < Rails::Railtie
3
+ config.before_initialize do
4
+ ActiveSupport.on_load(:action_controller) do
5
+ ActionController::Base.class_eval do
6
+ def featureflow
7
+ @featureflow ||= Featureflow::RailsClient.new(request)
37
8
  end
38
9
  end
39
10
  end
40
-
41
11
  end
42
12
  end
@@ -0,0 +1,34 @@
1
+ module Featureflow
2
+ class UserBuilder
3
+ def initialize(id)
4
+ raise ArgumentError, 'Parameter id must be a String' unless id.is_a?(String) && !id.empty?
5
+ @user_id = id
6
+ @attributes = {}
7
+ self
8
+ end
9
+
10
+ def with_attributes(hash)
11
+ raise ArgumentError, 'Parameter hash must be a Hash' unless hash.is_a?(Hash)
12
+ normalized = {}
13
+ hash.each do |k, v|
14
+ raise ArgumentError, "Value for #{k} must be a valid 'primitive' JSON datatype" unless valid_attribute?(v)
15
+ normalized[k.to_s] = v
16
+ end
17
+ @attributes = @attributes.merge(normalized)
18
+ self
19
+ end
20
+
21
+ def build
22
+ {
23
+ id: @user_id,
24
+ attributes: @attributes
25
+ }
26
+ end
27
+
28
+ private def valid_attribute?(attributes)
29
+ Array(attributes).all? do |v|
30
+ [String, Numeric, TrueClass, FalseClass].any? { |type| v.is_a?(type) }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Featureflow
2
- VERSION = "0.5.1"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/featureflow.rb CHANGED
@@ -1,16 +1,38 @@
1
+ require 'logger'
1
2
  require 'featureflow/version'
3
+ require 'featureflow/configuration'
2
4
  require 'featureflow/client'
3
- require 'featureflow/context_builder'
5
+ require 'featureflow/user_builder'
4
6
  require 'featureflow/feature'
5
7
 
6
8
  module Featureflow
7
9
  class << self
8
- attr_writer :logger
10
+ def configure(config_hash = nil)
11
+ if config_hash
12
+ config_hash.each do |k, v|
13
+ configuration.send("#{k}=", v) rescue nil if configuration.respond_to?("#{k}=")
14
+ end
15
+ end
16
+
17
+ yield(configuration) if block_given?
18
+ end
19
+
20
+ def configuration
21
+ @configuration ||= Featureflow::Configuration.new
22
+ end
9
23
 
10
24
  def logger
11
- @logger ||= Logger.new($stderr).tap do |log|
12
- log.progname = self.name
13
- end
25
+ configuration.logger
26
+ end
27
+
28
+ def client
29
+ @client ||= Featureflow::Client.new(configuration)
30
+ end
31
+
32
+ alias featureflow client
33
+
34
+ def evaluate(*args)
35
+ client.evaluate(*args)
14
36
  end
15
37
  end
16
38
  end
@@ -9,7 +9,7 @@ class FeatureflowGenerator < Rails::Generators::Base
9
9
  desc "Configures the featureflow with your API key"
10
10
 
11
11
  def create_initializer_file
12
- unless /^srv-env-[a-f0-9]{32}$/ =~ api_key
12
+ unless /^sdk-srv-env-[a-f0-9]{32}$/ =~ api_key
13
13
  raise Thor::Error, "Invalid featureflow environment api key #{api_key.inspect}\nYou can find your environment api key on your featureflow dashboard at https://[APP-NAME].featureflow.io/"
14
14
  end
15
15
 
metadata CHANGED
@@ -1,105 +1,113 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: featureflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
- - Henry Young
8
- autorequire:
7
+ - Oliver Oldfield-Hodge
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-17 00:00:00.000000000 Z
11
+ date: 2026-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.15'
19
+ version: '13.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.15'
26
+ version: '13.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '3.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '3.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec
42
+ name: cucumber
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
47
+ version: '11.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
54
+ version: '11.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: cucumber
56
+ name: webrick
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: debug
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: 2.4.0
75
+ version: '1.9'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: 2.4.0
82
+ version: '1.9'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: excon
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 0.57.0
89
+ version: '1.5'
76
90
  type: :runtime
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 0.57.0
96
+ version: '1.5'
83
97
  description: Connects your server with Featureflow. Create features at https://featureflow.io.
84
98
  email:
85
- - henry@featureflow.io
99
+ - contact@featureflow.io
86
100
  executables: []
87
101
  extensions: []
88
102
  extra_rdoc_files: []
89
103
  files:
90
- - ".gitignore"
91
- - ".rubocop.yml"
92
104
  - CHANGELOG.md
93
- - Gemfile
94
- - Gemfile.lock
95
105
  - LICENSE
96
106
  - README.md
97
- - Rakefile
98
- - featureflow.gemspec
99
107
  - lib/featureflow.rb
100
108
  - lib/featureflow/client.rb
101
109
  - lib/featureflow/conditions.rb
102
- - lib/featureflow/context_builder.rb
110
+ - lib/featureflow/configuration.rb
103
111
  - lib/featureflow/evaluate.rb
104
112
  - lib/featureflow/evaluate_helpers.rb
105
113
  - lib/featureflow/events_client.rb
@@ -107,14 +115,18 @@ files:
107
115
  - lib/featureflow/polling_client.rb
108
116
  - lib/featureflow/rails/rails_client.rb
109
117
  - lib/featureflow/rails/railtie.rb
118
+ - lib/featureflow/user_builder.rb
110
119
  - lib/featureflow/version.rb
111
120
  - lib/generators/featureflow_generator.rb
112
- - test.rb
113
121
  homepage: https://github.com/featureflow/featureflow-ruby-sdk
114
122
  licenses:
115
- - MIT
116
- metadata: {}
117
- post_install_message:
123
+ - Apache-2.0
124
+ metadata:
125
+ homepage_uri: https://github.com/featureflow/featureflow-ruby-sdk
126
+ source_code_uri: https://github.com/featureflow/featureflow-ruby-sdk
127
+ changelog_uri: https://github.com/featureflow/featureflow-ruby-sdk/blob/master/CHANGELOG.md
128
+ bug_tracker_uri: https://github.com/featureflow/featureflow-ruby-sdk/issues
129
+ post_install_message:
118
130
  rdoc_options: []
119
131
  require_paths:
120
132
  - lib
@@ -122,16 +134,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
134
  requirements:
123
135
  - - ">="
124
136
  - !ruby/object:Gem::Version
125
- version: '0'
137
+ version: '3.1'
126
138
  required_rubygems_version: !ruby/object:Gem::Requirement
127
139
  requirements:
128
140
  - - ">="
129
141
  - !ruby/object:Gem::Version
130
142
  version: '0'
131
143
  requirements: []
132
- rubyforge_project:
133
- rubygems_version: 2.6.8
134
- signing_key:
144
+ rubygems_version: 3.5.22
145
+ signing_key:
135
146
  specification_version: 4
136
147
  summary: Ruby SDK for Featureflow
137
148
  test_files: []
data/.gitignore DELETED
@@ -1 +0,0 @@
1
- pkg
data/.rubocop.yml DELETED
@@ -1,5 +0,0 @@
1
- Metrics/LineLength:
2
- Max: 120
3
-
4
- Metrics/MethodLength:
5
- Max: 20
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in featureflow.gemspec
4
- gemspec
data/Gemfile.lock DELETED
@@ -1,53 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- featureflow (0.5.0)
5
- excon (~> 0.57.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- builder (3.2.3)
11
- cucumber (2.4.0)
12
- builder (>= 2.1.2)
13
- cucumber-core (~> 1.5.0)
14
- cucumber-wire (~> 0.0.1)
15
- diff-lcs (>= 1.1.3)
16
- gherkin (~> 4.0)
17
- multi_json (>= 1.7.5, < 2.0)
18
- multi_test (>= 0.1.2)
19
- cucumber-core (1.5.0)
20
- gherkin (~> 4.0)
21
- cucumber-wire (0.0.1)
22
- diff-lcs (1.3)
23
- excon (0.57.0)
24
- gherkin (4.1.3)
25
- multi_json (1.12.1)
26
- multi_test (0.1.2)
27
- rake (10.5.0)
28
- rspec (3.6.0)
29
- rspec-core (~> 3.6.0)
30
- rspec-expectations (~> 3.6.0)
31
- rspec-mocks (~> 3.6.0)
32
- rspec-core (3.6.0)
33
- rspec-support (~> 3.6.0)
34
- rspec-expectations (3.6.0)
35
- diff-lcs (>= 1.2.0, < 2.0)
36
- rspec-support (~> 3.6.0)
37
- rspec-mocks (3.6.0)
38
- diff-lcs (>= 1.2.0, < 2.0)
39
- rspec-support (~> 3.6.0)
40
- rspec-support (3.6.0)
41
-
42
- PLATFORMS
43
- ruby
44
-
45
- DEPENDENCIES
46
- bundler (~> 1.15)
47
- cucumber (~> 2.4.0)
48
- featureflow!
49
- rake (~> 10.0)
50
- rspec (~> 3.0)
51
-
52
- BUNDLED WITH
53
- 1.15.1
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
data/featureflow.gemspec DELETED
@@ -1,38 +0,0 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'featureflow/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "featureflow"
8
- spec.version = Featureflow::VERSION
9
- spec.authors = ["Henry Young"]
10
- spec.email = ["henry@featureflow.io"]
11
-
12
- spec.summary = "Ruby SDK for Featureflow"
13
- spec.description = "Connects your server with Featureflow. Create features at https://featureflow.io."
14
- spec.homepage = "https://github.com/featureflow/featureflow-ruby-sdk"
15
- spec.license = "MIT"
16
-
17
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
- # to allow pushing to a single host or delete this section to allow pushing to any host.
19
- # if spec.respond_to?(:metadata)
20
- # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
21
- # else
22
- # raise "RubyGems 2.0 or newer is required to protect against " \
23
- # "public gem pushes."
24
- # end
25
-
26
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
- f.match(%r{^(test|spec|features)/})
28
- end
29
- spec.bindir = "exe"
30
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
32
-
33
- spec.add_development_dependency "bundler", "~> 1.15"
34
- spec.add_development_dependency "rake", "~> 10.0"
35
- spec.add_development_dependency "rspec", "~> 3.0"
36
- spec.add_development_dependency "cucumber", "~> 2.4.0"
37
- spec.add_dependency "excon", "~> 0.57.0"
38
- end
@@ -1,34 +0,0 @@
1
- module Featureflow
2
- class ContextBuilder
3
- def initialize(key)
4
- raise ArgumentError, 'Parameter key must be a String' unless key.is_a?(String) && !key.empty?
5
- @context_key = key
6
- @values = {}
7
- self
8
- end
9
-
10
- def with_values(hash)
11
- raise ArgumentError, 'Parameter hash must be a Hash' unless hash.is_a?(Hash)
12
- hash = hash.dup
13
- hash.each do |k, v|
14
- raise ArgumentError, "Value for #{k} must be a valid 'primitive' JSON datatype" unless valid_value?(v)
15
- hash[k.to_s] = h.delete(k) unless k.is_a?(String)
16
- end
17
- @values = @values.merge(hash)
18
- self
19
- end
20
-
21
- def build
22
- {
23
- key: @context_key,
24
- values: @values
25
- }
26
- end
27
-
28
- private def valid_value?(values)
29
- Array(values).all? do |v|
30
- [String, Numeric, TrueClass, FalseClass].any? { |type| v.is_a?(type) }
31
- end
32
- end
33
- end
34
- end
data/test.rb DELETED
@@ -1,21 +0,0 @@
1
- $:.unshift(File.dirname(__FILE__) + "/lib/")
2
-
3
- require 'featureflow'
4
- with_features = [
5
- Featureflow::Feature.create('default', 'variant')
6
- ]
7
-
8
-
9
-
10
- # api_key = 'srv-env-9b5fff890c724d119a334a64ed4d2eb2'
11
- api_key = 'srv-env-f472cfa8c2774ea2b3678fc5a3dbfe13'
12
- featureflow_client = Featureflow::Client.new(api_key: api_key, with_features: with_features, url: 'http://10.10.2.163:8081')
13
- context = Featureflow::ContextBuilder.new('user1').build
14
- puts('test-integration is on? ' + featureflow_client.evaluate('test-integration', context).on?.to_s)
15
- featureflow_client.evaluate('nooooo', context).on?
16
- featureflow_client.evaluate('default', context).on?
17
-
18
- #
19
- loop do
20
- sleep 1
21
- end