prefab-cloud-ruby 1.8.7 → 1.8.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08c5749617f280419c9737d7a0961a69f718dd8c5cc1db73066a198c0d37561c'
4
- data.tar.gz: aa211a7fcc73b06d343178ce75a8d673fd45bca7bdf40837a1da5f7217bf3c00
3
+ metadata.gz: 543a1cb8d31c7ae3efbc8e63f020803a1813e5de5129cf6adb9093bbfad16a7d
4
+ data.tar.gz: 15b576851d86a6b1c36f0dc1982392eef70fc53ea01da63419e07128ca33ae0a
5
5
  SHA512:
6
- metadata.gz: d9a6ca64f6157cb62d4fddb62d8f5f3c6b9764e37b79ba53b7901d5988008733ac6c95851a8b66056dfdfb636f5739e4a4bd15c20dd8cd157a8329913e813151
7
- data.tar.gz: 0a6470d0f65411f96e02512dbecf7806732f21b2b18431a9e0cb1e5b3a9c043d7c30c7ea5398a9abb4955b0f4be4051c4bdbaa382c25b5baa882372bb66f189d
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.7
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
@@ -64,6 +64,22 @@ module Prefab
64
64
  !PROP_ENDS_WITH_ONE_OF(criterion, properties)
65
65
  end
66
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
+
67
83
  def HIERARCHICAL_MATCH(criterion, properties)
68
84
  value = value_from_properties(criterion, properties)
69
85
  value&.start_with?(criterion.value_to_match.string)
@@ -79,12 +95,129 @@ module Prefab
79
95
  value && value >= criterion.value_to_match.int_range.start && value < criterion.value_to_match.int_range.end
80
96
  end
81
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
+
82
152
  def value_from_properties(criterion, properties)
83
153
  criterion.property_name == NAMESPACE_KEY ? @namespace : properties.get(criterion.property_name)
84
154
  end
85
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
+
86
164
  private
87
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
+
88
221
  def evaluate_for_env(env_id, properties)
89
222
  @config.rows.each_with_index do |row, index|
90
223
  next unless row.project_env_id == env_id
@@ -115,7 +248,7 @@ module Prefab
115
248
  end
116
249
 
117
250
  def matches?(criterion, value, properties)
118
- 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,
119
252
  properties, @resolver).unwrap
120
253
 
121
254
  case criterion_value_or_values
@@ -135,6 +268,76 @@ module Prefab
135
268
  value.end_with?(ending)
136
269
  end
137
270
  end
271
+
272
+ def prop_starts_with_one_of?(criterion, value)
273
+ return false unless value
274
+
275
+ criterion.value_to_match.string_list.values.any? do |beginning|
276
+ value.start_with?(beginning)
277
+ end
278
+ end
279
+
280
+ def prop_contains_one_of?(criterion, value)
281
+ return false unless value
282
+
283
+ criterion.value_to_match.string_list.values.any? do |substring|
284
+ value.include?(substring)
285
+ end
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
138
341
  end
139
342
  end
140
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'