eppo-server-sdk 0.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'faraday'
4
- require 'faraday/retry'
5
-
6
- require_relative 'custom_errors'
7
-
8
- REQUEST_TIMEOUT_SECONDS = 2
9
- # This applies only to failed DNS lookups and connection timeouts,
10
- # never to requests where data has made it to the server.
11
- MAX_RETRIES = 3
12
-
13
- module EppoClient
14
- # The SDK params object
15
- class SdkParams
16
- attr_reader :api_key, :sdk_name, :sdk_version
17
-
18
- def initialize(api_key, sdk_name, sdk_version)
19
- @api_key = api_key
20
- @sdk_name = sdk_name
21
- @sdk_version = sdk_version
22
- end
23
-
24
- # attributes are camelCase because that's what the backend endpoint expects
25
- def formatted
26
- {
27
- 'apiKey' => api_key,
28
- 'sdkName' => sdk_name,
29
- 'sdkVersion' => sdk_version
30
- }
31
- end
32
-
33
- # Hide instance variables (specifically api_key) from logs
34
- def inspect
35
- "#<EppoClient::SdkParams:#{object_id}>"
36
- end
37
- end
38
-
39
- # The http request client with retry/timeout behavior
40
- class HttpClient
41
- attr_reader :is_unauthorized
42
-
43
- @retry_options = {
44
- max: MAX_RETRIES,
45
- interval: 0.05,
46
- interval_randomness: 0.5,
47
- backoff_factor: 2,
48
- exceptions: ['Timeout::Error']
49
- }
50
-
51
- def initialize(base_url, sdk_params)
52
- @base_url = base_url
53
- @sdk_params = sdk_params
54
- @is_unauthorized = false
55
- end
56
-
57
- def get(resource)
58
- conn = Faraday::Connection.new(@base_url, params: @sdk_params) do |f|
59
- f.request :retry, @retry_options
60
- end
61
- conn.options.timeout = REQUEST_TIMEOUT_SECONDS
62
- response = conn.get(resource)
63
- @is_unauthorized = response.status == 401
64
- raise get_http_error(response.status, resource) if response.status != 200
65
-
66
- JSON.parse(response.body)
67
- end
68
-
69
- private
70
-
71
- def get_http_error(status_code, resource)
72
- EppoClient::HttpRequestError.new("HTTP #{status_code} error while requesting resource #{resource}", status_code)
73
- end
74
- end
75
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module EppoClient
4
- # The LRU cache relies on the fact that Ruby's Hash class maintains insertion order. So deleting
5
- # and re-inserting a key-value pair on access moves the key to the last position. When an
6
- # entry is added and the cache is full, the first entry is removed.
7
- class LRUCache
8
- attr_reader :cache, :size
9
-
10
- # Creates a new LRUCache that can hold +size+ entries.
11
- def initialize(size)
12
- @size = size
13
- @cache = {}
14
- end
15
-
16
- # Returns the stored value for +key+ or +nil+ if no value was stored under the key.
17
- def [](key)
18
- (val = @cache.delete(key)).nil? ? nil : @cache[key] = val
19
- end
20
-
21
- # Stores the +value+ under the +key+.
22
- def []=(key, value)
23
- @cache.delete(key)
24
- @cache[key] = value
25
- @cache.shift if @cache.length > @size
26
- end
27
- end
28
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'concurrent/atom'
4
-
5
- # The poller
6
- module EppoClient
7
- # The poller class invokes a callback and waits on repeat on a separate thread
8
- class Poller
9
- def initialize(interval_millis, jitter_millis, callback)
10
- @jitter_millis = jitter_millis
11
- @interval = interval_millis
12
- @stopped = Concurrent::Atom.new(false)
13
- @callback = callback
14
- @thread = nil
15
- end
16
-
17
- def start
18
- @stopped.reset(false)
19
- @thread = Thread.new { poll }
20
- end
21
-
22
- def stop
23
- @stopped.reset(true)
24
- Thread.kill(@thread)
25
- end
26
-
27
- def stopped?
28
- @stopped.value
29
- end
30
-
31
- def poll
32
- until stopped?
33
- begin
34
- @callback.call
35
- rescue StandardError => e
36
- Logger.new($stdout).error("Unexpected error running poll task: #{e}")
37
- break
38
- end
39
- _wait_for_interval
40
- end
41
- end
42
-
43
- def _wait_for_interval
44
- interval_with_jitter = @interval - rand(@jitter_millis)
45
- sleep interval_with_jitter / 1000
46
- end
47
- end
48
- end
@@ -1,119 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'semver'
4
-
5
- # The helper module for rules
6
- module EppoClient
7
- module OperatorType
8
- MATCHES = 'MATCHES'
9
- GTE = 'GTE'
10
- GT = 'GT'
11
- LTE = 'LTE'
12
- LT = 'LT'
13
- ONE_OF = 'ONE_OF'
14
- NOT_ONE_OF = 'NOT_ONE_OF'
15
- end
16
-
17
- # A class for the Condition object
18
- class Condition
19
- attr_accessor :operator, :attribute, :value
20
-
21
- def initialize(operator:, attribute:, value:)
22
- @operator = operator
23
- @attribute = attribute
24
- @value = value
25
- end
26
- end
27
-
28
- # A class for the Rule object
29
- class Rule
30
- attr_accessor :allocation_key, :conditions
31
-
32
- def initialize(allocation_key:, conditions:)
33
- @allocation_key = allocation_key
34
- @conditions = conditions
35
- end
36
- end
37
-
38
- def find_matching_rule(subject_attributes, rules)
39
- rules.each do |rule|
40
- return rule if matches_rule(subject_attributes, rule)
41
- end
42
- nil
43
- end
44
-
45
- def matches_rule(subject_attributes, rule)
46
- rule.conditions.each do |condition|
47
- return false unless evaluate_condition(subject_attributes, condition)
48
- end
49
- true
50
- end
51
-
52
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
53
- def evaluate_condition(subject_attributes, condition)
54
- subject_value = subject_attributes[condition.attribute]
55
- return false if subject_value.nil?
56
-
57
- case condition.operator
58
- when OperatorType::MATCHES
59
- !!(Regexp.new(condition.value) =~ subject_value.to_s)
60
- when OperatorType::ONE_OF
61
- condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
62
- when OperatorType::NOT_ONE_OF
63
- !condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
64
- else
65
- # Numeric operator: value could be numeric or semver.
66
- if subject_value.is_a?(Numeric)
67
- evaluate_numeric_condition(subject_value, condition)
68
- elsif valid_semver?(subject_value)
69
- compare_semver(subject_value, condition.value, condition.operator)
70
- end
71
- end
72
- end
73
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
74
-
75
- # rubocop:disable Metrics/MethodLength
76
- def evaluate_numeric_condition(subject_value, condition)
77
- case condition.operator
78
- when OperatorType::GT
79
- subject_value > condition.value
80
- when OperatorType::GTE
81
- subject_value >= condition.value
82
- when OperatorType::LT
83
- subject_value < condition.value
84
- when OperatorType::LTE
85
- subject_value <= condition.value
86
- else
87
- false
88
- end
89
- end
90
- # rubocop:enable Metrics/MethodLength
91
-
92
- # rubocop:disable Metrics/MethodLength
93
- def compare_semver(attribute_value, condition_value, operator)
94
- unless valid_semver?(attribute_value) && valid_semver?(condition_value)
95
- return false
96
- end
97
-
98
- case operator
99
- when OperatorType::GT
100
- SemVer.parse(attribute_value) > SemVer.parse(condition_value)
101
- when OperatorType::GTE
102
- SemVer.parse(attribute_value) >= SemVer.parse(condition_value)
103
- when OperatorType::LT
104
- SemVer.parse(attribute_value) < SemVer.parse(condition_value)
105
- when OperatorType::LTE
106
- SemVer.parse(attribute_value) <= SemVer.parse(condition_value)
107
- else
108
- false
109
- end
110
- end
111
- # rubocop:enable Metrics/MethodLength
112
-
113
- def valid_semver?(string)
114
- !SemVer.parse(string).nil?
115
- end
116
-
117
- module_function :find_matching_rule, :matches_rule, :evaluate_condition,
118
- :evaluate_numeric_condition, :valid_semver?, :compare_semver
119
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'digest'
4
-
5
- # The helper module for shard logic
6
- module EppoClient
7
- # A class for checking if a shard is in a range
8
- class ShardRange
9
- attr_reader :start, :end
10
-
11
- def initialize(range_start, range_end)
12
- @start = range_start
13
- @end = range_end
14
- end
15
-
16
- def shard_in_range?(shard)
17
- shard >= @start && shard < @end
18
- end
19
- end
20
-
21
- module_function
22
-
23
- def get_shard(input, subject_shards)
24
- hash_output = Digest::MD5.hexdigest(input)
25
- # get the first 4 bytes of the md5 hex string and parse it using base 16
26
- # (8 hex characters represent 4 bytes, e.g. 0xffffffff represents the max 4-byte integer)
27
- int_from_hash = hash_output[0...8].to_i(16)
28
- int_from_hash % subject_shards
29
- end
30
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module EppoClient
6
- # The class for configuring the Eppo client singleton
7
- module VariationType
8
- STRING_TYPE = 'string'
9
- NUMERIC_TYPE = 'numeric'
10
- BOOLEAN_TYPE = 'boolean'
11
- JSON_TYPE = 'json'
12
-
13
- module_function
14
-
15
- # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
16
- def expected_type?(assigned_variation, expected_variation_type)
17
- case expected_variation_type
18
- when STRING_TYPE
19
- assigned_variation.typed_value.is_a?(String)
20
- when NUMERIC_TYPE
21
- assigned_variation.typed_value.is_a?(Numeric)
22
- when BOOLEAN_TYPE
23
- assigned_variation.typed_value.is_a?(TrueClass) ||
24
- assigned_variation.typed_value.is_a?(FalseClass)
25
- when JSON_TYPE
26
- begin
27
- parsed_json = JSON.parse(assigned_variation.value)
28
- JSON.dump(assigned_variation.typed_value)
29
- parsed_json == assigned_variation.typed_value
30
- rescue JSON::JSONError
31
- false
32
- end
33
- else
34
- false
35
- end
36
- end
37
- # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
38
- end
39
- end