eppo-server-sdk 0.3.0 → 3.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.
@@ -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