sdk-reforge 1.9.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 +7 -0
- data/.envrc.sample +3 -0
- data/.github/CODEOWNERS +2 -0
- data/.github/pull_request_template.md +8 -0
- data/.github/workflows/ruby.yml +48 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +257 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +182 -0
- data/LICENSE.txt +20 -0
- data/README.md +105 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/compile_protos.sh +20 -0
- data/dev/allocation_stats +60 -0
- data/dev/benchmark +40 -0
- data/dev/console +12 -0
- data/dev/script_setup.rb +18 -0
- data/lib/prefab_pb.rb +77 -0
- data/lib/reforge/caching_http_connection.rb +95 -0
- data/lib/reforge/client.rb +133 -0
- data/lib/reforge/config_client.rb +275 -0
- data/lib/reforge/config_client_presenter.rb +18 -0
- data/lib/reforge/config_loader.rb +67 -0
- data/lib/reforge/config_resolver.rb +84 -0
- data/lib/reforge/config_value_unwrapper.rb +123 -0
- data/lib/reforge/config_value_wrapper.rb +18 -0
- data/lib/reforge/context.rb +241 -0
- data/lib/reforge/context_shape.rb +20 -0
- data/lib/reforge/context_shape_aggregator.rb +70 -0
- data/lib/reforge/criteria_evaluator.rb +345 -0
- data/lib/reforge/duration.rb +58 -0
- data/lib/reforge/encryption.rb +65 -0
- data/lib/reforge/error.rb +6 -0
- data/lib/reforge/errors/env_var_parse_error.rb +11 -0
- data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
- data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
- data/lib/reforge/errors/missing_default_error.rb +13 -0
- data/lib/reforge/errors/missing_env_var_error.rb +11 -0
- data/lib/reforge/errors/uninitialized_error.rb +13 -0
- data/lib/reforge/evaluation.rb +53 -0
- data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
- data/lib/reforge/example_contexts_aggregator.rb +77 -0
- data/lib/reforge/exponential_backoff.rb +21 -0
- data/lib/reforge/feature_flag_client.rb +43 -0
- data/lib/reforge/fixed_size_hash.rb +14 -0
- data/lib/reforge/http_connection.rb +45 -0
- data/lib/reforge/internal_logger.rb +43 -0
- data/lib/reforge/javascript_stub.rb +99 -0
- data/lib/reforge/local_config_parser.rb +151 -0
- data/lib/reforge/murmer3.rb +50 -0
- data/lib/reforge/options.rb +191 -0
- data/lib/reforge/periodic_sync.rb +74 -0
- data/lib/reforge/prefab.rb +120 -0
- data/lib/reforge/rate_limit_cache.rb +41 -0
- data/lib/reforge/resolved_config_presenter.rb +86 -0
- data/lib/reforge/semver.rb +132 -0
- data/lib/reforge/sse_config_client.rb +112 -0
- data/lib/reforge/time_helpers.rb +7 -0
- data/lib/reforge/weighted_value_resolver.rb +42 -0
- data/lib/reforge/yaml_config_parser.rb +34 -0
- data/lib/reforge-sdk.rb +57 -0
- data/test/fixtures/datafile.json +87 -0
- data/test/integration_test.rb +171 -0
- data/test/integration_test_helpers.rb +114 -0
- data/test/support/common_helpers.rb +201 -0
- data/test/support/mock_base_client.rb +41 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_caching_http_connection.rb +218 -0
- data/test/test_client.rb +351 -0
- data/test/test_config_client.rb +84 -0
- data/test/test_config_loader.rb +82 -0
- data/test/test_config_resolver.rb +502 -0
- data/test/test_config_value_unwrapper.rb +270 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +271 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +150 -0
- data/test/test_criteria_evaluator.rb +1180 -0
- data/test/test_duration.rb +37 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +233 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +16 -0
- data/test/test_fixed_size_hash.rb +119 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +75 -0
- data/test/test_internal_logger.rb +25 -0
- data/test/test_javascript_stub.rb +176 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +93 -0
- data/test/test_prefab.rb +16 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_semver.rb +108 -0
- data/test/test_sse_config_client.rb +211 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- metadata +345 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class Duration
|
5
|
+
PATTERN = /P(?:(?<days>\d+(?:\.\d+)?)D)?(?:T(?:(?<hours>\d+(?:\.\d+)?)H)?(?:(?<minutes>\d+(?:\.\d+)?)M)?(?:(?<seconds>\d+(?:\.\d+)?)S)?)?/
|
6
|
+
MINUTES_IN_SECONDS = 60
|
7
|
+
HOURS_IN_SECONDS = 60 * MINUTES_IN_SECONDS
|
8
|
+
DAYS_IN_SECONDS = 24 * HOURS_IN_SECONDS
|
9
|
+
|
10
|
+
def initialize(definition)
|
11
|
+
@seconds = self.class.parse(definition)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.parse(definition)
|
15
|
+
match = PATTERN.match(definition)
|
16
|
+
return 0 unless match
|
17
|
+
|
18
|
+
days = match[:days]&.to_f || 0
|
19
|
+
hours = match[:hours]&.to_f || 0
|
20
|
+
minutes = match[:minutes]&.to_f || 0
|
21
|
+
seconds = match[:seconds]&.to_f || 0
|
22
|
+
|
23
|
+
(days * DAYS_IN_SECONDS + hours * HOURS_IN_SECONDS + minutes * MINUTES_IN_SECONDS + seconds)
|
24
|
+
end
|
25
|
+
|
26
|
+
def in_seconds
|
27
|
+
@seconds
|
28
|
+
end
|
29
|
+
|
30
|
+
def in_minutes
|
31
|
+
in_seconds / 60.0
|
32
|
+
end
|
33
|
+
|
34
|
+
def in_hours
|
35
|
+
in_minutes / 60.0
|
36
|
+
end
|
37
|
+
|
38
|
+
def in_days
|
39
|
+
in_hours / 24.0
|
40
|
+
end
|
41
|
+
|
42
|
+
def in_weeks
|
43
|
+
in_days / 7.0
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_i
|
47
|
+
in_seconds.to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_f
|
51
|
+
in_seconds.to_f
|
52
|
+
end
|
53
|
+
|
54
|
+
def as_json
|
55
|
+
{ ms: in_seconds * 1000, seconds: in_seconds }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class Encryption
|
5
|
+
CIPHER_TYPE = "aes-256-gcm" # 32/12
|
6
|
+
SEPARATOR = "--"
|
7
|
+
|
8
|
+
# Hexadecimal format ensures that generated keys are representable with
|
9
|
+
# plain text
|
10
|
+
#
|
11
|
+
# To convert back to the original string with the desired length:
|
12
|
+
# [ value ].pack("H*")
|
13
|
+
def self.generate_new_hex_key
|
14
|
+
generate_random_key.unpack("H*")[0]
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(key_string_hex)
|
18
|
+
@key = [key_string_hex].pack("H*")
|
19
|
+
end
|
20
|
+
|
21
|
+
def encrypt(clear_text)
|
22
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
23
|
+
cipher.encrypt
|
24
|
+
iv = cipher.random_iv
|
25
|
+
|
26
|
+
# load them into the cipher
|
27
|
+
cipher.key = @key
|
28
|
+
cipher.iv = iv
|
29
|
+
cipher.auth_data = ""
|
30
|
+
|
31
|
+
# encrypt the message
|
32
|
+
encrypted = cipher.update(clear_text)
|
33
|
+
encrypted << cipher.final
|
34
|
+
tag = cipher.auth_tag
|
35
|
+
|
36
|
+
# pack and join
|
37
|
+
[encrypted, iv, tag].map { |p| p.unpack("H*")[0] }.join(SEPARATOR)
|
38
|
+
end
|
39
|
+
|
40
|
+
def decrypt(encrypted_string)
|
41
|
+
unpacked_parts = encrypted_string.split(SEPARATOR).map { |p| [p].pack("H*") }
|
42
|
+
|
43
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
44
|
+
cipher.decrypt
|
45
|
+
cipher.key = @key
|
46
|
+
cipher.iv = unpacked_parts[1]
|
47
|
+
cipher.auth_tag = unpacked_parts[2]
|
48
|
+
|
49
|
+
# and decrypt it
|
50
|
+
decrypted = cipher.update(unpacked_parts[0])
|
51
|
+
decrypted << cipher.final
|
52
|
+
decrypted
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def self.generate_random_key
|
58
|
+
SecureRandom.random_bytes(key_length)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.key_length
|
62
|
+
OpenSSL::Cipher.new(CIPHER_TYPE).key_len
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
module Errors
|
5
|
+
class EnvVarParseError < Reforge::Error
|
6
|
+
def initialize(env_var, config, env_var_name)
|
7
|
+
super("Evaluating #{config.key} couldn't coerce #{env_var_name} of #{env_var} to #{config.value_type}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
module Errors
|
5
|
+
class InitializationTimeoutError < Reforge::Error
|
6
|
+
def initialize(timeout_sec, key)
|
7
|
+
message = "Reforge couldn't initialize in #{timeout_sec} second timeout. Trying to fetch key `#{key}`."
|
8
|
+
super(message)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
module Errors
|
5
|
+
class InvalidSdkKeyError < Reforge::Error
|
6
|
+
def initialize(key)
|
7
|
+
if key.nil? || key.empty?
|
8
|
+
message = 'No SDK key. Set REFORGE_SDK_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY'
|
9
|
+
|
10
|
+
super(message)
|
11
|
+
else
|
12
|
+
message = "Your SDK key format is invalid. Expecting something like 123-development-yourapikey-SDK. You provided `#{key}`"
|
13
|
+
|
14
|
+
super(message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
module Errors
|
5
|
+
class MissingDefaultError < Reforge::Error
|
6
|
+
def initialize(key)
|
7
|
+
message = "No value found for key '#{key}' and no default was provided.\n\nIf you'd prefer returning `nil` rather than raising when this occurs, modify the `on_no_default` value you provide in your Reforge::Options."
|
8
|
+
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
# Records the result of evaluating a config's criteria and forensics for reporting
|
5
|
+
class Evaluation
|
6
|
+
attr_reader :value, :context
|
7
|
+
|
8
|
+
def initialize(config:, value:, value_index:, config_row_index:, context:, resolver:)
|
9
|
+
@config = config
|
10
|
+
@value = value
|
11
|
+
@value_index = value_index
|
12
|
+
@config_row_index = config_row_index
|
13
|
+
@context = context
|
14
|
+
@resolver = resolver
|
15
|
+
end
|
16
|
+
|
17
|
+
def unwrapped_value
|
18
|
+
deepest_value.unwrap
|
19
|
+
end
|
20
|
+
|
21
|
+
def reportable_value
|
22
|
+
deepest_value.reportable_value
|
23
|
+
end
|
24
|
+
|
25
|
+
def report_and_return(evaluation_summary_aggregator)
|
26
|
+
report(evaluation_summary_aggregator)
|
27
|
+
|
28
|
+
unwrapped_value
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def report(evaluation_summary_aggregator)
|
34
|
+
return if @config.config_type == :LOG_LEVEL
|
35
|
+
|
36
|
+
evaluation_summary_aggregator&.record(
|
37
|
+
config_key: @config.key,
|
38
|
+
config_type: @config.config_type,
|
39
|
+
counter: {
|
40
|
+
config_id: @config.id,
|
41
|
+
config_row_index: @config_row_index,
|
42
|
+
conditional_value_index: @value_index,
|
43
|
+
selected_value: deepest_value.reportable_wrapped_value,
|
44
|
+
weighted_value_index: deepest_value.weighted_value_index,
|
45
|
+
selected_index: nil # TODO
|
46
|
+
})
|
47
|
+
end
|
48
|
+
|
49
|
+
def deepest_value
|
50
|
+
@deepest_value ||= Reforge::ConfigValueUnwrapper.deepest_value(@value, @config, @context, @resolver)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'periodic_sync'
|
4
|
+
|
5
|
+
module Reforge
|
6
|
+
# This class aggregates the number of times each config is evaluated, and
|
7
|
+
# details about how the config is evaluated This data is reported to the
|
8
|
+
# server at a regular interval defined by `sync_interval`.
|
9
|
+
class EvaluationSummaryAggregator
|
10
|
+
include Reforge::PeriodicSync
|
11
|
+
LOG = Reforge::InternalLogger.new(self)
|
12
|
+
|
13
|
+
attr_reader :data
|
14
|
+
|
15
|
+
def initialize(client:, max_keys:, sync_interval:)
|
16
|
+
@client = client
|
17
|
+
@max_keys = max_keys
|
18
|
+
@name = 'evaluation_summary_aggregator'
|
19
|
+
|
20
|
+
@data = Concurrent::Hash.new
|
21
|
+
|
22
|
+
start_periodic_sync(sync_interval)
|
23
|
+
end
|
24
|
+
|
25
|
+
def record(config_key:, config_type:, counter:)
|
26
|
+
return if @data.size >= @max_keys
|
27
|
+
|
28
|
+
key = [config_key, config_type]
|
29
|
+
@data[key] ||= Concurrent::Hash.new
|
30
|
+
|
31
|
+
@data[key][counter] ||= 0
|
32
|
+
@data[key][counter] += 1
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def counter_proto(counter, count)
|
38
|
+
PrefabProto::ConfigEvaluationCounter.new(
|
39
|
+
config_id: counter[:config_id],
|
40
|
+
selected_index: counter[:selected_index],
|
41
|
+
config_row_index: counter[:config_row_index],
|
42
|
+
conditional_value_index: counter[:conditional_value_index],
|
43
|
+
weighted_value_index: counter[:weighted_value_index],
|
44
|
+
selected_value: counter[:selected_value],
|
45
|
+
count: count
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def flush(to_ship, start_at_was)
|
50
|
+
pool.post do
|
51
|
+
LOG.debug "Flushing #{to_ship.size} summaries"
|
52
|
+
|
53
|
+
summaries_proto = PrefabProto::ConfigEvaluationSummaries.new(
|
54
|
+
start: start_at_was,
|
55
|
+
end: Reforge::TimeHelpers.now_in_ms,
|
56
|
+
summaries: summaries(to_ship)
|
57
|
+
)
|
58
|
+
|
59
|
+
result = post('/api/v1/telemetry', events(summaries_proto))
|
60
|
+
|
61
|
+
LOG.debug "Uploaded #{to_ship.size} summaries: #{result.status}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def events(summaries)
|
66
|
+
event = PrefabProto::TelemetryEvent.new(summaries: summaries)
|
67
|
+
|
68
|
+
PrefabProto::TelemetryEvents.new(
|
69
|
+
instance_hash: @client.instance_hash,
|
70
|
+
events: [event]
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def summaries(data)
|
75
|
+
data.map do |(config_key, config_type), counters|
|
76
|
+
counter_protos = counters.map { |counter, count| counter_proto(counter, count) }
|
77
|
+
|
78
|
+
PrefabProto::ConfigEvaluationSummary.new(
|
79
|
+
key: config_key,
|
80
|
+
type: config_type,
|
81
|
+
counters: counter_protos
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'periodic_sync'
|
4
|
+
|
5
|
+
module Reforge
|
6
|
+
# This class aggregates example contexts. It dedupes based on the
|
7
|
+
# concatenation of the keys of the contexts.
|
8
|
+
#
|
9
|
+
# It shouldn't send the same context more than once per hour.
|
10
|
+
class ExampleContextsAggregator
|
11
|
+
include Reforge::PeriodicSync
|
12
|
+
LOG = Reforge::InternalLogger.new(self)
|
13
|
+
|
14
|
+
attr_reader :data, :cache
|
15
|
+
|
16
|
+
ONE_HOUR = 60 * 60
|
17
|
+
|
18
|
+
def initialize(client:, max_contexts:, sync_interval:)
|
19
|
+
@client = client
|
20
|
+
@max_contexts = max_contexts
|
21
|
+
@name = 'example_contexts_aggregator'
|
22
|
+
|
23
|
+
@data = Concurrent::Array.new
|
24
|
+
@cache = Reforge::RateLimitCache.new(ONE_HOUR)
|
25
|
+
|
26
|
+
start_periodic_sync(sync_interval)
|
27
|
+
end
|
28
|
+
|
29
|
+
def record(contexts)
|
30
|
+
key = contexts.grouped_key
|
31
|
+
|
32
|
+
return unless @data.size < @max_contexts && !@cache.fresh?(key)
|
33
|
+
|
34
|
+
@cache.set(key)
|
35
|
+
|
36
|
+
@data.push(contexts)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def on_prepare_data
|
42
|
+
@cache.prune
|
43
|
+
end
|
44
|
+
|
45
|
+
def flush(to_ship, _)
|
46
|
+
pool.post do
|
47
|
+
LOG.debug "Flushing #{to_ship.size} examples"
|
48
|
+
|
49
|
+
result = post('/api/v1/telemetry', events(to_ship))
|
50
|
+
|
51
|
+
LOG.debug "Uploaded #{to_ship.size} examples: #{result.status}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def example_contexts(to_ship)
|
56
|
+
to_ship.map do |contexts|
|
57
|
+
PrefabProto::ExampleContext.new(
|
58
|
+
timestamp: contexts.seen_at * 1000,
|
59
|
+
contextSet: contexts.slim_proto
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def events(to_ship)
|
65
|
+
event = PrefabProto::TelemetryEvent.new(
|
66
|
+
example_contexts: PrefabProto::ExampleContexts.new(
|
67
|
+
examples: example_contexts(to_ship)
|
68
|
+
)
|
69
|
+
)
|
70
|
+
|
71
|
+
PrefabProto::TelemetryEvents.new(
|
72
|
+
instance_hash: @client.instance_hash,
|
73
|
+
events: [event]
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
# This class implements exponential backoff with a maximum delay.
|
5
|
+
#
|
6
|
+
# This is the default sync interval for aggregators.
|
7
|
+
class ExponentialBackoff
|
8
|
+
def initialize(max_delay:, initial_delay: 2, multiplier: 2)
|
9
|
+
@initial_delay = initial_delay
|
10
|
+
@max_delay = max_delay
|
11
|
+
@multiplier = multiplier
|
12
|
+
@delay = initial_delay
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
delay = @delay
|
17
|
+
@delay = [@delay * @multiplier, @max_delay].min
|
18
|
+
delay
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class FeatureFlagClient
|
5
|
+
LOG = Reforge::InternalLogger.new(self)
|
6
|
+
def initialize(base_client)
|
7
|
+
@base_client = base_client
|
8
|
+
end
|
9
|
+
|
10
|
+
def feature_is_on?(feature_name)
|
11
|
+
feature_is_on_for?(feature_name, {})
|
12
|
+
end
|
13
|
+
|
14
|
+
def feature_is_on_for?(feature_name, properties)
|
15
|
+
variant = @base_client.config_client.get(feature_name, false, properties)
|
16
|
+
|
17
|
+
is_on?(variant)
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(feature_name, properties, default: false)
|
21
|
+
value = _get(feature_name, properties)
|
22
|
+
|
23
|
+
value.nil? ? default : value
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def _get(feature_name, properties)
|
29
|
+
@base_client.config_client.get(feature_name, nil, properties)
|
30
|
+
end
|
31
|
+
|
32
|
+
def is_on?(variant)
|
33
|
+
return false if variant.nil?
|
34
|
+
|
35
|
+
return variant if variant == !!variant
|
36
|
+
|
37
|
+
variant.bool
|
38
|
+
rescue StandardError
|
39
|
+
LOG.info("is_on? methods only work for boolean feature flags variants. This feature flags variant is '#{variant}'. Returning false")
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Reforge
|
3
|
+
class FixedSizeHash < Hash
|
4
|
+
def initialize(max_size)
|
5
|
+
@max_size = max_size
|
6
|
+
super()
|
7
|
+
end
|
8
|
+
|
9
|
+
def []=(key, value)
|
10
|
+
shift if size >= @max_size && !key?(key) # Only evict if adding a new key
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class HttpConnection
|
5
|
+
AUTH_USER = 'authuser'
|
6
|
+
PROTO_HEADERS = {
|
7
|
+
'Content-Type' => 'application/x-protobuf',
|
8
|
+
'Accept' => 'application/x-protobuf',
|
9
|
+
'X-Reforge-SDK-Version' => "sdk-ruby-#{Reforge::VERSION}"
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def initialize(uri, sdk_key)
|
13
|
+
@uri = uri
|
14
|
+
@sdk_key = sdk_key
|
15
|
+
end
|
16
|
+
|
17
|
+
def uri
|
18
|
+
@uri
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(path, headers = {})
|
22
|
+
connection(PROTO_HEADERS.merge(headers)).get(path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def post(path, body)
|
26
|
+
connection(PROTO_HEADERS).post(path, body.to_proto)
|
27
|
+
end
|
28
|
+
|
29
|
+
def connection(headers = {})
|
30
|
+
if Faraday::VERSION[0].to_i >= 2
|
31
|
+
Faraday.new(@uri) do |conn|
|
32
|
+
conn.request :authorization, :basic, AUTH_USER, @sdk_key
|
33
|
+
|
34
|
+
conn.headers.merge!(headers)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
Faraday.new(@uri) do |conn|
|
38
|
+
conn.request :basic_auth, AUTH_USER, @sdk_key
|
39
|
+
|
40
|
+
conn.headers.merge!(headers)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Reforge
|
2
|
+
class InternalLogger < SemanticLogger::Logger
|
3
|
+
|
4
|
+
def initialize(klass)
|
5
|
+
default_level = ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].downcase.to_sym : :warn
|
6
|
+
super(klass, default_level)
|
7
|
+
instances << self
|
8
|
+
end
|
9
|
+
|
10
|
+
def log(log, message = nil, progname = nil, &block)
|
11
|
+
return if recurse_check[local_log_id]
|
12
|
+
recurse_check[local_log_id] = true
|
13
|
+
begin
|
14
|
+
super(log, message, progname, &block)
|
15
|
+
ensure
|
16
|
+
recurse_check[local_log_id] = false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def local_log_id
|
21
|
+
Thread.current.__id__
|
22
|
+
end
|
23
|
+
|
24
|
+
# Our client outputs debug logging,
|
25
|
+
# but if you aren't using Reforge logging this could be too chatty.
|
26
|
+
# If you aren't using reforge log filter, only log warn level and above
|
27
|
+
def self.using_reforge_log_filter!
|
28
|
+
@@instances.each do |l|
|
29
|
+
l.level = :trace
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def instances
|
36
|
+
@@instances ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
def recurse_check
|
40
|
+
@recurse_check ||=Concurrent::Map.new(initial_capacity: 2)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|