eppo-server-sdk 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ce6db52c824656217a079b75231b2749b10bbbd656bda249e2f103666b173acb
4
+ data.tar.gz: 4629ce299c56cbcbfe342e7572803c935266172b376ef1c8aa1eac8f0bb8074b
5
+ SHA512:
6
+ metadata.gz: 29de3064e5d780257e0e883295a6101b86e321ba6203dcbcc21e46192cc853156faab52031782b323be2cf37fe6e045345a5099acdb3ead7f6383fbcf5396f2c
7
+ data.tar.gz: c3a669f35584ecac1b4f12e4796b61de972bb2f117cfd95e5cd3c693fb9f53af3bf232d9800a3a519245408576978037371394ff4b9931655850f38b80e8f0bb
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'custom_errors'
4
+
5
+ module EppoClient
6
+ # The base assignment logger class to override
7
+ class AssignmentLogger
8
+ def log_assignment(_assignment)
9
+ raise(EppoClient::AssignmentLoggerError, 'log_assignment has not been set up')
10
+ end
11
+ end
12
+ end
data/lib/client.rb ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'time'
5
+
6
+ require 'validation'
7
+ require 'rules'
8
+ require 'shard'
9
+ require 'sdk_logger'
10
+ require 'custom_errors'
11
+
12
+ module EppoClient
13
+ # The main client singleton
14
+ class Client
15
+ include Singleton
16
+ attr_accessor :config_requestor, :assignment_logger, :poller
17
+
18
+ def instance
19
+ Client.instance
20
+ end
21
+
22
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
23
+ def get_assignment(subject_key, flag_or_experiment_key, subject_attributes = {})
24
+ EppoClient.validate_not_blank('subject_key', subject_key)
25
+ EppoClient.validate_not_blank('flag_or_experiment_key', flag_or_experiment_key)
26
+ experiment_config = @config_requestor.get_configuration(flag_or_experiment_key)
27
+ override = get_subject_variation_override(experiment_config, subject_key)
28
+ return override unless override.nil?
29
+
30
+ if experiment_config.nil? || experiment_config.enabled == false
31
+ EppoClient.logger('out').info(
32
+ "[Eppo SDK] No assigned variation. No active experiment or flag for key: #{flag_or_experiment_key}"
33
+ )
34
+ return nil
35
+ end
36
+
37
+ matched_rule = EppoClient.find_matching_rule(subject_attributes, experiment_config.rules)
38
+ if matched_rule.nil?
39
+ EppoClient.logger('out').info(
40
+ "[Eppo SDK] No assigned variation. Subject attributes do not match targeting rules: #{subject_attributes}"
41
+ )
42
+ return nil
43
+ end
44
+
45
+ allocation = experiment_config.allocations[matched_rule.allocation_key]
46
+ unless in_experiment_sample?(
47
+ subject_key,
48
+ flag_or_experiment_key,
49
+ experiment_config.subject_shards,
50
+ allocation.percent_exposure
51
+ )
52
+ EppoClient.logger('out').info(
53
+ '[Eppo SDK] No assigned variation. Subject is not part of experiment sample population'
54
+ )
55
+ return nil
56
+ end
57
+
58
+ shard = EppoClient.get_shard("assignment-#{subject_key}-#{flag_or_experiment_key}", experiment_config.subject_shards)
59
+ assigned_variation = allocation.variations.find { |var| var.shard_range.shard_in_range?(shard) }.value
60
+
61
+ assignment_event = {
62
+ "experiment": flag_or_experiment_key,
63
+ "variation": assigned_variation,
64
+ "subject": subject_key,
65
+ "timestamp": Time.now.utc.iso8601,
66
+ "subjectAttributes": subject_attributes
67
+ }
68
+
69
+ begin
70
+ @assignment_logger.log_assignment(assignment_event)
71
+ rescue EppoClient::AssignmentLoggerError => e
72
+ # This error means that log_assignment was not set up. This is okay to ignore.
73
+ rescue StandardError => e
74
+ EppoClient.logger('err').info("[Eppo SDK] Error logging assignment event: #{e}")
75
+ end
76
+
77
+ assigned_variation
78
+ end
79
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
80
+
81
+ def shutdown
82
+ @poller.stop
83
+ end
84
+
85
+ def get_subject_variation_override(experiment_config, subject)
86
+ subject_hash = Digest::MD5.hexdigest(subject.to_s)
87
+ experiment_config&.overrides && experiment_config.overrides[subject_hash]
88
+ end
89
+
90
+ def in_experiment_sample?(subject, experiment_key, subject_shards, percent_exposure)
91
+ shard = EppoClient.get_shard("exposure-#{subject}-#{experiment_key}", subject_shards)
92
+ shard <= percent_exposure * subject_shards
93
+ end
94
+ end
95
+ end
data/lib/config.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'validation'
4
+ require 'assignment_logger'
5
+
6
+ module EppoClient
7
+ # The class for configuring the Eppo client singleton
8
+ class Config
9
+ attr_reader :api_key, :assignment_logger, :base_url
10
+
11
+ def initialize(api_key, assignment_logger: AssignmentLogger.new, base_url: 'https://eppo.cloud/api')
12
+ @api_key = api_key
13
+ @assignment_logger = assignment_logger
14
+ @base_url = base_url
15
+ end
16
+
17
+ def validate
18
+ EppoClient.validate_not_blank('api_key', @api_key)
19
+ end
20
+
21
+ # Hide instance variables (specifically api_key) from logs
22
+ def inspect
23
+ "#<EppoClient::Config:#{object_id}>"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sdk_logger'
4
+ require 'custom_errors'
5
+ require 'constants'
6
+
7
+ module EppoClient
8
+ # A class for the variation object
9
+ class VariationDto
10
+ attr_reader :name, :value, :shard_range
11
+
12
+ def initialize(name, value, shard_range)
13
+ @name = name
14
+ @value = value
15
+ @shard_range = shard_range
16
+ end
17
+ end
18
+
19
+ # A class for the allocation object
20
+ class AllocationDto
21
+ attr_reader :percent_exposure, :variations
22
+
23
+ def initialize(percent_exposure, variations)
24
+ @percent_exposure = percent_exposure
25
+ @variations = variations
26
+ end
27
+ end
28
+
29
+ # A class for the experiment configuration object
30
+ class ExperimentConfigurationDto
31
+ attr_reader :subject_shards, :enabled, :name, :overrides, :rules, :allocations
32
+
33
+ def initialize(exp_config)
34
+ @subject_shards = exp_config['subjectShards']
35
+ @enabled = exp_config['enabled']
36
+ @name = exp_config['name'] || nil
37
+ @overrides = exp_config['overrides'] || {}
38
+ @rules = exp_config['rules'] || []
39
+ @allocations = exp_config['allocations']
40
+ end
41
+ end
42
+
43
+ # A class for getting exp configs from the local cache or API
44
+ class ExperimentConfigurationRequestor
45
+
46
+ attr_reader :config_store
47
+
48
+ def initialize(http_client, config_store)
49
+ @http_client = http_client
50
+ @config_store = config_store
51
+ end
52
+
53
+ def get_configuration(experiment_key)
54
+ @http_client.is_unauthorized && raise(EppoClient::UnauthorizedError, 'please check your API key')
55
+ @config_store.retrieve_configuration(experiment_key)
56
+ end
57
+
58
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
59
+ def fetch_and_store_configurations
60
+ configs = {}
61
+ begin
62
+ exp_configs = @http_client.get(EppoClient::RAC_ENDPOINT).fetch('flags', {})
63
+ exp_configs.each do |exp_key, exp_config|
64
+ exp_config['allocations'].each do |k, v|
65
+ exp_config['allocations'][k] = EppoClient::AllocationDto.new(
66
+ v['percentExposure'],
67
+ v['variations'].map do |var|
68
+ EppoClient::VariationDto.new(
69
+ var['name'],
70
+ var['value'],
71
+ EppoClient::ShardRange.new(var['shardRange']['start'], var['shardRange']['end'])
72
+ )
73
+ end
74
+ )
75
+ end
76
+ exp_config['rules'] = exp_config['rules'].map do |rule|
77
+ EppoClient::Rule.new(
78
+ conditions: rule['conditions'].map do |condition|
79
+ EppoClient::Condition.new(
80
+ value: condition['value'],
81
+ operator: condition['operator'],
82
+ attribute: condition['attribute']
83
+ )
84
+ end,
85
+ allocation_key: rule['allocationKey']
86
+ )
87
+ end
88
+ configs[exp_key] = EppoClient::ExperimentConfigurationDto.new(exp_config)
89
+ end
90
+ @config_store.assign_configurations(configs)
91
+ rescue EppoClient::HttpRequestError => e
92
+ EppoClient.logger('err').error("Error retrieving assignment configurations: #{e}")
93
+ end
94
+ configs
95
+ end
96
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
97
+ end
98
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/atomic/read_write_lock'
4
+
5
+ require 'lru_cache'
6
+
7
+ module EppoClient
8
+ # A thread safe store for the configurations to ensure that retrievals pull from a single source of truth
9
+ class ConfigurationStore
10
+ attr_reader :lock, :cache
11
+
12
+ def initialize(max_size)
13
+ @cache = EppoClient::LRUCache.new(max_size)
14
+ @lock = Concurrent::ReadWriteLock.new
15
+ end
16
+
17
+ def retrieve_configuration(key)
18
+ @lock.with_read_lock { @cache[key] }
19
+ end
20
+
21
+ def assign_configurations(configs)
22
+ @lock.with_write_lock do
23
+ configs.each do |key, config|
24
+ @cache[key] = config
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/constants.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EppoClient
4
+ # configuration cache constants
5
+ MAX_CACHE_ENTRIES = 1000 # arbitrary; the caching library requires a max limit
6
+
7
+ # poller constants
8
+ SECOND_MILLIS = 1000
9
+ MINUTE_MILLIS = 60 * SECOND_MILLIS
10
+ POLL_JITTER_MILLIS = 30 * SECOND_MILLIS
11
+ POLL_INTERVAL_MILLIS = 5 * MINUTE_MILLIS
12
+
13
+ # the configs endpoint
14
+ RAC_ENDPOINT = 'randomized_assignment/v2/config'
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EppoClient
4
+ # A custom error class for AssignmentLogger
5
+ class AssignmentLoggerError < StandardError
6
+ def initialize(message)
7
+ super("AssignmentLoggerError: #{message}")
8
+ end
9
+ end
10
+
11
+ # A custom error class for unauthorized requests
12
+ class UnauthorizedError < StandardError
13
+ def initialize(message)
14
+ super("Unauthorized: #{message}")
15
+ end
16
+ end
17
+
18
+ # A custom error class for HTTP requests
19
+ class HttpRequestError < StandardError
20
+ attr_reader :status_code
21
+
22
+ def initialize(message, status_code)
23
+ @status_code = status_code
24
+ super("HttpRequestError: #{message}")
25
+ end
26
+ end
27
+
28
+ # A custom error class for invalid values
29
+ class InvalidValueError < StandardError
30
+ def initialize(message)
31
+ super("InvalidValueError: #{message}")
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'assignment_logger'
4
+ require 'http_client'
5
+ require 'poller'
6
+ require 'config'
7
+ require 'client'
8
+ require 'constants'
9
+ require 'configuration_requestor'
10
+ require 'configuration_store'
11
+
12
+ # This module scopes all the client SDK's classes and functions
13
+ module EppoClient
14
+ @sdk_version = '1.1.1'
15
+
16
+ # rubocop:disable Metrics/MethodLength
17
+ def initialize_client(config_requestor, assignment_logger)
18
+ client = EppoClient::Client.instance
19
+ !client.poller.nil? && client.shutdown
20
+ client.config_requestor = config_requestor
21
+ client.assignment_logger = assignment_logger
22
+ client.poller = EppoClient::Poller.new(
23
+ EppoClient::POLL_INTERVAL_MILLIS,
24
+ EppoClient::POLL_JITTER_MILLIS,
25
+ proc { client.config_requestor.fetch_and_store_configurations }
26
+ )
27
+ client.poller.start
28
+ client
29
+ end
30
+ # rubocop:enable Metrics/MethodLength
31
+
32
+ def init(config)
33
+ config.validate
34
+ sdk_params = EppoClient::SdkParams.new(config.api_key, 'ruby', @sdk_version)
35
+ http_client = EppoClient::HttpClient.new(config.base_url, sdk_params.formatted)
36
+ config_store = EppoClient::ConfigurationStore.new(EppoClient::MAX_CACHE_ENTRIES)
37
+ config_store.lock.with_write_lock do
38
+ EppoClient.initialize_client(
39
+ EppoClient::ExperimentConfigurationRequestor.new(http_client, config_store),
40
+ config.assignment_logger
41
+ )
42
+ end
43
+ end
44
+
45
+ module_function :init, :initialize_client
46
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+
6
+ require '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
data/lib/lru_cache.rb ADDED
@@ -0,0 +1,28 @@
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
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/poller.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/atom'
4
+ require 'sdk_logger'
5
+
6
+ # The poller
7
+ module EppoClient
8
+ # The poller class invokes a callback and waits on repeat on a separate thread
9
+ class Poller
10
+ def initialize(interval_millis, jitter_millis, callback)
11
+ @jitter_millis = jitter_millis
12
+ @interval = interval_millis
13
+ @stopped = Concurrent::Atom.new(false)
14
+ @callback = callback
15
+ @thread = nil
16
+ end
17
+
18
+ def start
19
+ @stopped.reset(false)
20
+ @thread = Thread.new { poll }
21
+ end
22
+
23
+ def stop
24
+ @stopped.reset(true)
25
+ Thread.kill(@thread)
26
+ end
27
+
28
+ def stopped?
29
+ @stopped.value
30
+ end
31
+
32
+ def poll
33
+ until stopped?
34
+ begin
35
+ @callback.call
36
+ rescue StandardError => e
37
+ EppoClient.logger('err').error("Unexpected error running poll task: #{e}")
38
+ break
39
+ end
40
+ _wait_for_interval
41
+ end
42
+ end
43
+
44
+ def _wait_for_interval
45
+ interval_with_jitter = @interval - rand(@jitter_millis)
46
+ sleep interval_with_jitter / 1000
47
+ end
48
+ end
49
+ end
data/lib/rules.rb ADDED
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The helper module for rules
4
+ module EppoClient
5
+ module OperatorType
6
+ MATCHES = 'MATCHES'
7
+ GTE = 'GTE'
8
+ GT = 'GT'
9
+ LTE = 'LTE'
10
+ LT = 'LT'
11
+ ONE_OF = 'ONE_OF'
12
+ NOT_ONE_OF = 'NOT_ONE_OF'
13
+ end
14
+
15
+ # A class for the Condition object
16
+ class Condition
17
+ attr_accessor :operator, :attribute, :value
18
+
19
+ def initialize(operator:, attribute:, value:)
20
+ @operator = operator
21
+ @attribute = attribute
22
+ @value = value
23
+ end
24
+ end
25
+
26
+ # A class for the Rule object
27
+ class Rule
28
+ attr_accessor :allocation_key, :conditions
29
+
30
+ def initialize(allocation_key:, conditions:)
31
+ @allocation_key = allocation_key
32
+ @conditions = conditions
33
+ end
34
+ end
35
+
36
+ def find_matching_rule(subject_attributes, rules)
37
+ rules.each do |rule|
38
+ return rule if matches_rule(subject_attributes, rule)
39
+ end
40
+ nil
41
+ end
42
+
43
+ def matches_rule(subject_attributes, rule)
44
+ rule.conditions.each do |condition|
45
+ return false unless evaluate_condition(subject_attributes, condition)
46
+ end
47
+ true
48
+ end
49
+
50
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
51
+ def evaluate_condition(subject_attributes, condition)
52
+ subject_value = subject_attributes[condition.attribute]
53
+ return false if subject_value.nil?
54
+
55
+ case condition.operator
56
+ when OperatorType::MATCHES
57
+ !!(Regexp.new(condition.value) =~ subject_value.to_s)
58
+ when OperatorType::ONE_OF
59
+ condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
60
+ when OperatorType::NOT_ONE_OF
61
+ !condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
62
+ else
63
+ subject_value.is_a?(Numeric) && evaluate_numeric_condition(subject_value, condition)
64
+ end
65
+ end
66
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
67
+
68
+ # rubocop:disable Metrics/MethodLength
69
+ def evaluate_numeric_condition(subject_value, condition)
70
+ case condition.operator
71
+ when OperatorType::GT
72
+ subject_value > condition.value
73
+ when OperatorType::GTE
74
+ subject_value >= condition.value
75
+ when OperatorType::LT
76
+ subject_value < condition.value
77
+ when OperatorType::LTE
78
+ subject_value <= condition.value
79
+ else
80
+ false
81
+ end
82
+ end
83
+ # rubocop:enable Metrics/MethodLength
84
+
85
+ module_function :find_matching_rule, :matches_rule, :evaluate_condition, :evaluate_numeric_condition
86
+ end
data/lib/sdk_logger.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # The helper module for logging
6
+ module EppoClient
7
+ @stdout_logger = Logger.new($stdout)
8
+ @stderr_logger = Logger.new($stderr)
9
+
10
+ def self.logger(type)
11
+ case type
12
+ when 'out'
13
+ @stdout_logger
14
+ when 'err'
15
+ @stderr_logger
16
+ else
17
+ @stderr_logger.error("[Eppo SDK] Invalid logger type: #{type}")
18
+ end
19
+ end
20
+ end
data/lib/shard.rb ADDED
@@ -0,0 +1,30 @@
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
+ def get_shard(input, subject_shards)
22
+ hash_output = Digest::MD5.hexdigest(input)
23
+ # get the first 4 bytes of the md5 hex string and parse it using base 16
24
+ # (8 hex characters represent 4 bytes, e.g. 0xffffffff represents the max 4-byte integer)
25
+ int_from_hash = hash_output[0...8].to_i(16)
26
+ int_from_hash % subject_shards
27
+ end
28
+
29
+ module_function :get_shard
30
+ end
data/lib/validation.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'custom_errors'
4
+
5
+ # The helper module to validate keys
6
+ module EppoClient
7
+ def validate_not_blank(field_name, field_value)
8
+ (field_value.nil? || field_value == '') && raise(EppoClient::InvalidValueError, "#{field_name} cannot be blank")
9
+ end
10
+
11
+ module_function :validate_not_blank
12
+ end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eppo-server-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Eppo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.9
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.9
33
+ - !ruby/object:Gem::Dependency
34
+ name: faraday
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.7'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.7.1
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '2.7'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.7.1
53
+ - !ruby/object:Gem::Dependency
54
+ name: faraday-retry
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '2.0'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.0.0
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '2.0'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 2.0.0
73
+ - !ruby/object:Gem::Dependency
74
+ name: rake
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '13.0'
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 13.0.6
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.0'
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 13.0.6
93
+ - !ruby/object:Gem::Dependency
94
+ name: rspec
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '3.12'
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.12.0
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.12'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 3.12.0
113
+ - !ruby/object:Gem::Dependency
114
+ name: rubocop
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.41'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '1.41'
127
+ - !ruby/object:Gem::Dependency
128
+ name: webmock
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '3.18'
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 3.18.1
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '3.18'
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: 3.18.1
147
+ description:
148
+ email: eppo-team@geteppo.com
149
+ executables: []
150
+ extensions: []
151
+ extra_rdoc_files: []
152
+ files:
153
+ - lib/assignment_logger.rb
154
+ - lib/client.rb
155
+ - lib/config.rb
156
+ - lib/configuration_requestor.rb
157
+ - lib/configuration_store.rb
158
+ - lib/constants.rb
159
+ - lib/custom_errors.rb
160
+ - lib/eppo_client.rb
161
+ - lib/http_client.rb
162
+ - lib/lru_cache.rb
163
+ - lib/poller.rb
164
+ - lib/rules.rb
165
+ - lib/sdk_logger.rb
166
+ - lib/shard.rb
167
+ - lib/validation.rb
168
+ homepage: https://github.com/Eppo-exp/ruby-sdk
169
+ licenses:
170
+ - MIT
171
+ metadata: {}
172
+ post_install_message:
173
+ rdoc_options: []
174
+ require_paths:
175
+ - lib
176
+ required_ruby_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: 3.1.2
181
+ required_rubygems_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ requirements: []
187
+ rubygems_version: 3.3.7
188
+ signing_key:
189
+ specification_version: 4
190
+ summary: Eppo SDK for Ruby
191
+ test_files: []