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.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/pull_request_template.md +8 -0
  5. data/.github/workflows/ruby.yml +48 -0
  6. data/.gitmodules +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.tool-versions +1 -0
  9. data/CHANGELOG.md +257 -0
  10. data/CODEOWNERS +1 -0
  11. data/Gemfile +29 -0
  12. data/Gemfile.lock +182 -0
  13. data/LICENSE.txt +20 -0
  14. data/README.md +105 -0
  15. data/Rakefile +63 -0
  16. data/VERSION +1 -0
  17. data/compile_protos.sh +20 -0
  18. data/dev/allocation_stats +60 -0
  19. data/dev/benchmark +40 -0
  20. data/dev/console +12 -0
  21. data/dev/script_setup.rb +18 -0
  22. data/lib/prefab_pb.rb +77 -0
  23. data/lib/reforge/caching_http_connection.rb +95 -0
  24. data/lib/reforge/client.rb +133 -0
  25. data/lib/reforge/config_client.rb +275 -0
  26. data/lib/reforge/config_client_presenter.rb +18 -0
  27. data/lib/reforge/config_loader.rb +67 -0
  28. data/lib/reforge/config_resolver.rb +84 -0
  29. data/lib/reforge/config_value_unwrapper.rb +123 -0
  30. data/lib/reforge/config_value_wrapper.rb +18 -0
  31. data/lib/reforge/context.rb +241 -0
  32. data/lib/reforge/context_shape.rb +20 -0
  33. data/lib/reforge/context_shape_aggregator.rb +70 -0
  34. data/lib/reforge/criteria_evaluator.rb +345 -0
  35. data/lib/reforge/duration.rb +58 -0
  36. data/lib/reforge/encryption.rb +65 -0
  37. data/lib/reforge/error.rb +6 -0
  38. data/lib/reforge/errors/env_var_parse_error.rb +11 -0
  39. data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/reforge/errors/missing_default_error.rb +13 -0
  42. data/lib/reforge/errors/missing_env_var_error.rb +11 -0
  43. data/lib/reforge/errors/uninitialized_error.rb +13 -0
  44. data/lib/reforge/evaluation.rb +53 -0
  45. data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
  46. data/lib/reforge/example_contexts_aggregator.rb +77 -0
  47. data/lib/reforge/exponential_backoff.rb +21 -0
  48. data/lib/reforge/feature_flag_client.rb +43 -0
  49. data/lib/reforge/fixed_size_hash.rb +14 -0
  50. data/lib/reforge/http_connection.rb +45 -0
  51. data/lib/reforge/internal_logger.rb +43 -0
  52. data/lib/reforge/javascript_stub.rb +99 -0
  53. data/lib/reforge/local_config_parser.rb +151 -0
  54. data/lib/reforge/murmer3.rb +50 -0
  55. data/lib/reforge/options.rb +191 -0
  56. data/lib/reforge/periodic_sync.rb +74 -0
  57. data/lib/reforge/prefab.rb +120 -0
  58. data/lib/reforge/rate_limit_cache.rb +41 -0
  59. data/lib/reforge/resolved_config_presenter.rb +86 -0
  60. data/lib/reforge/semver.rb +132 -0
  61. data/lib/reforge/sse_config_client.rb +112 -0
  62. data/lib/reforge/time_helpers.rb +7 -0
  63. data/lib/reforge/weighted_value_resolver.rb +42 -0
  64. data/lib/reforge/yaml_config_parser.rb +34 -0
  65. data/lib/reforge-sdk.rb +57 -0
  66. data/test/fixtures/datafile.json +87 -0
  67. data/test/integration_test.rb +171 -0
  68. data/test/integration_test_helpers.rb +114 -0
  69. data/test/support/common_helpers.rb +201 -0
  70. data/test/support/mock_base_client.rb +41 -0
  71. data/test/support/mock_config_client.rb +19 -0
  72. data/test/support/mock_config_loader.rb +1 -0
  73. data/test/test_caching_http_connection.rb +218 -0
  74. data/test/test_client.rb +351 -0
  75. data/test/test_config_client.rb +84 -0
  76. data/test/test_config_loader.rb +82 -0
  77. data/test/test_config_resolver.rb +502 -0
  78. data/test/test_config_value_unwrapper.rb +270 -0
  79. data/test/test_config_value_wrapper.rb +42 -0
  80. data/test/test_context.rb +271 -0
  81. data/test/test_context_shape.rb +50 -0
  82. data/test/test_context_shape_aggregator.rb +150 -0
  83. data/test/test_criteria_evaluator.rb +1180 -0
  84. data/test/test_duration.rb +37 -0
  85. data/test/test_encryption.rb +16 -0
  86. data/test/test_evaluation_summary_aggregator.rb +162 -0
  87. data/test/test_example_contexts_aggregator.rb +233 -0
  88. data/test/test_exponential_backoff.rb +18 -0
  89. data/test/test_feature_flag_client.rb +16 -0
  90. data/test/test_fixed_size_hash.rb +119 -0
  91. data/test/test_helper.rb +17 -0
  92. data/test/test_integration.rb +75 -0
  93. data/test/test_internal_logger.rb +25 -0
  94. data/test/test_javascript_stub.rb +176 -0
  95. data/test/test_local_config_parser.rb +147 -0
  96. data/test/test_logger_initialization.rb +12 -0
  97. data/test/test_options.rb +93 -0
  98. data/test/test_prefab.rb +16 -0
  99. data/test/test_rate_limit_cache.rb +44 -0
  100. data/test/test_semver.rb +108 -0
  101. data/test/test_sse_config_client.rb +211 -0
  102. data/test/test_weighted_value_resolver.rb +71 -0
  103. 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