launchdarkly-server-sdk 8.11.2 → 8.11.3

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +66 -3
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +243 -0
  5. data/lib/ldclient-rb/events.rb +34 -19
  6. data/lib/ldclient-rb/flags_state.rb +1 -1
  7. data/lib/ldclient-rb/impl/big_segments.rb +4 -4
  8. data/lib/ldclient-rb/impl/cache_store.rb +44 -0
  9. data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
  10. data/lib/ldclient-rb/impl/data_source/requestor.rb +106 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +78 -0
  12. data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
  13. data/lib/ldclient-rb/impl/data_source.rb +3 -3
  14. data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
  15. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
  16. data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
  17. data/lib/ldclient-rb/impl/data_store/status_provider.rb +82 -0
  18. data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
  19. data/lib/ldclient-rb/impl/data_store.rb +11 -97
  20. data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
  21. data/lib/ldclient-rb/impl/data_system/fdv2.rb +471 -0
  22. data/lib/ldclient-rb/impl/data_system/polling.rb +601 -0
  23. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  24. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  25. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  26. data/lib/ldclient-rb/impl/event_sender.rb +4 -3
  27. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  28. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  29. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +288 -0
  30. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  31. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  32. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  33. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  34. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  35. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  36. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  37. data/lib/ldclient-rb/impl/util.rb +65 -0
  38. data/lib/ldclient-rb/impl.rb +1 -2
  39. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  40. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  41. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  42. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  43. data/lib/ldclient-rb/integrations/test_data_v2.rb +248 -0
  44. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  45. data/lib/ldclient-rb/interfaces/data_system.rb +755 -0
  46. data/lib/ldclient-rb/interfaces/feature_store.rb +3 -0
  47. data/lib/ldclient-rb/ldclient.rb +55 -131
  48. data/lib/ldclient-rb/util.rb +11 -70
  49. data/lib/ldclient-rb/version.rb +1 -1
  50. data/lib/ldclient-rb.rb +8 -17
  51. metadata +35 -17
  52. data/lib/ldclient-rb/cache_store.rb +0 -45
  53. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  54. data/lib/ldclient-rb/memoized_value.rb +0 -32
  55. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  56. data/lib/ldclient-rb/polling.rb +0 -102
  57. data/lib/ldclient-rb/requestor.rb +0 -102
  58. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  59. data/lib/ldclient-rb/stream.rb +0 -197
@@ -0,0 +1,601 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ldclient-rb/interfaces"
4
+ require "ldclient-rb/interfaces/data_system"
5
+ require "ldclient-rb/impl/data_system"
6
+ require "ldclient-rb/impl/data_system/protocolv2"
7
+ require "ldclient-rb/impl/data_source/requestor"
8
+ require "ldclient-rb/impl/util"
9
+ require "concurrent"
10
+ require "json"
11
+ require "uri"
12
+ require "http"
13
+
14
+ module LaunchDarkly
15
+ module Impl
16
+ module DataSystem
17
+ FDV2_POLLING_ENDPOINT = "/sdk/poll"
18
+ FDV1_POLLING_ENDPOINT = "/sdk/latest-all"
19
+
20
+ LD_ENVID_HEADER = "x-launchdarkly-env-id"
21
+ LD_FD_FALLBACK_HEADER = "x-launchdarkly-fd-fallback"
22
+
23
+ #
24
+ # Requester protocol for polling data source
25
+ #
26
+ module Requester
27
+ #
28
+ # Fetches the data for the given selector.
29
+ # Returns a Result containing a tuple of [ChangeSet, headers],
30
+ # or an error if the data could not be retrieved.
31
+ #
32
+ # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
33
+ # @return [Result]
34
+ #
35
+ def fetch(selector)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ #
40
+ # Closes any persistent connections and releases resources.
41
+ # This method should be called when the requester is no longer needed.
42
+ # Implementations should handle being called multiple times gracefully.
43
+ #
44
+ def stop
45
+ # Optional - implementations may override if they need cleanup
46
+ end
47
+ end
48
+
49
+ #
50
+ # PollingDataSource is a data source that can retrieve information from
51
+ # LaunchDarkly either as an Initializer or as a Synchronizer.
52
+ #
53
+ class PollingDataSource
54
+ include LaunchDarkly::Interfaces::DataSystem::Initializer
55
+ include LaunchDarkly::Interfaces::DataSystem::Synchronizer
56
+
57
+ attr_reader :name
58
+
59
+ #
60
+ # @param poll_interval [Float] Polling interval in seconds
61
+ # @param requester [Requester] The requester to use for fetching data
62
+ # @param logger [Logger] The logger
63
+ #
64
+ def initialize(poll_interval, requester, logger)
65
+ @requester = requester
66
+ @poll_interval = poll_interval
67
+ @logger = logger
68
+ @interrupt_event = Concurrent::Event.new
69
+ @stop = Concurrent::Event.new
70
+ @name = "PollingDataSourceV2"
71
+ end
72
+
73
+ #
74
+ # Fetch returns a Basis, or an error if the Basis could not be retrieved.
75
+ #
76
+ # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
77
+ # @return [LaunchDarkly::Interfaces::DataSystem::Basis, nil]
78
+ #
79
+ def fetch(ss)
80
+ poll(ss)
81
+ ensure
82
+ # Ensure the requester is stopped to avoid leaving open connections.
83
+ @requester.stop if @requester.respond_to?(:stop)
84
+ end
85
+
86
+ #
87
+ # sync begins the synchronization process for the data source, yielding
88
+ # Update objects until the connection is closed or an unrecoverable error
89
+ # occurs.
90
+ #
91
+ # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
92
+ # @yieldparam update [LaunchDarkly::Interfaces::DataSystem::Update]
93
+ #
94
+ def sync(ss)
95
+ @logger.info { "[LDClient] Starting PollingDataSourceV2 synchronizer" }
96
+ @stop.reset
97
+ @interrupt_event.reset
98
+
99
+ until @stop.set?
100
+ result = @requester.fetch(ss.selector)
101
+
102
+ if !result.success?
103
+ fallback = false
104
+ envid = nil
105
+
106
+ if result.headers
107
+ fallback = result.headers[LD_FD_FALLBACK_HEADER] == 'true'
108
+ envid = result.headers[LD_ENVID_HEADER]
109
+ end
110
+
111
+ if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
112
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
113
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE,
114
+ result.exception.status,
115
+ Impl::Util.http_error_message(
116
+ result.exception.status, "polling request", "will retry"
117
+ ),
118
+ Time.now
119
+ )
120
+
121
+ status_code = result.exception.status
122
+ if Impl::Util.http_error_recoverable?(status_code)
123
+ # If fallback is requested, send OFF status to signal shutdown
124
+ if fallback
125
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
126
+ state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
127
+ error: error_info,
128
+ environment_id: envid,
129
+ revert_to_fdv1: true
130
+ )
131
+ break
132
+ end
133
+
134
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
135
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
136
+ error: error_info,
137
+ environment_id: envid,
138
+ revert_to_fdv1: false
139
+ )
140
+ @interrupt_event.wait(@poll_interval)
141
+ next
142
+ end
143
+
144
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
145
+ state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
146
+ error: error_info,
147
+ environment_id: envid,
148
+ revert_to_fdv1: fallback
149
+ )
150
+ break
151
+ end
152
+
153
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
154
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::NETWORK_ERROR,
155
+ 0,
156
+ result.error,
157
+ Time.now
158
+ )
159
+
160
+ # If fallback is requested, send OFF status to signal shutdown
161
+ if fallback
162
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
163
+ state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
164
+ error: error_info,
165
+ environment_id: envid,
166
+ revert_to_fdv1: true
167
+ )
168
+ break
169
+ end
170
+
171
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
172
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
173
+ error: error_info,
174
+ environment_id: envid,
175
+ revert_to_fdv1: false
176
+ )
177
+ else
178
+ change_set, headers = result.value
179
+ fallback = headers[LD_FD_FALLBACK_HEADER] == 'true'
180
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
181
+ state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
182
+ change_set: change_set,
183
+ environment_id: headers[LD_ENVID_HEADER],
184
+ revert_to_fdv1: fallback
185
+ )
186
+ end
187
+
188
+ break if fallback
189
+ break if @interrupt_event.wait(@poll_interval)
190
+ end
191
+ ensure
192
+ # Ensure the requester is stopped to avoid leaving open connections.
193
+ @requester.stop if @requester.respond_to?(:stop)
194
+ end
195
+
196
+ #
197
+ # Stops the synchronizer.
198
+ #
199
+ def stop
200
+ @logger.info { "[LDClient] Stopping PollingDataSourceV2 synchronizer" }
201
+ @interrupt_event.set
202
+ @stop.set
203
+ end
204
+
205
+ #
206
+ # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
207
+ # @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::Basis, String>]
208
+ #
209
+ private def poll(ss)
210
+ result = @requester.fetch(ss.selector)
211
+
212
+ unless result.success?
213
+ if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
214
+ status_code = result.exception.status
215
+ http_error_message_result = Impl::Util.http_error_message(
216
+ status_code, "polling request", "will retry"
217
+ )
218
+ @logger.warn { "[LDClient] #{http_error_message_result}" } if Impl::Util.http_error_recoverable?(status_code)
219
+ return LaunchDarkly::Result.fail(http_error_message_result, result.exception)
220
+ end
221
+
222
+ return LaunchDarkly::Result.fail(result.error || 'Failed to request payload', result.exception)
223
+ end
224
+
225
+ change_set, headers = result.value
226
+
227
+ env_id = headers[LD_ENVID_HEADER]
228
+ env_id = nil unless env_id.is_a?(String)
229
+
230
+ basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
231
+ change_set: change_set,
232
+ persist: change_set.selector.defined?,
233
+ environment_id: env_id
234
+ )
235
+
236
+ LaunchDarkly::Result.success(basis)
237
+ rescue => e
238
+ msg = "Error: Exception encountered when updating flags. #{e}"
239
+ @logger.error { "[LDClient] #{msg}" }
240
+ @logger.debug { "[LDClient] Exception trace: #{e.backtrace}" }
241
+ LaunchDarkly::Result.fail(msg, e)
242
+ end
243
+ end
244
+
245
+ #
246
+ # HTTPPollingRequester is a Requester that uses HTTP to make
247
+ # requests to the FDv2 polling endpoint.
248
+ #
249
+ class HTTPPollingRequester
250
+ include Requester
251
+
252
+ #
253
+ # @param sdk_key [String]
254
+ # @param config [LaunchDarkly::Config]
255
+ #
256
+ def initialize(sdk_key, config)
257
+ @etag = nil
258
+ @config = config
259
+ @sdk_key = sdk_key
260
+ @poll_uri = config.base_uri + FDV2_POLLING_ENDPOINT
261
+ @http_client = Impl::Util.new_http_client(config.base_uri, config)
262
+ .use(:auto_inflate)
263
+ .headers("Accept-Encoding" => "gzip")
264
+ end
265
+
266
+ #
267
+ # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
268
+ # @return [Result]
269
+ #
270
+ def fetch(selector)
271
+ query_params = []
272
+ query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
273
+
274
+ if selector && selector.defined?
275
+ query_params << ["selector", selector.state]
276
+ end
277
+
278
+ uri = @poll_uri
279
+ if query_params.any?
280
+ filter_query = URI.encode_www_form(query_params)
281
+ uri = "#{uri}?#{filter_query}"
282
+ end
283
+
284
+ headers = {}
285
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
286
+ headers["If-None-Match"] = @etag unless @etag.nil?
287
+
288
+ begin
289
+ response = @http_client.request("GET", uri, headers: headers)
290
+ status = response.status.code
291
+ response_headers = response.headers.to_h.transform_keys(&:downcase)
292
+
293
+ if status >= 400
294
+ return LaunchDarkly::Result.fail(
295
+ "HTTP error #{status}",
296
+ LaunchDarkly::Impl::DataSource::UnexpectedResponseError.new(status),
297
+ response_headers
298
+ )
299
+ end
300
+
301
+ if status == 304
302
+ return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
303
+ end
304
+
305
+ body = response.to_s
306
+ data = JSON.parse(body, symbolize_names: true)
307
+ etag = response_headers["etag"]
308
+ @etag = etag unless etag.nil?
309
+
310
+ @config.logger.debug { "[LDClient] #{uri} response status:[#{status}] ETag:[#{etag}]" }
311
+
312
+ changeset_result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(data)
313
+ if changeset_result.success?
314
+ LaunchDarkly::Result.success([changeset_result.value, response_headers])
315
+ else
316
+ LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
317
+ end
318
+ rescue JSON::ParserError => e
319
+ LaunchDarkly::Result.fail("Failed to parse JSON: #{e.message}", e, response_headers)
320
+ rescue => e
321
+ LaunchDarkly::Result.fail("Network error: #{e.message}", e)
322
+ end
323
+ end
324
+
325
+ #
326
+ # Closes the HTTP client and releases any persistent connections.
327
+ #
328
+ def stop
329
+ begin
330
+ @http_client.close if @http_client
331
+ rescue
332
+ end
333
+ end
334
+ end
335
+
336
+ #
337
+ # HTTPFDv1PollingRequester is a Requester that uses HTTP to make
338
+ # requests to the FDv1 polling endpoint.
339
+ #
340
+ class HTTPFDv1PollingRequester
341
+ include Requester
342
+
343
+ #
344
+ # @param sdk_key [String]
345
+ # @param config [LaunchDarkly::Config]
346
+ #
347
+ def initialize(sdk_key, config)
348
+ @etag = nil
349
+ @config = config
350
+ @sdk_key = sdk_key
351
+ @poll_uri = config.base_uri + FDV1_POLLING_ENDPOINT
352
+ @http_client = Impl::Util.new_http_client(config.base_uri, config)
353
+ .use(:auto_inflate)
354
+ .headers("Accept-Encoding" => "gzip")
355
+ end
356
+
357
+ #
358
+ # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
359
+ # @return [Result]
360
+ #
361
+ def fetch(selector)
362
+ query_params = []
363
+ query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
364
+
365
+ uri = @poll_uri
366
+ if query_params.any?
367
+ filter_query = URI.encode_www_form(query_params)
368
+ uri = "#{uri}?#{filter_query}"
369
+ end
370
+
371
+ headers = {}
372
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
373
+ headers["If-None-Match"] = @etag unless @etag.nil?
374
+
375
+ begin
376
+ response = @http_client.request("GET", uri, headers: headers)
377
+ status = response.status.code
378
+ response_headers = response.headers.to_h.transform_keys(&:downcase)
379
+
380
+ if status >= 400
381
+ return LaunchDarkly::Result.fail(
382
+ "HTTP error #{status}",
383
+ LaunchDarkly::Impl::DataSource::UnexpectedResponseError.new(status),
384
+ response_headers
385
+ )
386
+ end
387
+
388
+ if status == 304
389
+ return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
390
+ end
391
+
392
+ body = response.to_s
393
+ data = JSON.parse(body, symbolize_names: true)
394
+ etag = response_headers["etag"]
395
+ @etag = etag unless etag.nil?
396
+
397
+ @config.logger.debug { "[LDClient] #{uri} response status:[#{status}] ETag:[#{etag}]" }
398
+
399
+ changeset_result = LaunchDarkly::Impl::DataSystem.fdv1_polling_payload_to_changeset(data)
400
+ if changeset_result.success?
401
+ LaunchDarkly::Result.success([changeset_result.value, response_headers])
402
+ else
403
+ LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
404
+ end
405
+ rescue JSON::ParserError => e
406
+ LaunchDarkly::Result.fail("Failed to parse JSON: #{e.message}", e, response_headers)
407
+ rescue => e
408
+ LaunchDarkly::Result.fail("Network error: #{e.message}", e)
409
+ end
410
+ end
411
+
412
+ #
413
+ # Closes the HTTP client and releases any persistent connections.
414
+ #
415
+ def stop
416
+ begin
417
+ @http_client.close if @http_client
418
+ rescue
419
+ end
420
+ end
421
+ end
422
+
423
+ #
424
+ # Converts a polling payload into a ChangeSet.
425
+ #
426
+ # @param data [Hash] The polling payload
427
+ # @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::ChangeSet, String>] Result containing ChangeSet on success, or error message on failure
428
+ #
429
+ def self.polling_payload_to_changeset(data)
430
+ unless data[:events].is_a?(Array)
431
+ return LaunchDarkly::Result.fail("Invalid payload: 'events' key is missing or not a list")
432
+ end
433
+
434
+ builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
435
+
436
+ data[:events].each do |event|
437
+ unless event.is_a?(Hash)
438
+ return LaunchDarkly::Result.fail("Invalid payload: 'events' must be a list of objects")
439
+ end
440
+
441
+ next unless event[:event]
442
+
443
+ case event[:event]
444
+ when LaunchDarkly::Interfaces::DataSystem::EventName::SERVER_INTENT
445
+ begin
446
+ server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.from_h(event[:data])
447
+ rescue ArgumentError => e
448
+ return LaunchDarkly::Result.fail("Invalid JSON in server intent", e)
449
+ end
450
+
451
+ if server_intent.payload.code == LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_NONE
452
+ return LaunchDarkly::Result.success(LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes)
453
+ end
454
+
455
+ builder.start(server_intent.payload.code)
456
+
457
+ when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
458
+ begin
459
+ put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(event[:data])
460
+ rescue ArgumentError => e
461
+ return LaunchDarkly::Result.fail("Invalid JSON in put object", e)
462
+ end
463
+
464
+ builder.add_put(put.kind, put.key, put.version, put.object)
465
+
466
+ when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
467
+ begin
468
+ delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(event[:data])
469
+ rescue ArgumentError => e
470
+ return LaunchDarkly::Result.fail("Invalid JSON in delete object", e)
471
+ end
472
+
473
+ builder.add_delete(delete_object.kind, delete_object.key, delete_object.version)
474
+
475
+ when LaunchDarkly::Interfaces::DataSystem::EventName::PAYLOAD_TRANSFERRED
476
+ begin
477
+ selector = LaunchDarkly::Interfaces::DataSystem::Selector.from_h(event[:data])
478
+ changeset = builder.finish(selector)
479
+ return LaunchDarkly::Result.success(changeset)
480
+ rescue ArgumentError, RuntimeError => e
481
+ return LaunchDarkly::Result.fail("Invalid JSON in payload transferred object", e)
482
+ end
483
+ end
484
+ end
485
+
486
+ LaunchDarkly::Result.fail("didn't receive any known protocol events in polling payload")
487
+ end
488
+
489
+ #
490
+ # Converts an FDv1 polling payload into a ChangeSet.
491
+ #
492
+ # @param data [Hash] The FDv1 polling payload
493
+ # @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::ChangeSet, String>] Result containing ChangeSet on success, or error message on failure
494
+ #
495
+ def self.fdv1_polling_payload_to_changeset(data)
496
+ builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
497
+ builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
498
+ selector = LaunchDarkly::Interfaces::DataSystem::Selector.no_selector
499
+
500
+ kind_mappings = [
501
+ [LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, :flags],
502
+ [LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, :segments],
503
+ ]
504
+
505
+ kind_mappings.each do |kind, fdv1_key|
506
+ kind_data = data[fdv1_key]
507
+ next if kind_data.nil?
508
+
509
+ unless kind_data.is_a?(Hash)
510
+ return LaunchDarkly::Result.fail("Invalid format: #{fdv1_key} is not an object")
511
+ end
512
+
513
+ kind_data.each do |key, flag_or_segment|
514
+ unless flag_or_segment.is_a?(Hash)
515
+ return LaunchDarkly::Result.fail("Invalid format: #{key} is not an object")
516
+ end
517
+
518
+ version = flag_or_segment[:version]
519
+ return LaunchDarkly::Result.fail("Invalid format: #{key} does not have a version set") if version.nil?
520
+
521
+ builder.add_put(kind, key.to_s, version, flag_or_segment)
522
+ end
523
+ end
524
+
525
+ LaunchDarkly::Result.success(builder.finish(selector))
526
+ end
527
+
528
+ #
529
+ # Builder for a PollingDataSource.
530
+ #
531
+ class PollingDataSourceBuilder
532
+ #
533
+ # @param sdk_key [String]
534
+ # @param config [LaunchDarkly::Config]
535
+ #
536
+ def initialize(sdk_key, config)
537
+ @sdk_key = sdk_key
538
+ @config = config
539
+ @requester = nil
540
+ end
541
+
542
+ #
543
+ # Sets a custom Requester for the PollingDataSource.
544
+ #
545
+ # @param requester [Requester]
546
+ # @return [PollingDataSourceBuilder]
547
+ #
548
+ def requester(requester)
549
+ @requester = requester
550
+ self
551
+ end
552
+
553
+ #
554
+ # Builds the PollingDataSource with the configured parameters.
555
+ #
556
+ # @return [PollingDataSource]
557
+ #
558
+ def build
559
+ requester = @requester || HTTPPollingRequester.new(@sdk_key, @config)
560
+ PollingDataSource.new(@config.poll_interval, requester, @config.logger)
561
+ end
562
+ end
563
+
564
+ #
565
+ # Builder for an FDv1 PollingDataSource.
566
+ #
567
+ class FDv1PollingDataSourceBuilder
568
+ #
569
+ # @param sdk_key [String]
570
+ # @param config [LaunchDarkly::Config]
571
+ #
572
+ def initialize(sdk_key, config)
573
+ @sdk_key = sdk_key
574
+ @config = config
575
+ @requester = nil
576
+ end
577
+
578
+ #
579
+ # Sets a custom Requester for the PollingDataSource.
580
+ #
581
+ # @param requester [Requester]
582
+ # @return [FDv1PollingDataSourceBuilder]
583
+ #
584
+ def requester(requester)
585
+ @requester = requester
586
+ self
587
+ end
588
+
589
+ #
590
+ # Builds the PollingDataSource with the configured parameters.
591
+ #
592
+ # @return [PollingDataSource]
593
+ #
594
+ def build
595
+ requester = @requester || HTTPFDv1PollingRequester.new(@sdk_key, @config)
596
+ PollingDataSource.new(@config.poll_interval, requester, @config.logger)
597
+ end
598
+ end
599
+ end
600
+ end
601
+ end