prefab-cloud-ruby 1.8.8.pre.1 → 1.8.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9485b9faf8e79e7e95f3eb69fb0e9f13c8ab4f1cc9c5f3b8d637f5822ed3ba3b
4
- data.tar.gz: efb3287eb90d718ee8ddf3f4461b756a228bb619b21ddf9a6224d994a6a180fc
3
+ metadata.gz: 543a1cb8d31c7ae3efbc8e63f020803a1813e5de5129cf6adb9093bbfad16a7d
4
+ data.tar.gz: 15b576851d86a6b1c36f0dc1982392eef70fc53ea01da63419e07128ca33ae0a
5
5
  SHA512:
6
- metadata.gz: 943920075095daaee6f2defc658cea5ac17f77534936d1ba7783bc4993b994cd867515d3dc83ced057ff26ce2cf74ea1acb1b62e0bbe7cad651b9af9c84fe359
7
- data.tar.gz: df03d8f2ba7f57f40ccad2e96b5ca73c42a7f4b27f5524506fe6a95a030420a852b62c2e987d70b49d8e441bf092b5fcc37b1fd57183460824979cb6284d5d1e
6
+ metadata.gz: a9029c5d87c7c2e62c48697d7b40ddcf44d35e12fb211e7a4292f4212e110dcd42492c110b02d7f49df3985a23b4b76692c6367c2587572f9ea7fa177bd808be
7
+ data.tar.gz: 0f4fdeab62be128860fcd52b0c5de1486e6e3cb88e4494c155bdc7a6b4c65d4a34fb6adcab60c2ac418194b0398797d1441265716bce80d46d193b3cbae0ee14
@@ -0,0 +1,8 @@
1
+ ## Description
2
+ *(Brief overview of what this PR changes and/or why the changes are needed)*
3
+
4
+ ## Testing & Validation
5
+ *(Outline the steps needed to be taken to verify the changes.)*
6
+
7
+ ## Rollout
8
+ *(Optional section: Provide rollout and rollback procedures, when outside the bounds or requiring additional work beyond standard deployment)*
@@ -22,7 +22,7 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  strategy:
24
24
  matrix:
25
- ruby-version: ['2.7', '3.0', '3.3']
25
+ ruby-version: ['3.0','3.1','3.2','3.3']
26
26
 
27
27
  steps:
28
28
  - uses: actions/checkout@v4
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.0.3
1
+ ruby 3.2.7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## 1.8.8 - 2025-02-28
5
+
6
+ - Add conditional fetch support for configurations (#226)
7
+ - Operator support for string starts with, contains (#212)
8
+ - Operator support for regex, semver (protobuf update) (#215)
9
+ - Operator support for date comparison (before/after) (#221)
10
+ - Operator support for numeric comparisons (#220)
11
+
12
+
3
13
  ## 1.8.7 - 2024-10-25
4
14
 
5
15
  - Add option symbolize_json_names (#211)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.8.pre.1
1
+ 1.8.8
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class CachingHttpConnection
5
+ CACHE_SIZE = 2.freeze
6
+ CacheEntry = Struct.new(:data, :etag, :expires_at)
7
+
8
+ class << self
9
+ def cache
10
+ @cache ||= FixedSizeHash.new(CACHE_SIZE)
11
+ end
12
+
13
+ def reset_cache!
14
+ @cache = FixedSizeHash.new(CACHE_SIZE)
15
+ end
16
+ end
17
+
18
+ def initialize(uri, api_key)
19
+ @connection = HttpConnection.new(uri, api_key)
20
+ end
21
+
22
+ def get(path)
23
+ now = Time.now.to_i
24
+ cache_key = "#{@connection.uri}#{path}"
25
+ cached = self.class.cache[cache_key]
26
+
27
+ # Check if we have a valid cached response
28
+ if cached&.data && cached.expires_at && now < cached.expires_at
29
+ return Faraday::Response.new(
30
+ status: 200,
31
+ body: cached.data,
32
+ response_headers: {
33
+ 'ETag' => cached.etag,
34
+ 'X-Cache' => 'HIT',
35
+ 'X-Cache-Expires-At' => cached.expires_at.to_s
36
+ }
37
+ )
38
+ end
39
+
40
+ # Make request with conditional GET if we have an ETag
41
+ response = if cached&.etag
42
+ @connection.get(path, { 'If-None-Match' => cached.etag })
43
+ else
44
+ @connection.get(path)
45
+ end
46
+
47
+ # Handle 304 Not Modified
48
+ if response.status == 304 && cached&.data
49
+ return Faraday::Response.new(
50
+ status: 200,
51
+ body: cached.data,
52
+ response_headers: {
53
+ 'ETag' => cached.etag,
54
+ 'X-Cache' => 'HIT',
55
+ 'X-Cache-Expires-At' => cached.expires_at.to_s
56
+ }
57
+ )
58
+ end
59
+
60
+ # Parse caching headers
61
+ cache_control = response.headers['Cache-Control'].to_s
62
+ etag = response.headers['ETag']
63
+
64
+ # Always add X-Cache header
65
+ response.headers['X-Cache'] = 'MISS'
66
+
67
+ # Don't cache if no-store is present
68
+ return response if cache_control.include?('no-store')
69
+
70
+ # Calculate expiration
71
+ max_age = cache_control.match(/max-age=(\d+)/)&.captures&.first&.to_i
72
+ expires_at = max_age ? now + max_age : nil
73
+
74
+ # Cache the response if we have caching headers
75
+ if etag || expires_at
76
+ self.class.cache[cache_key] = CacheEntry.new(
77
+ response.body,
78
+ etag,
79
+ expires_at
80
+ )
81
+ end
82
+
83
+ response
84
+ end
85
+
86
+ # Delegate other methods to the underlying connection
87
+ def post(path, body)
88
+ @connection.post(path, body)
89
+ end
90
+
91
+ def uri
92
+ @connection.uri
93
+ end
94
+ end
95
+ end
@@ -118,7 +118,7 @@ module Prefab
118
118
  end
119
119
 
120
120
  def load_checkpoint
121
- success = load_source_checkpoint
121
+ success = load_source_checkpoint(:start_at_id => @config_loader.highwater_mark)
122
122
  return if success
123
123
 
124
124
  success = load_cache
@@ -127,11 +127,10 @@ module Prefab
127
127
  LOG.warn 'No success loading checkpoints'
128
128
  end
129
129
 
130
- def load_source_checkpoint
130
+ def load_source_checkpoint(start_at_id: 0)
131
131
  @options.config_sources.each do |source|
132
- conn = Prefab::HttpConnection.new("#{source}/api/v1/configs/0", @base_client.api_key)
132
+ conn = Prefab::CachingHttpConnection.new("#{source}/api/v1/configs/#{start_at_id}", @base_client.api_key)
133
133
  result = load_url(conn, :remote_api)
134
-
135
134
  return true if result
136
135
  end
137
136
 
@@ -146,18 +145,18 @@ module Prefab
146
145
  cache_configs(configs)
147
146
  true
148
147
  else
149
- LOG.info "Checkpoint #{source} failed to load. Response #{resp.status}"
148
+ LOG.info "Checkpoint #{source} [#{conn.uri}] failed to load. Response #{resp.status}"
150
149
  false
151
150
  end
152
151
  rescue Faraday::ConnectionFailed => e
153
152
  if !initialized?
154
- LOG.warn "Connection Fail loading #{source} checkpoint."
153
+ LOG.warn "Connection Fail loading #{source} [#{conn.uri}] checkpoint."
155
154
  else
156
- LOG.debug "Connection Fail loading #{source} checkpoint."
155
+ LOG.debug "Connection Fail loading #{source} [#{conn.uri}] checkpoint."
157
156
  end
158
157
  false
159
158
  rescue StandardError => e
160
- LOG.warn "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
159
+ LOG.warn "Unexpected #{source} [#{conn.uri}] problem loading checkpoint #{e} #{conn}"
161
160
  LOG.debug e.backtrace
162
161
  false
163
162
  end
@@ -21,7 +21,7 @@ module Prefab
21
21
 
22
22
  def evaluate(properties)
23
23
  rtn = evaluate_for_env(@project_env_id, properties) ||
24
- evaluate_for_env(0, properties)
24
+ evaluate_for_env(0, properties)
25
25
  LOG.trace {
26
26
  "Eval Key #{@config.key} Result #{rtn&.reportable_value} with #{properties.to_h}"
27
27
  } unless @config.config_type == :LOG_LEVEL
@@ -95,12 +95,129 @@ module Prefab
95
95
  value && value >= criterion.value_to_match.int_range.start && value < criterion.value_to_match.int_range.end
96
96
  end
97
97
 
98
+ def PROP_MATCHES(criterion, properties)
99
+ result = check_regex_match(criterion, properties)
100
+ if result.error
101
+ false
102
+ else
103
+ result.matched
104
+ end
105
+ end
106
+
107
+ def PROP_DOES_NOT_MATCH(criterion, properties)
108
+ result = check_regex_match(criterion, properties)
109
+ if result.error
110
+ false
111
+ else
112
+ !result.matched
113
+ end
114
+ end
115
+
116
+ def PROP_LESS_THAN(criterion, properties)
117
+ evaluate_number_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than]).matched
118
+ end
119
+
120
+ def PROP_LESS_THAN_OR_EQUAL(criterion, properties)
121
+ evaluate_number_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than_or_equal]).matched
122
+ end
123
+
124
+ def PROP_GREATER_THAN(criterion, properties)
125
+ evaluate_number_comparison(criterion, properties, COMPARE_TO_OPERATORS[:greater_than]).matched
126
+ end
127
+
128
+ def PROP_GREATER_THAN_OR_EQUAL(criterion, properties)
129
+ evaluate_number_comparison(criterion, properties,COMPARE_TO_OPERATORS[:greater_than_or_equal]) .matched
130
+ end
131
+
132
+ def PROP_BEFORE(criterion, properties)
133
+ evaluate_date_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than]).matched
134
+ end
135
+
136
+ def PROP_AFTER(criterion, properties)
137
+ evaluate_date_comparison(criterion, properties, COMPARE_TO_OPERATORS[:greater_than]).matched
138
+ end
139
+
140
+ def PROP_SEMVER_LESS_THAN(criterion, properties)
141
+ evaluate_semver_comparison(criterion, properties, COMPARE_TO_OPERATORS[:less_than]).matched
142
+ end
143
+
144
+ def PROP_SEMVER_EQUAL(criterion, properties)
145
+ evaluate_semver_comparison(criterion, properties, COMPARE_TO_OPERATORS[:equal_to]).matched
146
+ end
147
+
148
+ def PROP_SEMVER_GREATER_THAN(criterion, properties)
149
+ evaluate_semver_comparison(criterion, properties, COMPARE_TO_OPERATORS[:greater_than]).matched
150
+ end
151
+
98
152
  def value_from_properties(criterion, properties)
99
153
  criterion.property_name == NAMESPACE_KEY ? @namespace : properties.get(criterion.property_name)
100
154
  end
101
155
 
156
+ COMPARE_TO_OPERATORS = {
157
+ less_than_or_equal: -> cmp { cmp <= 0 },
158
+ less_than: -> cmp { cmp < 0 },
159
+ equal_to: -> cmp { cmp == 0 },
160
+ greater_than: -> cmp { cmp > 0 },
161
+ greater_than_or_equal: -> cmp { cmp >= 0 },
162
+ }
163
+
102
164
  private
103
165
 
166
+ def evaluate_semver_comparison(criterion, properties, predicate)
167
+ context_version = value_from_properties(criterion, properties)&.then { |v| SemanticVersion.parse_quietly(v) }
168
+ config_version = criterion.value_to_match&.string&.then {|v| SemanticVersion.parse_quietly(criterion.value_to_match.string) }
169
+
170
+ unless context_version && config_version
171
+ return MatchResult.error
172
+ end
173
+ predicate.call(context_version <=> config_version) ? MatchResult.matched : MatchResult.not_matched
174
+ end
175
+
176
+ def evaluate_date_comparison(criterion, properties, predicate)
177
+ context_millis = as_millis(value_from_properties(criterion, properties))
178
+ config_millis = as_millis(Prefab::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config,
179
+ properties, @resolver).unwrap)
180
+
181
+ unless config_millis && context_millis
182
+ return MatchResult.error
183
+ end
184
+
185
+ predicate.call(context_millis <=> config_millis) ? MatchResult.matched : MatchResult.not_matched
186
+ end
187
+
188
+ def evaluate_number_comparison(criterion, properties, predicate)
189
+ context_value = value_from_properties(criterion, properties)
190
+ value_to_match = extract_numeric_value(criterion.value_to_match)
191
+
192
+ return MatchResult.error if value_to_match.nil?
193
+ return MatchResult.error unless context_value.is_a?(Numeric)
194
+
195
+ # Compare the values and apply the predicate method
196
+ comparison_result = context_value <=> value_to_match
197
+ return MatchResult.error if comparison_result.nil?
198
+
199
+ predicate.call(comparison_result) ? MatchResult.matched : MatchResult.not_matched
200
+ end
201
+
202
+ def extract_numeric_value(config_value)
203
+ case config_value.type
204
+ when :int
205
+ config_value.int
206
+ when :double
207
+ config_value.double
208
+ end
209
+ end
210
+
211
+ def as_millis(obj)
212
+ if obj.is_a?(Numeric)
213
+ return obj.to_int if obj.respond_to?(:to_int)
214
+ end
215
+ if obj.is_a?(String)
216
+ Time.iso8601(obj).utc.to_i * 1000 rescue nil
217
+ end
218
+ end
219
+
220
+
104
221
  def evaluate_for_env(env_id, properties)
105
222
  @config.rows.each_with_index do |row, index|
106
223
  next unless row.project_env_id == env_id
@@ -131,7 +248,7 @@ module Prefab
131
248
  end
132
249
 
133
250
  def matches?(criterion, value, properties)
134
- criterion_value_or_values = Prefab::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config.key,
251
+ criterion_value_or_values = Prefab::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config,
135
252
  properties, @resolver).unwrap
136
253
 
137
254
  case criterion_value_or_values
@@ -167,6 +284,60 @@ module Prefab
167
284
  value.include?(substring)
168
285
  end
169
286
  end
287
+
288
+ def check_regex_match(criterion, properties)
289
+ begin
290
+ regex_definition = Prefab::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config.key,
291
+ properties, @resolver).unwrap
292
+
293
+ return MatchResult.error unless regex_definition.is_a?(String)
294
+
295
+ value = value_from_properties(criterion, properties)
296
+
297
+ regex = compile_regex_safely(ensure_anchored_regex(regex_definition))
298
+ return MatchResult.error unless regex
299
+
300
+ matches = regex.match?(value.to_s)
301
+ matches ? MatchResult.matched : MatchResult.not_matched
302
+ rescue RegexpError
303
+ MatchResult.error
304
+ end
305
+ end
306
+
307
+ def compile_regex_safely(pattern)
308
+ begin
309
+ Regexp.new(pattern)
310
+ rescue RegexpError
311
+ nil
312
+ end
313
+ end
314
+
315
+ def ensure_anchored_regex(pattern)
316
+ return pattern if pattern.start_with?("^") && pattern.end_with?("$")
317
+
318
+ "^#{pattern}$"
319
+ end
320
+
321
+ class MatchResult
322
+ attr_reader :matched, :error
323
+
324
+ def self.matched
325
+ new(matched: true)
326
+ end
327
+
328
+ def self.not_matched
329
+ new(matched: false)
330
+ end
331
+
332
+ def self.error
333
+ new(matched: false, error: true)
334
+ end
335
+
336
+ def initialize(matched:, error: false)
337
+ @matched = matched
338
+ @error = error
339
+ end
340
+ end
170
341
  end
171
342
  end
172
343
  # rubocop:enable Naming/MethodName
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ module Prefab
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
@@ -9,13 +9,17 @@ module Prefab
9
9
  'X-PrefabCloud-Client-Version' => "prefab-cloud-ruby-#{Prefab::VERSION}"
10
10
  }.freeze
11
11
 
12
- def initialize(api_root, api_key)
13
- @api_root = api_root
12
+ def initialize(uri, api_key)
13
+ @uri = uri
14
14
  @api_key = api_key
15
15
  end
16
16
 
17
- def get(path)
18
- connection(PROTO_HEADERS).get(path)
17
+ def uri
18
+ @uri
19
+ end
20
+
21
+ def get(path, headers = {})
22
+ connection(PROTO_HEADERS.merge(headers)).get(path)
19
23
  end
20
24
 
21
25
  def post(path, body)
@@ -24,13 +28,13 @@ module Prefab
24
28
 
25
29
  def connection(headers = {})
26
30
  if Faraday::VERSION[0].to_i >= 2
27
- Faraday.new(@api_root) do |conn|
31
+ Faraday.new(@uri) do |conn|
28
32
  conn.request :authorization, :basic, AUTH_USER, @api_key
29
33
 
30
34
  conn.headers.merge!(headers)
31
35
  end
32
36
  else
33
- Faraday.new(@api_root) do |conn|
37
+ Faraday.new(@uri) do |conn|
34
38
  conn.request :basic_auth, AUTH_USER, @api_key
35
39
 
36
40
  conn.headers.merge!(headers)
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SemanticVersion
4
+ include Comparable
5
+
6
+ SEMVER_PATTERN = /
7
+ ^
8
+ (?<major>0|[1-9]\d*)
9
+ \.
10
+ (?<minor>0|[1-9]\d*)
11
+ \.
12
+ (?<patch>0|[1-9]\d*)
13
+ (?:-(?<prerelease>
14
+ (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
15
+ (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
16
+ ))?
17
+ (?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
18
+ $
19
+ /x
20
+
21
+ attr_reader :major, :minor, :patch, :prerelease, :build_metadata
22
+
23
+ def self.parse(version_string)
24
+ raise ArgumentError, "version string cannot be nil" if version_string.nil?
25
+ raise ArgumentError, "version string cannot be empty" if version_string.empty?
26
+
27
+ match = SEMVER_PATTERN.match(version_string)
28
+ raise ArgumentError, "invalid semantic version format: #{version_string}" unless match
29
+
30
+ new(
31
+ major: match[:major].to_i,
32
+ minor: match[:minor].to_i,
33
+ patch: match[:patch].to_i,
34
+ prerelease: match[:prerelease],
35
+ build_metadata: match[:buildmetadata]
36
+ )
37
+ end
38
+
39
+ def self.parse_quietly(version_string)
40
+ parse(version_string)
41
+ rescue ArgumentError
42
+ nil
43
+ end
44
+
45
+ def initialize(major:, minor:, patch:, prerelease: nil, build_metadata: nil)
46
+ @major = major
47
+ @minor = minor
48
+ @patch = patch
49
+ @prerelease = prerelease
50
+ @build_metadata = build_metadata
51
+ end
52
+
53
+ def <=>(other)
54
+ return nil unless other.is_a?(SemanticVersion)
55
+
56
+ # Compare major.minor.patch
57
+ return major <=> other.major if major != other.major
58
+ return minor <=> other.minor if minor != other.minor
59
+ return patch <=> other.patch if patch != other.patch
60
+
61
+ # Compare pre-release versions
62
+ compare_prerelease(prerelease, other.prerelease)
63
+ end
64
+
65
+ def ==(other)
66
+ return false unless other.is_a?(SemanticVersion)
67
+
68
+ major == other.major &&
69
+ minor == other.minor &&
70
+ patch == other.patch &&
71
+ prerelease == other.prerelease
72
+ # Build metadata is ignored in equality checks
73
+ end
74
+
75
+ def eql?(other)
76
+ self == other
77
+ end
78
+
79
+ def hash
80
+ [major, minor, patch, prerelease].hash
81
+ end
82
+
83
+ def to_s
84
+ result = "#{major}.#{minor}.#{patch}"
85
+ result += "-#{prerelease}" if prerelease
86
+ result += "+#{build_metadata}" if build_metadata
87
+ result
88
+ end
89
+
90
+ private
91
+
92
+ def self.numeric?(str)
93
+ str.to_i.to_s == str
94
+ end
95
+
96
+ def compare_prerelease(pre1, pre2)
97
+ # If both are empty, they're equal
98
+ return 0 if pre1.nil? && pre2.nil?
99
+
100
+ # A version without prerelease has higher precedence
101
+ return 1 if pre1.nil?
102
+ return -1 if pre2.nil?
103
+
104
+ # Split into identifiers
105
+ ids1 = pre1.split('.')
106
+ ids2 = pre2.split('.')
107
+
108
+ # Compare each identifier until we find a difference
109
+ [ids1.length, ids2.length].min.times do |i|
110
+ cmp = compare_prerelease_identifiers(ids1[i], ids2[i])
111
+ return cmp if cmp != 0
112
+ end
113
+
114
+ # If all identifiers match up to the length of the shorter one,
115
+ # the longer one has higher precedence
116
+ ids1.length <=> ids2.length
117
+ end
118
+
119
+ def compare_prerelease_identifiers(id1, id2)
120
+ # If both are numeric, compare numerically
121
+ if self.class.numeric?(id1) && self.class.numeric?(id2)
122
+ return id1.to_i <=> id2.to_i
123
+ end
124
+
125
+ # If only one is numeric, numeric ones have lower precedence
126
+ return -1 if self.class.numeric?(id1)
127
+ return 1 if self.class.numeric?(id2)
128
+
129
+ # Neither is numeric, compare as strings
130
+ id1 <=> id2
131
+ end
132
+ end
@@ -54,3 +54,6 @@ require 'prefab/feature_flag_client'
54
54
  require 'prefab/prefab'
55
55
  require 'prefab/murmer3'
56
56
  require 'prefab/javascript_stub'
57
+ require 'prefab/semver'
58
+ require 'prefab/fixed_size_hash'
59
+ require 'prefab/caching_http_connection'