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.
- checksums.yaml +4 -4
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/Cargo.lock +1963 -0
- data/Cargo.toml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +22 -0
- data/Rakefile +53 -0
- data/Steepfile +27 -0
- data/ext/eppo_client/Cargo.toml +19 -0
- data/ext/eppo_client/build.rs +5 -0
- data/ext/eppo_client/extconf.rb +6 -0
- data/ext/eppo_client/src/client.rs +119 -0
- data/ext/eppo_client/src/lib.rs +26 -0
- data/lib/eppo_client/assignment_logger.rb +7 -3
- data/lib/eppo_client/client.rb +99 -196
- data/lib/eppo_client/config.rb +4 -4
- data/lib/eppo_client/custom_errors.rb +0 -17
- data/lib/eppo_client/validation.rb +2 -2
- data/lib/eppo_client/version.rb +1 -1
- data/lib/eppo_client.rb +7 -45
- data/sig/eppo_server_sdk.rbs +96 -0
- metadata +30 -176
- data/lib/eppo_client/configuration_requestor.rb +0 -108
- data/lib/eppo_client/configuration_store.rb +0 -35
- data/lib/eppo_client/constants.rb +0 -20
- data/lib/eppo_client/http_client.rb +0 -75
- data/lib/eppo_client/lru_cache.rb +0 -28
- data/lib/eppo_client/poller.rb +0 -48
- data/lib/eppo_client/rules.rb +0 -119
- data/lib/eppo_client/shard.rb +0 -30
- data/lib/eppo_client/variation_type.rb +0 -39
@@ -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
|
data/lib/eppo_client/poller.rb
DELETED
@@ -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
|
data/lib/eppo_client/rules.rb
DELETED
@@ -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
|
data/lib/eppo_client/shard.rb
DELETED
@@ -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
|