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,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class Context
|
5
|
+
BLANK_CONTEXT_NAME = ''
|
6
|
+
|
7
|
+
class NamedContext
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(name, hash)
|
11
|
+
@name = name.to_s
|
12
|
+
@hash = hash.transform_keys(&:to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
@hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def key
|
20
|
+
"#{@name}:#{@hash['key']}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_proto
|
24
|
+
PrefabProto::Context.new(
|
25
|
+
type: name,
|
26
|
+
values: @hash.transform_values do |value|
|
27
|
+
ConfigValueWrapper.wrap(value)
|
28
|
+
end
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
THREAD_KEY = :prefab_context
|
34
|
+
attr_reader :contexts, :seen_at, :id, :parent
|
35
|
+
|
36
|
+
class << self
|
37
|
+
def global_context=(context)
|
38
|
+
@global_context = join(hash: context, parent: nil, id: :global_context)
|
39
|
+
end
|
40
|
+
|
41
|
+
def global_context
|
42
|
+
@global_context ||= join(parent: nil, id: :global_context)
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_context=(context)
|
46
|
+
@default_context = join(hash: context, parent: global_context, id: :default_context)
|
47
|
+
|
48
|
+
self.current.update_parent(@default_context)
|
49
|
+
end
|
50
|
+
|
51
|
+
def default_context
|
52
|
+
@default_context ||= join(parent: global_context, id: :default_context)
|
53
|
+
end
|
54
|
+
|
55
|
+
def current=(context)
|
56
|
+
Thread.current[THREAD_KEY] = join(hash: context || {}, parent: default_context, id: :block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def current
|
60
|
+
Thread.current[THREAD_KEY] ||= join(parent: default_context, id: :block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_context(context)
|
64
|
+
old_context = Thread.current[THREAD_KEY]
|
65
|
+
Thread.current[THREAD_KEY] = join(parent: default_context, hash: context, id: :block)
|
66
|
+
yield
|
67
|
+
ensure
|
68
|
+
Thread.current[THREAD_KEY] = old_context
|
69
|
+
end
|
70
|
+
|
71
|
+
def with_merged_context(context)
|
72
|
+
old_context = Thread.current[THREAD_KEY]
|
73
|
+
Thread.current[THREAD_KEY] = join(parent: current, hash: context, id: :merged)
|
74
|
+
yield
|
75
|
+
ensure
|
76
|
+
Thread.current[THREAD_KEY] = old_context
|
77
|
+
end
|
78
|
+
|
79
|
+
def clear_current
|
80
|
+
Thread.current[THREAD_KEY] = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def merge_with_current(new_context_properties = {})
|
84
|
+
new(current.to_h.merge(new_context_properties.to_h))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.join(hash: {}, parent: nil, id: :not_provided)
|
89
|
+
context = new(hash)
|
90
|
+
context.update_parent(parent)
|
91
|
+
context.instance_variable_set(:@id, id)
|
92
|
+
context
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize(hash = {})
|
96
|
+
@contexts = {}
|
97
|
+
@flattened = {}
|
98
|
+
@seen_at = Time.now.utc.to_i
|
99
|
+
|
100
|
+
if hash.is_a?(Hash)
|
101
|
+
hash.map do |name, values|
|
102
|
+
unless values.is_a?(Hash)
|
103
|
+
warn "[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash."
|
104
|
+
values = { name => values }
|
105
|
+
name = BLANK_CONTEXT_NAME
|
106
|
+
end
|
107
|
+
|
108
|
+
@contexts[name.to_s] = NamedContext.new(name, values)
|
109
|
+
values.each do |key, value|
|
110
|
+
@flattened[name.to_s + '.' + key.to_s] = value
|
111
|
+
end
|
112
|
+
end
|
113
|
+
else
|
114
|
+
raise ArgumentError, 'must be a Hash'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def update_parent(parent)
|
119
|
+
@parent = parent
|
120
|
+
end
|
121
|
+
|
122
|
+
def blank?
|
123
|
+
contexts.empty?
|
124
|
+
end
|
125
|
+
|
126
|
+
def set(name, hash)
|
127
|
+
@contexts[name.to_s] = NamedContext.new(name, hash)
|
128
|
+
hash.each do |key, value|
|
129
|
+
@flattened[name.to_s + '.' + key.to_s] = value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def get(property_key, scope: nil)
|
134
|
+
if !property_key.include?(".")
|
135
|
+
property_key = BLANK_CONTEXT_NAME + '.' + property_key
|
136
|
+
end
|
137
|
+
|
138
|
+
if @flattened.key?(property_key)
|
139
|
+
@flattened[property_key]
|
140
|
+
else
|
141
|
+
scope ||= property_key.split('.').first
|
142
|
+
|
143
|
+
if @contexts[scope]
|
144
|
+
# If the key is in the present scope, parent values should not be used.
|
145
|
+
# We can consider the parent value clobbered by the present scope.
|
146
|
+
nil
|
147
|
+
else
|
148
|
+
@parent&.get(property_key, scope: scope)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_h
|
154
|
+
contexts.transform_values(&:to_h)
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_s
|
158
|
+
"#<Reforge::Context:#{object_id} id=#{@id} #{to_h}>"
|
159
|
+
end
|
160
|
+
|
161
|
+
# Visualize a tree of the context up through its parents
|
162
|
+
#
|
163
|
+
# example:
|
164
|
+
#
|
165
|
+
# | jit: {"user"=>{"name"=>"Frank"}}
|
166
|
+
# |-- block: {"clock"=>{"timezone"=>"PST"}}
|
167
|
+
# |---- default_context: {"prefab-api-key"=>{"user-id"=>123}}
|
168
|
+
# |------ global_context: {"cpu"=>{"count"=>4, "speed"=>"2.4GHz"}, "clock"=>{"timezone"=>"UTC"}}
|
169
|
+
def tree(depth = 0)
|
170
|
+
"|" + ("-" * depth) + " #{id}: #{(" " * (30 - id.to_s.length - depth ))}#{to_h}\n" + (@parent&.tree(depth + 2) || '')
|
171
|
+
end
|
172
|
+
|
173
|
+
def clear
|
174
|
+
@contexts = {}
|
175
|
+
@flattened = {}
|
176
|
+
end
|
177
|
+
|
178
|
+
def context(name)
|
179
|
+
contexts[name.to_s] || NamedContext.new(name, {})
|
180
|
+
end
|
181
|
+
|
182
|
+
def merge_default(defaults)
|
183
|
+
defaults.keys.each do |name|
|
184
|
+
set(name, context(name).merge!(defaults[name]))
|
185
|
+
end
|
186
|
+
|
187
|
+
self
|
188
|
+
end
|
189
|
+
|
190
|
+
def reportable_tree
|
191
|
+
ctx = self
|
192
|
+
reportables = []
|
193
|
+
|
194
|
+
while ctx
|
195
|
+
reportables.unshift(ctx)
|
196
|
+
ctx = ctx.parent
|
197
|
+
end
|
198
|
+
|
199
|
+
reportables
|
200
|
+
end
|
201
|
+
|
202
|
+
def to_proto(namespace)
|
203
|
+
reportable_contexts = {}
|
204
|
+
|
205
|
+
reportable_tree.each do |ctx|
|
206
|
+
ctx.contexts.each do |name, context|
|
207
|
+
reportable_contexts[name] = context
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
PrefabProto::ContextSet.new(
|
212
|
+
contexts: reportable_contexts.map do |name, context|
|
213
|
+
context.to_proto
|
214
|
+
end
|
215
|
+
)
|
216
|
+
end
|
217
|
+
|
218
|
+
def slim_proto
|
219
|
+
PrefabProto::ContextSet.new(
|
220
|
+
contexts: contexts.map do |_, context|
|
221
|
+
context.to_proto
|
222
|
+
end
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
def grouped_key
|
227
|
+
contexts.map do |_, context|
|
228
|
+
context.key
|
229
|
+
end.sort.join('|')
|
230
|
+
end
|
231
|
+
|
232
|
+
include Comparable
|
233
|
+
def <=>(other)
|
234
|
+
if other.is_a?(Reforge::Context)
|
235
|
+
to_h <=> other.to_h
|
236
|
+
else
|
237
|
+
super
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Reforge
|
2
|
+
class ContextShape
|
3
|
+
MAPPING = {
|
4
|
+
Integer => 1,
|
5
|
+
String => 2,
|
6
|
+
Float => 4,
|
7
|
+
TrueClass => 5,
|
8
|
+
FalseClass => 5,
|
9
|
+
Array => 10,
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
# We default to String if the type isn't a primitive we support.
|
13
|
+
# This is because we do a `to_s` in the CriteriaEvaluator.
|
14
|
+
DEFAULT = MAPPING[String]
|
15
|
+
|
16
|
+
def self.field_type_number(value)
|
17
|
+
MAPPING.fetch(value.class, DEFAULT)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'periodic_sync'
|
4
|
+
|
5
|
+
module Reforge
|
6
|
+
class ContextShapeAggregator
|
7
|
+
include Reforge::PeriodicSync
|
8
|
+
LOG = Reforge::InternalLogger.new(self)
|
9
|
+
|
10
|
+
attr_reader :data
|
11
|
+
|
12
|
+
def initialize(client:, max_shapes:, sync_interval:)
|
13
|
+
@max_shapes = max_shapes
|
14
|
+
@client = client
|
15
|
+
@name = 'context_shape_aggregator'
|
16
|
+
|
17
|
+
@data = Concurrent::Set.new
|
18
|
+
|
19
|
+
start_periodic_sync(sync_interval)
|
20
|
+
end
|
21
|
+
|
22
|
+
def push(context)
|
23
|
+
return if @data.size >= @max_shapes
|
24
|
+
|
25
|
+
context.contexts.each_pair do |name, name_context|
|
26
|
+
name_context.to_h.each_pair do |key, value|
|
27
|
+
@data.add [name, key, Reforge::ContextShape.field_type_number(value)]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def prepare_data
|
33
|
+
duped = @data.dup
|
34
|
+
@data.clear
|
35
|
+
|
36
|
+
duped.inject({}) do |acc, (name, key, type)|
|
37
|
+
acc[name] ||= {}
|
38
|
+
acc[name][key] = type
|
39
|
+
acc
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def flush(to_ship, _)
|
46
|
+
pool.post do
|
47
|
+
LOG.debug "Uploading context shapes for #{to_ship.values.size}"
|
48
|
+
|
49
|
+
events = PrefabProto::TelemetryEvents.new(
|
50
|
+
instance_hash: instance_hash,
|
51
|
+
events: [
|
52
|
+
PrefabProto::TelemetryEvent.new(context_shapes:
|
53
|
+
PrefabProto::ContextShapes.new(
|
54
|
+
shapes: to_ship.map do |name, shape|
|
55
|
+
PrefabProto::ContextShape.new(
|
56
|
+
name: name,
|
57
|
+
field_types: shape
|
58
|
+
)
|
59
|
+
end
|
60
|
+
))
|
61
|
+
]
|
62
|
+
)
|
63
|
+
|
64
|
+
result = post('/api/v1/telemetry', events)
|
65
|
+
|
66
|
+
LOG.debug "Uploaded #{to_ship.values.size} shapes: #{result.status}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,345 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Naming/MethodName
|
4
|
+
# We're intentionally keeping the UPCASED method names to match the protobuf
|
5
|
+
# and avoid wasting CPU cycles lowercasing things
|
6
|
+
module Reforge
|
7
|
+
# This class evaluates a config's criteria. `evaluate` returns the value of
|
8
|
+
# the first match based on the provided properties.
|
9
|
+
class CriteriaEvaluator
|
10
|
+
LOG = Reforge::InternalLogger.new(self)
|
11
|
+
NAMESPACE_KEY = 'NAMESPACE'
|
12
|
+
NO_MATCHING_ROWS = [].freeze
|
13
|
+
|
14
|
+
def initialize(config, project_env_id:, resolver:, namespace:, base_client:)
|
15
|
+
@config = config
|
16
|
+
@project_env_id = project_env_id
|
17
|
+
@resolver = resolver
|
18
|
+
@namespace = namespace
|
19
|
+
@base_client = base_client
|
20
|
+
end
|
21
|
+
|
22
|
+
def evaluate(properties)
|
23
|
+
rtn = evaluate_for_env(@project_env_id, properties) ||
|
24
|
+
evaluate_for_env(0, properties)
|
25
|
+
LOG.trace {
|
26
|
+
"Eval Key #{@config.key} Result #{rtn&.reportable_value} with #{properties.to_h}"
|
27
|
+
} unless @config.config_type == :LOG_LEVEL
|
28
|
+
rtn
|
29
|
+
end
|
30
|
+
|
31
|
+
def all_criteria_match?(conditional_value, props)
|
32
|
+
conditional_value.criteria.all? do |criterion|
|
33
|
+
public_send(criterion.operator, criterion, props)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def IN_SEG(criterion, properties)
|
38
|
+
in_segment?(criterion, properties)
|
39
|
+
end
|
40
|
+
|
41
|
+
def NOT_IN_SEG(criterion, properties)
|
42
|
+
!in_segment?(criterion, properties)
|
43
|
+
end
|
44
|
+
|
45
|
+
def ALWAYS_TRUE(_criterion, _properties)
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def PROP_IS_ONE_OF(criterion, properties)
|
50
|
+
Array(value_from_properties(criterion, properties)).any? do |prop|
|
51
|
+
matches?(criterion, prop, properties)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def PROP_IS_NOT_ONE_OF(criterion, properties)
|
56
|
+
!PROP_IS_ONE_OF(criterion, properties)
|
57
|
+
end
|
58
|
+
|
59
|
+
def PROP_ENDS_WITH_ONE_OF(criterion, properties)
|
60
|
+
prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
|
61
|
+
end
|
62
|
+
|
63
|
+
def PROP_DOES_NOT_END_WITH_ONE_OF(criterion, properties)
|
64
|
+
!PROP_ENDS_WITH_ONE_OF(criterion, properties)
|
65
|
+
end
|
66
|
+
|
67
|
+
def PROP_STARTS_WITH_ONE_OF(criterion, properties)
|
68
|
+
prop_starts_with_one_of?(criterion, value_from_properties(criterion, properties))
|
69
|
+
end
|
70
|
+
|
71
|
+
def PROP_DOES_NOT_START_WITH_ONE_OF(criterion, properties)
|
72
|
+
!PROP_STARTS_WITH_ONE_OF(criterion, properties)
|
73
|
+
end
|
74
|
+
|
75
|
+
def PROP_CONTAINS_ONE_OF(criterion, properties)
|
76
|
+
prop_contains_one_of?(criterion, value_from_properties(criterion, properties))
|
77
|
+
end
|
78
|
+
|
79
|
+
def PROP_DOES_NOT_CONTAIN_ONE_OF(criterion, properties)
|
80
|
+
!PROP_CONTAINS_ONE_OF(criterion, properties)
|
81
|
+
end
|
82
|
+
|
83
|
+
def HIERARCHICAL_MATCH(criterion, properties)
|
84
|
+
value = value_from_properties(criterion, properties)
|
85
|
+
value&.start_with?(criterion.value_to_match.string)
|
86
|
+
end
|
87
|
+
|
88
|
+
def IN_INT_RANGE(criterion, properties)
|
89
|
+
value = value_from_properties(criterion, properties)
|
90
|
+
value && value >= criterion.value_to_match.int_range.start && value < criterion.value_to_match.int_range.end
|
91
|
+
end
|
92
|
+
|
93
|
+
def PROP_MATCHES(criterion, properties)
|
94
|
+
result = check_regex_match(criterion, properties)
|
95
|
+
if result.error
|
96
|
+
false
|
97
|
+
else
|
98
|
+
result.matched
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def PROP_DOES_NOT_MATCH(criterion, properties)
|
103
|
+
result = check_regex_match(criterion, properties)
|
104
|
+
if result.error
|
105
|
+
false
|
106
|
+
else
|
107
|
+
!result.matched
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def PROP_LESS_THAN(criterion, properties)
|
112
|
+
evaluate_number_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than]).matched
|
113
|
+
end
|
114
|
+
|
115
|
+
def PROP_LESS_THAN_OR_EQUAL(criterion, properties)
|
116
|
+
evaluate_number_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than_or_equal]).matched
|
117
|
+
end
|
118
|
+
|
119
|
+
def PROP_GREATER_THAN(criterion, properties)
|
120
|
+
evaluate_number_comparison(criterion, properties, COMPARE_TO_OPERATORS[:greater_than]).matched
|
121
|
+
end
|
122
|
+
|
123
|
+
def PROP_GREATER_THAN_OR_EQUAL(criterion, properties)
|
124
|
+
evaluate_number_comparison(criterion, properties,COMPARE_TO_OPERATORS[:greater_than_or_equal]) .matched
|
125
|
+
end
|
126
|
+
|
127
|
+
def PROP_BEFORE(criterion, properties)
|
128
|
+
evaluate_date_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than]).matched
|
129
|
+
end
|
130
|
+
|
131
|
+
def PROP_AFTER(criterion, properties)
|
132
|
+
evaluate_date_comparison(criterion, properties, COMPARE_TO_OPERATORS[:greater_than]).matched
|
133
|
+
end
|
134
|
+
|
135
|
+
def PROP_SEMVER_LESS_THAN(criterion, properties)
|
136
|
+
evaluate_semver_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than]).matched
|
137
|
+
end
|
138
|
+
|
139
|
+
def PROP_SEMVER_EQUAL(criterion, properties)
|
140
|
+
evaluate_semver_comparison(criterion, properties, COMPARE_TO_OPERATORS[:equal_to]).matched
|
141
|
+
end
|
142
|
+
|
143
|
+
def PROP_SEMVER_GREATER_THAN(criterion, properties)
|
144
|
+
evaluate_semver_comparison(criterion, properties, COMPARE_TO_OPERATORS[:greater_than]).matched
|
145
|
+
end
|
146
|
+
|
147
|
+
def value_from_properties(criterion, properties)
|
148
|
+
case criterion.property_name
|
149
|
+
when NAMESPACE_KEY
|
150
|
+
@namespace
|
151
|
+
when 'prefab.current-time','reforge.current-time'
|
152
|
+
Time.now.utc.to_i * 1000
|
153
|
+
else
|
154
|
+
properties.get(criterion.property_name)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
COMPARE_TO_OPERATORS = {
|
159
|
+
less_than_or_equal: -> cmp { cmp <= 0 },
|
160
|
+
less_than: -> cmp { cmp < 0 },
|
161
|
+
equal_to: -> cmp { cmp == 0 },
|
162
|
+
greater_than: -> cmp { cmp > 0 },
|
163
|
+
greater_than_or_equal: -> cmp { cmp >= 0 },
|
164
|
+
}
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def evaluate_semver_comparison(criterion, properties, predicate)
|
169
|
+
context_version = value_from_properties(criterion, properties)&.then { |v| SemanticVersion.parse_quietly(v) }
|
170
|
+
config_version = criterion.value_to_match&.string&.then {|v| SemanticVersion.parse_quietly(criterion.value_to_match.string) }
|
171
|
+
|
172
|
+
unless context_version && config_version
|
173
|
+
return MatchResult.error
|
174
|
+
end
|
175
|
+
predicate.call(context_version <=> config_version) ? MatchResult.matched : MatchResult.not_matched
|
176
|
+
end
|
177
|
+
|
178
|
+
def evaluate_date_comparison(criterion, properties, predicate)
|
179
|
+
context_millis = as_millis(value_from_properties(criterion, properties))
|
180
|
+
config_millis = as_millis(Reforge::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config,
|
181
|
+
properties, @resolver).unwrap)
|
182
|
+
|
183
|
+
unless config_millis && context_millis
|
184
|
+
return MatchResult.error
|
185
|
+
end
|
186
|
+
|
187
|
+
predicate.call(context_millis <=> config_millis) ? MatchResult.matched : MatchResult.not_matched
|
188
|
+
end
|
189
|
+
|
190
|
+
def evaluate_number_comparison(criterion, properties, predicate)
|
191
|
+
context_value = value_from_properties(criterion, properties)
|
192
|
+
value_to_match = extract_numeric_value(criterion.value_to_match)
|
193
|
+
|
194
|
+
return MatchResult.error if value_to_match.nil?
|
195
|
+
return MatchResult.error unless context_value.is_a?(Numeric)
|
196
|
+
|
197
|
+
# Compare the values and apply the predicate method
|
198
|
+
comparison_result = context_value <=> value_to_match
|
199
|
+
return MatchResult.error if comparison_result.nil?
|
200
|
+
|
201
|
+
predicate.call(comparison_result) ? MatchResult.matched : MatchResult.not_matched
|
202
|
+
end
|
203
|
+
|
204
|
+
def extract_numeric_value(config_value)
|
205
|
+
case config_value.type
|
206
|
+
when :int
|
207
|
+
config_value.int
|
208
|
+
when :double
|
209
|
+
config_value.double
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def as_millis(obj)
|
214
|
+
if obj.is_a?(Numeric)
|
215
|
+
return obj.to_int if obj.respond_to?(:to_int)
|
216
|
+
end
|
217
|
+
if obj.is_a?(String)
|
218
|
+
Time.iso8601(obj).utc.to_i * 1000 rescue nil
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
def evaluate_for_env(env_id, properties)
|
224
|
+
@config.rows.each_with_index do |row, index|
|
225
|
+
next unless row.project_env_id == env_id
|
226
|
+
|
227
|
+
row.values.each_with_index do |conditional_value, value_index|
|
228
|
+
next unless all_criteria_match?(conditional_value, properties)
|
229
|
+
|
230
|
+
return Reforge::Evaluation.new(
|
231
|
+
config: @config,
|
232
|
+
value: conditional_value.value,
|
233
|
+
value_index: value_index,
|
234
|
+
config_row_index: index,
|
235
|
+
context: properties,
|
236
|
+
resolver: @resolver
|
237
|
+
)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
nil
|
242
|
+
end
|
243
|
+
|
244
|
+
def in_segment?(criterion, properties)
|
245
|
+
segment = @resolver.get(criterion.value_to_match.string, properties)
|
246
|
+
|
247
|
+
LOG.info("Segment #{criterion.value_to_match.string} not found") unless segment
|
248
|
+
|
249
|
+
segment&.report_and_return(@base_client.evaluation_summary_aggregator)
|
250
|
+
end
|
251
|
+
|
252
|
+
def matches?(criterion, value, properties)
|
253
|
+
criterion_value_or_values = Reforge::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config,
|
254
|
+
properties, @resolver).unwrap
|
255
|
+
|
256
|
+
case criterion_value_or_values
|
257
|
+
when Google::Protobuf::RepeatedField
|
258
|
+
# we to_s the value from properties for comparison because the
|
259
|
+
# criterion_value_or_values is a list of strings
|
260
|
+
criterion_value_or_values.include?(value.to_s)
|
261
|
+
else
|
262
|
+
criterion_value_or_values == value
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def prop_ends_with_one_of?(criterion, value)
|
267
|
+
return false unless value
|
268
|
+
|
269
|
+
criterion.value_to_match.string_list.values.any? do |ending|
|
270
|
+
value.end_with?(ending)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def prop_starts_with_one_of?(criterion, value)
|
275
|
+
return false unless value
|
276
|
+
|
277
|
+
criterion.value_to_match.string_list.values.any? do |beginning|
|
278
|
+
value.start_with?(beginning)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def prop_contains_one_of?(criterion, value)
|
283
|
+
return false unless value
|
284
|
+
|
285
|
+
criterion.value_to_match.string_list.values.any? do |substring|
|
286
|
+
value.include?(substring)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def check_regex_match(criterion, properties)
|
291
|
+
begin
|
292
|
+
regex_definition = Reforge::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config.key,
|
293
|
+
properties, @resolver).unwrap
|
294
|
+
|
295
|
+
return MatchResult.error unless regex_definition.is_a?(String)
|
296
|
+
|
297
|
+
value = value_from_properties(criterion, properties)
|
298
|
+
|
299
|
+
regex = compile_regex_safely(ensure_anchored_regex(regex_definition))
|
300
|
+
return MatchResult.error unless regex
|
301
|
+
|
302
|
+
matches = regex.match?(value.to_s)
|
303
|
+
matches ? MatchResult.matched : MatchResult.not_matched
|
304
|
+
rescue RegexpError
|
305
|
+
MatchResult.error
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def compile_regex_safely(pattern)
|
310
|
+
begin
|
311
|
+
Regexp.new(pattern)
|
312
|
+
rescue RegexpError
|
313
|
+
nil
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def ensure_anchored_regex(pattern)
|
318
|
+
return pattern if pattern.start_with?("^") && pattern.end_with?("$")
|
319
|
+
|
320
|
+
"^#{pattern}$"
|
321
|
+
end
|
322
|
+
|
323
|
+
class MatchResult
|
324
|
+
attr_reader :matched, :error
|
325
|
+
|
326
|
+
def self.matched
|
327
|
+
new(matched: true)
|
328
|
+
end
|
329
|
+
|
330
|
+
def self.not_matched
|
331
|
+
new(matched: false)
|
332
|
+
end
|
333
|
+
|
334
|
+
def self.error
|
335
|
+
new(matched: false, error: true)
|
336
|
+
end
|
337
|
+
|
338
|
+
def initialize(matched:, error: false)
|
339
|
+
@matched = matched
|
340
|
+
@error = error
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
# rubocop:enable Naming/MethodName
|