From f8cf2cf7696f94203f13775974298b401605fd22 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Tue, 3 Nov 2026 09:24:31 -0500 Subject: [PATCH 1/4] feat: Make data kinds public and add SerializedItemDescriptor A persistent data store integration in its own gem can only use public SDK types. Today the data kinds live in LaunchDarkly::Impl::DataStore, and the Redis, DynamoDB and Consul cores call LaunchDarkly::Impl::Model.serialize and .deserialize directly. DataKind, FEATURES, SEGMENTS and ALL_KINDS move to LaunchDarkly::Interfaces::DataStore. DataKind gains serialize and deserialize, which match the DataKind serialization methods in the Go, Java and .NET SDKs. SerializedItemDescriptor (version, deleted, serialized_item) matches the type that the persistent data store interface takes in those SDKs and in js-core. The Impl names stay as aliases, so no SDK code changes. A later change adds the persistent data store interface that uses these types. --- lib/ldclient-rb/impl/data_store.rb | 16 +-- lib/ldclient-rb/impl/data_store/data_kind.rb | 49 +------ lib/ldclient-rb/interfaces/data_store.rb | 130 +++++++++++++++++++ lib/ldclient-rb/interfaces/data_system.rb | 4 +- spec/{impl => interfaces}/data_store_spec.rb | 55 +++++++- 5 files changed, 190 insertions(+), 64 deletions(-) rename spec/{impl => interfaces}/data_store_spec.rb (55%) diff --git a/lib/ldclient-rb/impl/data_store.rb b/lib/ldclient-rb/impl/data_store.rb index 1a99fbb3..d14ef779 100644 --- a/lib/ldclient-rb/impl/data_store.rb +++ b/lib/ldclient-rb/impl/data_store.rb @@ -5,19 +5,9 @@ module LaunchDarkly module Impl module DataStore - # These constants denote the types of data that can be stored in the feature store. If - # we add another storable data type in the future, as long as it follows the same pattern - # (having "key", "version", and "deleted" properties), we only need to add a corresponding - # constant here and the existing store should be able to handle it. - # - # The :priority and :get_dependency_keys properties are used by FeatureStoreDataSetSorter - # to ensure data consistency during non-atomic updates. - - FEATURES = DataKind.new(namespace: "features", priority: 1).freeze - - SEGMENTS = DataKind.new(namespace: "segments", priority: 0).freeze - - ALL_KINDS = [FEATURES, SEGMENTS].freeze + FEATURES = LaunchDarkly::Interfaces::DataStore::FEATURES + SEGMENTS = LaunchDarkly::Interfaces::DataStore::SEGMENTS + ALL_KINDS = LaunchDarkly::Interfaces::DataStore::ALL_KINDS end end end \ No newline at end of file diff --git a/lib/ldclient-rb/impl/data_store/data_kind.rb b/lib/ldclient-rb/impl/data_store/data_kind.rb index c04a1b6f..2014cd02 100644 --- a/lib/ldclient-rb/impl/data_store/data_kind.rb +++ b/lib/ldclient-rb/impl/data_store/data_kind.rb @@ -4,54 +4,7 @@ module LaunchDarkly module Impl module DataStore - class DataKind - FEATURES = "features".freeze - SEGMENTS = "segments".freeze - - FEATURE_PREREQ_FN = lambda { |flag| (flag[:prerequisites] || []).map { |p| p[:key] } }.freeze - - attr_reader :namespace - attr_reader :priority - - # - # @param namespace [String] - # @param priority [Integer] - # - def initialize(namespace:, priority:) - @namespace = namespace - @priority = priority - end - - # - # Maintain the same behavior when these data kinds were standard ruby hashes. - # - # @param key [Symbol] - # @return [Object] - # - def [](key) - return priority if key == :priority - return namespace if key == :namespace - return get_dependency_keys_fn() if key == :get_dependency_keys - nil - end - - # - # Retrieve the dependency keys for a particular data kind. Right now, this is only defined for flags. - # - def get_dependency_keys_fn() - return nil unless @namespace == FEATURES - - FEATURE_PREREQ_FN - end - - def eql?(other) - other.is_a?(DataKind) && namespace == other.namespace && priority == other.priority - end - - def hash - [namespace, priority].hash - end - end + DataKind = LaunchDarkly::Interfaces::DataStore::DataKind class StatusProvider include LaunchDarkly::Interfaces::DataStore::StatusProvider diff --git a/lib/ldclient-rb/interfaces/data_store.rb b/lib/ldclient-rb/interfaces/data_store.rb index 0865dc70..fd1ec8bc 100644 --- a/lib/ldclient-rb/interfaces/data_store.rb +++ b/lib/ldclient-rb/interfaces/data_store.rb @@ -1,6 +1,136 @@ module LaunchDarkly module Interfaces module DataStore + # + # A kind of data that the SDK keeps in a data store, such as feature flags or segments. + # + # The SDK gives a data kind to each data store operation. A persistent data store uses + # {#namespace} to keep the items of each kind apart. It can use {#deserialize} to get the + # version of an item that it has in serialized form. + # + # The SDK supplies the data kinds {FEATURES} and {SEGMENTS}. Application code does not + # usually create a data kind. + # + class DataKind + FEATURE_PREREQ_FN = lambda { |flag| (flag[:prerequisites] || []).map { |p| p[:key] } }.freeze + private_constant :FEATURE_PREREQ_FN + + # + # The name of the kind, for example "features". A data store uses this name to keep the + # items of each kind apart. + # + # @return [String] + # + attr_reader :namespace + + # + # The order in which the SDK writes this kind to a data store. The SDK writes a kind with + # a lower priority first. + # + # @return [Integer] + # + attr_reader :priority + + # + # @param namespace [String] + # @param priority [Integer] + # + def initialize(namespace:, priority:) + @namespace = namespace + @priority = priority + end + + # + # Converts an item of this kind to a string for a data store. + # + # A deleted item becomes a placeholder that has the item's key and version and the + # property "deleted" with the value true. + # + # @param item [Object] an item of this kind, or a deleted item placeholder + # @return [String] + # + def serialize(item) + LaunchDarkly::Impl::Model.serialize(self, item) + end + + # + # Converts a string from a data store to an item of this kind. + # + # @param serialized [String] the string that {#serialize} made + # @param logger [Logger, nil] logs data validation problems, if any + # @return [Object] the item, or a deleted item placeholder + # + def deserialize(serialized, logger = nil) + LaunchDarkly::Impl::Model.deserialize(self, serialized, logger) + end + + # + # Gives the properties of this kind by name, as when data kinds were hashes. + # + # @param key [Symbol] + # @return [Object] + # + def [](key) + return priority if key == :priority + return namespace if key == :namespace + return get_dependency_keys_fn() if key == :get_dependency_keys + nil + end + + # + # Gives a function that returns the keys of the items an item depends on. Only feature + # flags have dependencies. + # + # @return [Proc, nil] + # + def get_dependency_keys_fn() + return nil unless @namespace == FEATURES.namespace + + FEATURE_PREREQ_FN + end + + def eql?(other) + other.is_a?(DataKind) && namespace == other.namespace && priority == other.priority + end + + def hash + [namespace, priority].hash + end + end + + # + # The data kind for feature flags. + # + FEATURES = DataKind.new(namespace: "features", priority: 1).freeze + + # + # The data kind for segments. The SDK writes segments before feature flags, because + # feature flags can refer to segments. + # + SEGMENTS = DataKind.new(namespace: "segments", priority: 0).freeze + + # + # All of the data kinds that the SDK keeps in a data store. + # + ALL_KINDS = [FEATURES, SEGMENTS].freeze + + # + # An item in the serialized form that a persistent data store keeps. + # + # The SDK serializes items before it gives them to a persistent data store, and + # deserializes the items that the store returns. The store keeps only the serialized string + # and the version. + # + # @!attribute [r] version + # @return [Integer] the version of the item. A store that does not keep the version + # separately returns 0, and the SDK gets the version from the serialized item. + # @!attribute [r] deleted + # @return [Boolean] true if the item is a deleted item placeholder + # @!attribute [r] serialized_item + # @return [String, nil] the serialized item, from {DataKind#serialize} + # + SerializedItemDescriptor = Data.define(:version, :deleted, :serialized_item) + # # An interface for querying the status of a persistent data store. # diff --git a/lib/ldclient-rb/interfaces/data_system.rb b/lib/ldclient-rb/interfaces/data_system.rb index ef517a85..75600e47 100644 --- a/lib/ldclient-rb/interfaces/data_system.rb +++ b/lib/ldclient-rb/interfaces/data_system.rb @@ -611,7 +611,7 @@ module ReadOnlyStore # # Retrieves an item by kind and key. # - # @param kind [LaunchDarkly::Impl::DataStore::DataKind] The data kind (e.g., LaunchDarkly::Impl::DataStore::FEATURES, LaunchDarkly::Impl::DataStore::SEGMENTS) + # @param kind [LaunchDarkly::Interfaces::DataStore::DataKind] The data kind (e.g., LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS) # @param key [String] The item key # @return [Hash, nil] The item, or nil if not found or deleted # @@ -622,7 +622,7 @@ def get(kind, key) # # Retrieves all items of a given kind. # - # @param kind [LaunchDarkly::Impl::DataStore::DataKind] The data kind (e.g., LaunchDarkly::Impl::DataStore::FEATURES, LaunchDarkly::Impl::DataStore::SEGMENTS) + # @param kind [LaunchDarkly::Interfaces::DataStore::DataKind] The data kind (e.g., LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS) # @return [Hash] Hash of keys to items (excluding deleted items) # def all(kind) diff --git a/spec/impl/data_store_spec.rb b/spec/interfaces/data_store_spec.rb similarity index 55% rename from spec/impl/data_store_spec.rb rename to spec/interfaces/data_store_spec.rb index 2a1a6789..6582e91c 100644 --- a/spec/impl/data_store_spec.rb +++ b/spec/interfaces/data_store_spec.rb @@ -1,9 +1,48 @@ require "spec_helper" module LaunchDarkly - module Impl + module Interfaces module DataStore describe DataKind do + describe "serialize and deserialize" do + it "round-trips a feature flag" do + json = '{"key":"flag","version":3,"on":true}' + flag = FEATURES.deserialize(json) + expect(flag).to be_a(LaunchDarkly::Impl::Model::FeatureFlag) + expect(flag[:version]).to eq 3 + expect(JSON.parse(FEATURES.serialize(flag))).to eq JSON.parse(json) + end + + it "round-trips a segment" do + json = '{"key":"segment","version":2,"included":["a"]}' + segment = SEGMENTS.deserialize(json) + expect(segment).to be_a(LaunchDarkly::Impl::Model::Segment) + expect(segment[:version]).to eq 2 + expect(JSON.parse(SEGMENTS.serialize(segment))).to eq JSON.parse(json) + end + + it "round-trips a deleted item placeholder" do + json = '{"key":"flag","version":4,"deleted":true}' + item = FEATURES.deserialize(json) + expect(item[:deleted]).to be true + expect(item[:version]).to eq 4 + expect(JSON.parse(FEATURES.serialize(item))).to eq JSON.parse(json) + end + + it "deserializes nil to nil" do + expect(FEATURES.deserialize(nil)).to be_nil + end + end + + describe "Impl aliases" do + it "are the public constants" do + expect(LaunchDarkly::Impl::DataStore::FEATURES).to be(FEATURES) + expect(LaunchDarkly::Impl::DataStore::SEGMENTS).to be(SEGMENTS) + expect(LaunchDarkly::Impl::DataStore::ALL_KINDS).to be(ALL_KINDS) + expect(LaunchDarkly::Impl::DataStore::DataKind).to be(DataKind) + end + end + describe "eql?" do it "constant instances are equal to themselves" do expect(FEATURES.eql?(FEATURES)).to be true @@ -58,6 +97,20 @@ module DataStore end end end + + describe SerializedItemDescriptor do + it "keeps its values" do + descriptor = SerializedItemDescriptor.new(version: 5, deleted: false, serialized_item: "{}") + expect(descriptor.version).to eq 5 + expect(descriptor.deleted).to be false + expect(descriptor.serialized_item).to eq "{}" + end + + it "compares by value" do + expect(SerializedItemDescriptor.new(version: 1, deleted: true, serialized_item: nil)) + .to eq SerializedItemDescriptor.new(version: 1, deleted: true, serialized_item: nil) + end + end end end end From 1457b9d4a3235e913a068fd77c0f47440397173a Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Tue, 3 Nov 2026 09:42:46 -0500 Subject: [PATCH 2/4] feat!: Remove the Impl::DataStore data kind aliases The data kinds are now public in LaunchDarkly::Interfaces::DataStore. The LaunchDarkly::Impl::DataStore names for DataKind, FEATURES, SEGMENTS and ALL_KINDS were internal and had no compatibility guarantee, but the FDv2 store interface docs told store authors to use them. Code that used those names must use the LaunchDarkly::Interfaces::DataStore names instead. All SDK code now uses the public names. ldclient-rb/impl/data_store.rb is gone, and ldclient-rb/impl/data_store/data_kind.rb, which now holds only the status provider and update sink, is renamed to status.rb. --- lib/ldclient-rb/impl/data_source.rb | 6 +- lib/ldclient-rb/impl/data_source/stream.rb | 8 +- lib/ldclient-rb/impl/data_store.rb | 13 -- .../data_store/in_memory_feature_store.rb | 10 +- .../data_store/{data_kind.rb => status.rb} | 2 - lib/ldclient-rb/impl/data_store/store.rb | 16 +-- lib/ldclient-rb/impl/data_system/fdv1.rb | 2 +- lib/ldclient-rb/impl/dependency_tracker.rb | 6 +- .../impl/integrations/file_data_source.rb | 10 +- lib/ldclient-rb/impl/model/serialization.rb | 10 +- lib/ldclient-rb/in_memory_store.rb | 2 +- lib/ldclient-rb/integrations/test_data.rb | 12 +- lib/ldclient-rb/ldclient.rb | 10 +- spec/feature_store_spec_base.rb | 4 +- spec/impl/data_source/polling_spec.rb | 22 ++-- spec/impl/data_source/stream_spec.rb | 12 +- spec/impl/data_source_spec.rb | 36 +++--- .../in_memory_feature_store_v2_spec.rb | 120 +++++++++--------- spec/impl/data_store/store_spec.rb | 26 ++-- spec/impl/data_system/fdv2_datasystem_spec.rb | 2 +- .../impl/data_system/fdv2_persistence_spec.rb | 32 ++--- spec/impl/evaluator_spec_base.rb | 4 +- spec/impl/model/preprocessed_data_spec.rb | 2 +- spec/impl/model/serialization_spec.rb | 10 +- spec/integrations/dynamodb_stores_spec.rb | 10 +- spec/integrations/file_data_source_spec.rb | 26 ++-- spec/integrations/redis_stores_spec.rb | 16 +-- spec/integrations/test_data_spec.rb | 20 +-- spec/interfaces/data_store_spec.rb | 9 -- spec/ldclient_cached_data_warning_spec.rb | 2 +- spec/ldclient_spec.rb | 16 +-- spec/model_builders.rb | 12 +- 32 files changed, 232 insertions(+), 256 deletions(-) delete mode 100644 lib/ldclient-rb/impl/data_store.rb rename lib/ldclient-rb/impl/data_store/{data_kind.rb => status.rb} (95%) diff --git a/lib/ldclient-rb/impl/data_source.rb b/lib/ldclient-rb/impl/data_source.rb index 8459d81f..b16930a9 100644 --- a/lib/ldclient-rb/impl/data_source.rb +++ b/lib/ldclient-rb/impl/data_source.rb @@ -89,7 +89,7 @@ def init(all_data) monitor_store_update do if @flag_change_broadcaster.has_listeners? old_data = {} - Impl::DataStore::ALL_KINDS.each do |kind| + LaunchDarkly::Interfaces::DataStore::ALL_KINDS.each do |kind| old_data[kind] = @data_store.all(kind) end end @@ -193,7 +193,7 @@ def update_status(new_state, new_error) private def compute_changed_items_for_full_data_set(old_data, new_data) affected_items = Set.new - Impl::DataStore::ALL_KINDS.each do |kind| + LaunchDarkly::Interfaces::DataStore::ALL_KINDS.each do |kind| old_items = old_data[kind] || {} new_items = new_data[kind] || {} @@ -217,7 +217,7 @@ def update_status(new_state, new_error) # private def send_change_events(affected_items) affected_items.each do |item| - if item[:kind] == Impl::DataStore::FEATURES + if item[:kind] == LaunchDarkly::Interfaces::DataStore::FEATURES @flag_change_broadcaster.broadcast(LaunchDarkly::Interfaces::FlagChange.new(item[:key].to_s)) end end diff --git a/lib/ldclient-rb/impl/data_source/stream.rb b/lib/ldclient-rb/impl/data_source/stream.rb index 7e9d404b..d386f96b 100644 --- a/lib/ldclient-rb/impl/data_source/stream.rb +++ b/lib/ldclient-rb/impl/data_source/stream.rb @@ -17,8 +17,8 @@ module DataSource READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes KEY_PATHS = { - Impl::DataStore::FEATURES => "/flags/", - Impl::DataStore::SEGMENTS => "/segments/", + LaunchDarkly::Interfaces::DataStore::FEATURES => "/flags/", + LaunchDarkly::Interfaces::DataStore::SEGMENTS => "/segments/", } class StreamProcessor @@ -182,7 +182,7 @@ def process_message(message) @ready.set elsif method == PATCH data = JSON.parse(message.data, symbolize_names: true) - for kind in [Impl::DataStore::FEATURES, Impl::DataStore::SEGMENTS] + for kind in [LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS] key = key_for_path(kind, data[:path]) if key item = Impl::Model.deserialize(kind, data[:data], @config.logger) @@ -192,7 +192,7 @@ def process_message(message) end elsif method == DELETE data = JSON.parse(message.data, symbolize_names: true) - for kind in [Impl::DataStore::FEATURES, Impl::DataStore::SEGMENTS] + for kind in [LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS] key = key_for_path(kind, data[:path]) if key update_sink_or_data_store.delete(kind, key, data[:version]) diff --git a/lib/ldclient-rb/impl/data_store.rb b/lib/ldclient-rb/impl/data_store.rb deleted file mode 100644 index d14ef779..00000000 --- a/lib/ldclient-rb/impl/data_store.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'concurrent' -require "ldclient-rb/interfaces" -require "ldclient-rb/impl/data_store/data_kind" - -module LaunchDarkly - module Impl - module DataStore - FEATURES = LaunchDarkly::Interfaces::DataStore::FEATURES - SEGMENTS = LaunchDarkly::Interfaces::DataStore::SEGMENTS - ALL_KINDS = LaunchDarkly::Interfaces::DataStore::ALL_KINDS - end - end -end \ No newline at end of file diff --git a/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb b/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb index cef3de7f..b393b4d5 100644 --- a/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +++ b/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb @@ -2,7 +2,7 @@ require "concurrent" require "concurrent/atomics" -require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/data_store/status" require "ldclient-rb/interfaces/data_system" module LaunchDarkly @@ -58,7 +58,7 @@ def initialized? # # Initializes the store with a full set of data, replacing any existing data. # - # @param collections [Hash>] Hash of data kinds to collections of items + # @param collections [Hash>] Hash of data kinds to collections of items # @return [Boolean] true if successful, false otherwise # def set_basis(collections) @@ -80,7 +80,7 @@ def set_basis(collections) # # Applies a delta update to the store. # - # @param collections [Hash>] Hash of data kinds to collections with updates + # @param collections [Hash>] Hash of data kinds to collections with updates # @return [Boolean] true if successful, false otherwise # def apply_delta(collections) @@ -105,8 +105,8 @@ def apply_delta(collections) # # Decodes a collection of items. # - # @param collections [Hash>] Hash of data kinds to collections - # @return [Hash>, nil] Decoded collection with symbol keys, or nil on error + # @param collections [Hash>] Hash of data kinds to collections + # @return [Hash>, nil] Decoded collection with symbol keys, or nil on error # private def decode_collection(collections) all_decoded = {} diff --git a/lib/ldclient-rb/impl/data_store/data_kind.rb b/lib/ldclient-rb/impl/data_store/status.rb similarity index 95% rename from lib/ldclient-rb/impl/data_store/data_kind.rb rename to lib/ldclient-rb/impl/data_store/status.rb index 2014cd02..e834e522 100644 --- a/lib/ldclient-rb/impl/data_store/data_kind.rb +++ b/lib/ldclient-rb/impl/data_store/status.rb @@ -4,8 +4,6 @@ module LaunchDarkly module Impl module DataStore - DataKind = LaunchDarkly::Interfaces::DataStore::DataKind - class StatusProvider include LaunchDarkly::Interfaces::DataStore::StatusProvider diff --git a/lib/ldclient-rb/impl/data_store/store.rb b/lib/ldclient-rb/impl/data_store/store.rb index 704c82c5..e69e1d16 100644 --- a/lib/ldclient-rb/impl/data_store/store.rb +++ b/lib/ldclient-rb/impl/data_store/store.rb @@ -2,7 +2,7 @@ require "concurrent" require "set" -require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/data_store/status" require "ldclient-rb/impl/data_store/in_memory_feature_store" require "ldclient-rb/impl/dependency_tracker" require "ldclient-rb/interfaces/data_system" @@ -148,7 +148,7 @@ def commit begin # Get all data from memory store and write to persistent store all_data = {} - [FEATURES, SEGMENTS].each do |kind| + [LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS].each do |kind| all_data[kind] = @memory_store.all(kind) end @persistent_store.init(all_data) @@ -204,7 +204,7 @@ def get_data_store_status_provider old_data = nil if @flag_change_broadcaster.has_listeners? old_data = {} - [FEATURES, SEGMENTS].each do |kind| + [LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS].each do |kind| old_data[kind] = @memory_store.all(kind) end end @@ -313,12 +313,12 @@ def get_data_store_status_provider # private def changes_to_store_data(changes) all_data = { - FEATURES => {}, - SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::FEATURES => {}, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, } changes.each do |change| - kind = change.kind == LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG ? FEATURES : SEGMENTS + kind = change.kind == LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG ? LaunchDarkly::Interfaces::DataStore::FEATURES : LaunchDarkly::Interfaces::DataStore::SEGMENTS if change.action == LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT && !change.object.nil? all_data[kind][change.key] = change.object elsif change.action == LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE @@ -352,7 +352,7 @@ def get_data_store_status_provider # private def send_change_events(affected_items) affected_items.each do |item| - if item[:kind] == FEATURES + if item[:kind] == LaunchDarkly::Interfaces::DataStore::FEATURES @flag_change_broadcaster.broadcast(LaunchDarkly::Interfaces::FlagChange.new(item[:key].to_s)) end end @@ -368,7 +368,7 @@ def get_data_store_status_provider private def compute_changed_items_for_full_data_set(old_data, new_data) affected_items = Set.new - [FEATURES, SEGMENTS].each do |kind| + [LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS].each do |kind| old_items = old_data[kind] || {} new_items = new_data[kind] || {} diff --git a/lib/ldclient-rb/impl/data_system/fdv1.rb b/lib/ldclient-rb/impl/data_system/fdv1.rb index b6ba6cbc..010f91d0 100644 --- a/lib/ldclient-rb/impl/data_system/fdv1.rb +++ b/lib/ldclient-rb/impl/data_system/fdv1.rb @@ -5,7 +5,7 @@ require 'ldclient-rb/impl/data_source/polling' require 'ldclient-rb/impl/data_source/requestor' require 'ldclient-rb/impl/data_source/stream' -require 'ldclient-rb/impl/data_store' +require 'ldclient-rb/impl/data_store/status' require 'ldclient-rb/impl/data_system' require 'ldclient-rb/impl/store_client_wrapper' diff --git a/lib/ldclient-rb/impl/dependency_tracker.rb b/lib/ldclient-rb/impl/dependency_tracker.rb index 7ae3c79d..59eea8d7 100644 --- a/lib/ldclient-rb/impl/dependency_tracker.rb +++ b/lib/ldclient-rb/impl/dependency_tracker.rb @@ -42,7 +42,7 @@ def update_dependencies_from(from_kind, from_key, from_item) def self.segment_keys_from_clauses(clauses) clauses.flat_map do |clause| if clause.op == :segmentMatch - clause.values.map { |value| {kind: DataStore::SEGMENTS, key: value }} + clause.values.map { |value| {kind: LaunchDarkly::Interfaces::DataStore::SEGMENTS, key: value }} else [] end @@ -66,13 +66,13 @@ def self.compute_dependencies_from(from_kind, from_item, logger = nil) from_item end - if from_kind == DataStore::FEATURES && from_item.is_a?(LaunchDarkly::Impl::Model::FeatureFlag) + if from_kind == LaunchDarkly::Interfaces::DataStore::FEATURES && from_item.is_a?(LaunchDarkly::Impl::Model::FeatureFlag) prereq_keys = from_item.prerequisites.map { |prereq| {kind: from_kind, key: prereq.key} } segment_keys = from_item.rules.flat_map { |rule| DependencyTracker.segment_keys_from_clauses(rule.clauses) } results = Set.new(prereq_keys) results.merge(segment_keys) - elsif from_kind == DataStore::SEGMENTS && from_item.is_a?(LaunchDarkly::Impl::Model::Segment) + elsif from_kind == LaunchDarkly::Interfaces::DataStore::SEGMENTS && from_item.is_a?(LaunchDarkly::Impl::Model::Segment) kind_and_keys = from_item.rules.flat_map do |rule| DependencyTracker.segment_keys_from_clauses(rule.clauses) end diff --git a/lib/ldclient-rb/impl/integrations/file_data_source.rb b/lib/ldclient-rb/impl/integrations/file_data_source.rb index 1b3cbf8b..d5a984ca 100644 --- a/lib/ldclient-rb/impl/integrations/file_data_source.rb +++ b/lib/ldclient-rb/impl/integrations/file_data_source.rb @@ -75,8 +75,8 @@ def stop def load_all all_data = { - Impl::DataStore::FEATURES => {}, - Impl::DataStore::SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::FEATURES => {}, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, } @paths.each do |path| begin @@ -105,14 +105,14 @@ def load_file(path, all_data) parsed = parse_content(File.read(path)) (parsed[:flags] || {}).each do |key, flag| flag[:version] = version - add_item(all_data, Impl::DataStore::FEATURES, flag) + add_item(all_data, LaunchDarkly::Interfaces::DataStore::FEATURES, flag) end (parsed[:flagValues] || {}).each do |key, value| - add_item(all_data, Impl::DataStore::FEATURES, make_flag_with_value(key.to_s, value, version)) + add_item(all_data, LaunchDarkly::Interfaces::DataStore::FEATURES, make_flag_with_value(key.to_s, value, version)) end (parsed[:segments] || {}).each do |key, segment| segment[:version] = version - add_item(all_data, Impl::DataStore::SEGMENTS, segment) + add_item(all_data, LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment) end end diff --git a/lib/ldclient-rb/impl/model/serialization.rb b/lib/ldclient-rb/impl/model/serialization.rb index 978111d9..14df5ee4 100644 --- a/lib/ldclient-rb/impl/model/serialization.rb +++ b/lib/ldclient-rb/impl/model/serialization.rb @@ -34,7 +34,7 @@ module Model # SDK code outside of Impl::Model should use this method instead of calling the model class # constructors directly, so as not to rely on implementation details. # - # @param kind [Hash] normally either Impl::DataStore::FEATURES or Impl::DataStore::SEGMENTS + # @param kind [Hash] normally either LaunchDarkly::Interfaces::DataStore::FEATURES or LaunchDarkly::Interfaces::DataStore::SEGMENTS # @param input [object] a JSON string or a parsed hash (or a data model object, in which case # we'll just return the original object) # @param logger [Logger|nil] logs errors if there are any data validation problems @@ -44,9 +44,9 @@ def self.deserialize(kind, input, logger = nil) return input unless input.is_a?(String) || input.is_a?(Hash) data = input.is_a?(Hash) ? input : JSON.parse(input, symbolize_names: true) case kind - when Impl::DataStore::FEATURES + when LaunchDarkly::Interfaces::DataStore::FEATURES FeatureFlag.new(data, logger) - when Impl::DataStore::SEGMENTS + when LaunchDarkly::Interfaces::DataStore::SEGMENTS Segment.new(data, logger) else data @@ -63,8 +63,8 @@ def self.serialize(kind, item) # Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format. def self.make_all_store_data(received_data, logger = nil) { - Impl::DataStore::FEATURES => (received_data[:flags] || {}).transform_values { |data| FeatureFlag.new(data, logger) }, - Impl::DataStore::SEGMENTS => (received_data[:segments] || {}).transform_values { |data| Segment.new(data, logger) }, + LaunchDarkly::Interfaces::DataStore::FEATURES => (received_data[:flags] || {}).transform_values { |data| FeatureFlag.new(data, logger) }, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => (received_data[:segments] || {}).transform_values { |data| Segment.new(data, logger) }, } end end diff --git a/lib/ldclient-rb/in_memory_store.rb b/lib/ldclient-rb/in_memory_store.rb index 200b629c..3f212721 100644 --- a/lib/ldclient-rb/in_memory_store.rb +++ b/lib/ldclient-rb/in_memory_store.rb @@ -1,5 +1,5 @@ require "concurrent/atomics" -require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/data_store/status" module LaunchDarkly # diff --git a/lib/ldclient-rb/integrations/test_data.rb b/lib/ldclient-rb/integrations/test_data.rb index 880f4517..cbe3face 100644 --- a/lib/ldclient-rb/integrations/test_data.rb +++ b/lib/ldclient-rb/integrations/test_data.rb @@ -121,10 +121,10 @@ def update(flag_builder) if @current_flags[flag_key] version = @current_flags[flag_key][:version] end - new_flag = LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Impl::DataStore::FEATURES, flag_builder.build(version+1)) + new_flag = LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_builder.build(version+1)) @current_flags[flag_key] = new_flag end - update_item(LaunchDarkly::Impl::DataStore::FEATURES, new_flag) + update_item(LaunchDarkly::Interfaces::DataStore::FEATURES, new_flag) self end @@ -147,7 +147,7 @@ def update(flag_builder) # @return [TestData] the TestData instance # def use_preconfigured_flag(flag) - use_preconfigured_item(LaunchDarkly::Impl::DataStore::FEATURES, flag, @current_flags) + use_preconfigured_item(LaunchDarkly::Interfaces::DataStore::FEATURES, flag, @current_flags) end # @@ -167,7 +167,7 @@ def use_preconfigured_flag(flag) # @return [TestData] the TestData instance # def use_preconfigured_segment(segment) - use_preconfigured_item(LaunchDarkly::Impl::DataStore::SEGMENTS, segment, @current_segments) + use_preconfigured_item(LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment, @current_segments) end private def use_preconfigured_item(kind, item, current) @@ -198,8 +198,8 @@ def use_preconfigured_segment(segment) def make_init_data @lock.with_read_lock do { - LaunchDarkly::Impl::DataStore::FEATURES => @current_flags.clone, - LaunchDarkly::Impl::DataStore::SEGMENTS => @current_segments.clone, + LaunchDarkly::Interfaces::DataStore::FEATURES => @current_flags.clone, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => @current_segments.clone, } end end diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index 63df8c9b..c155d1a3 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -2,7 +2,7 @@ require "ldclient-rb/impl/broadcaster" require "ldclient-rb/impl/context" require "ldclient-rb/impl/data_source" -require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/data_store/status" require "ldclient-rb/impl/data_system/fdv1" require "ldclient-rb/impl/data_system/fdv2" require "ldclient-rb/impl/diagnostic_events" @@ -181,8 +181,8 @@ def postfork(wait_for_sec = 5) @big_segment_store_manager = Impl::BigSegmentStoreManager.new(@config.big_segments, @config.logger) @big_segment_store_status_provider = @big_segment_store_manager.status_provider - get_flag = lambda { |key| @data_system.store.get(Impl::DataStore::FEATURES, key) } - get_segment = lambda { |key| @data_system.store.get(Impl::DataStore::SEGMENTS, key) } + get_flag = lambda { |key| @data_system.store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, key) } + get_segment = lambda { |key| @data_system.store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, key) } get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_context_membership(key) } @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger) @@ -650,7 +650,7 @@ def all_flags_state(context, options={}) end begin - features = @data_system.store.all(Impl::DataStore::FEATURES) + features = @data_system.store.all(LaunchDarkly::Interfaces::DataStore::FEATURES) rescue => exn Impl::Util.log_exception(@config.logger, "Unable to read flags for all_flags_state", exn) return FeatureFlagsState.new(false) @@ -784,7 +784,7 @@ def flag_tracker end begin - feature = @data_system.store.get(Impl::DataStore::FEATURES, key) + feature = @data_system.store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, key) rescue # Ignored end diff --git a/spec/feature_store_spec_base.rb b/spec/feature_store_spec_base.rb index ea24bc84..2fc8d368 100644 --- a/spec/feature_store_spec_base.rb +++ b/spec/feature_store_spec_base.rb @@ -195,8 +195,8 @@ def new_version_plus(f, delta_version, attrs = {}) deleted: false, } with_inited_store([]) do |store| - store.upsert(LaunchDarkly::Impl::DataStore::FEATURES, flag) - expect(store.get(LaunchDarkly::Impl::DataStore::FEATURES, flag[:key])).to eq flag + store.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, flag) + expect(store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag[:key])).to eq flag end end end diff --git a/spec/impl/data_source/polling_spec.rb b/spec/impl/data_source/polling_spec.rb index d3bb28c6..5337e005 100644 --- a/spec/impl/data_source/polling_spec.rb +++ b/spec/impl/data_source/polling_spec.rb @@ -36,10 +36,10 @@ def with_processor(store, initialize_to_valid = false) flag = Impl::Model::FeatureFlag.new({ key: 'flagkey', version: 1 }) segment = Impl::Model::Segment.new({ key: 'segkey', version: 1 }) all_data = { - Impl::DataStore::FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { flagkey: flag, }, - Impl::DataStore::SEGMENTS => { + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { segkey: segment, }, } @@ -50,8 +50,8 @@ def with_processor(store, initialize_to_valid = false) with_processor(store) do |processor| ready = processor.start ready.wait - expect(store.get(Impl::DataStore::FEATURES, "flagkey")).to eq(flag) - expect(store.get(Impl::DataStore::SEGMENTS, "segkey")).to eq(segment) + expect(store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "flagkey")).to eq(flag) + expect(store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, "segkey")).to eq(segment) end end @@ -75,8 +75,8 @@ def with_processor(store, initialize_to_valid = false) with_processor(store) do |processor| ready = processor.start ready.wait - expect(store.get(Impl::DataStore::FEATURES, "flagkey")).to eq(flag) - expect(store.get(Impl::DataStore::SEGMENTS, "segkey")).to eq(segment) + expect(store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "flagkey")).to eq(flag) + expect(store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, "segkey")).to eq(segment) expect(listener.statuses.count).to eq(1) expect(listener.statuses[0].state).to eq(Interfaces::DataSource::Status::VALID) @@ -106,8 +106,8 @@ def with_processor(store, initialize_to_valid = false) describe 'environment ID' do flag = Impl::Model::FeatureFlag.new({ key: 'flagkey', version: 1 }) all_data = { - Impl::DataStore::FEATURES => { flagkey: flag }, - Impl::DataStore::SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flagkey: flag }, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, } it 'is recorded from the response headers' do @@ -236,7 +236,7 @@ def verify_recoverable_http_error(status) describe 'retry delay' do let(:logger) { double("logger").as_null_object } - let(:all_data) { { Impl::DataStore::FEATURES => {}, Impl::DataStore::SEGMENTS => {} } } + let(:all_data) { { LaunchDarkly::Interfaces::DataStore::FEATURES => {}, LaunchDarkly::Interfaces::DataStore::SEGMENTS => {} } } def make_processor config = Config.new(feature_store: InMemoryFeatureStore.new, logger: logger) @@ -339,8 +339,8 @@ def next_delay(processor) it 'closes requestor HTTP connections on stop' do requestor_with_stop = double("RequestorWithStop") allow(requestor_with_stop).to receive(:request_all_data).and_return({ - Impl::DataStore::FEATURES => {}, - Impl::DataStore::SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::FEATURES => {}, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, }) allow(requestor_with_stop).to receive(:stop) diff --git a/spec/impl/data_source/stream_spec.rb b/spec/impl/data_source/stream_spec.rb index 7a19e623..5438bd4b 100644 --- a/spec/impl/data_source/stream_spec.rb +++ b/spec/impl/data_source/stream_spec.rb @@ -28,26 +28,26 @@ module LaunchDarkly it "will accept PUT methods" do processor.send(:process_message, put_message) - expect(config.feature_store.get(Impl::DataStore::FEATURES, "asdf")).to eq(Flags.from_hash(key: "asdf")) - expect(config.feature_store.get(Impl::DataStore::SEGMENTS, "segkey")).to eq(Segments.from_hash(key: "segkey")) + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "asdf")).to eq(Flags.from_hash(key: "asdf")) + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, "segkey")).to eq(Segments.from_hash(key: "segkey")) end it "will accept PATCH methods for flags" do processor.send(:process_message, patch_flag_message) - expect(config.feature_store.get(Impl::DataStore::FEATURES, "asdf")).to eq(Flags.from_hash(key: "asdf", version: 1)) + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "asdf")).to eq(Flags.from_hash(key: "asdf", version: 1)) end it "will accept PATCH methods for segments" do processor.send(:process_message, patch_seg_message) - expect(config.feature_store.get(Impl::DataStore::SEGMENTS, "asdf")).to eq(Segments.from_hash(key: "asdf", version: 1)) + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, "asdf")).to eq(Segments.from_hash(key: "asdf", version: 1)) end it "will accept DELETE methods for flags" do processor.send(:process_message, patch_flag_message) processor.send(:process_message, delete_flag_message) - expect(config.feature_store.get(Impl::DataStore::FEATURES, "key")).to eq(nil) + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "key")).to eq(nil) end it "will accept DELETE methods for segments" do processor.send(:process_message, patch_seg_message) processor.send(:process_message, delete_seg_message) - expect(config.feature_store.get(Impl::DataStore::SEGMENTS, "key")).to eq(nil) + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, "key")).to eq(nil) end it "will log a warning if the method is not recognized" do expect(processor.instance_variable_get(:@config).logger).to receive :warn diff --git a/spec/impl/data_source_spec.rb b/spec/impl/data_source_spec.rb index 05ff23ec..b6da5ecd 100644 --- a/spec/impl/data_source_spec.rb +++ b/spec/impl/data_source_spec.rb @@ -99,7 +99,7 @@ module Impl describe "simple flag change listener" do let(:all_data) { { - LaunchDarkly::Impl::DataStore::FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag1: LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag1', version: 1 }), flag2: LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag2', version: 1 }), flag3: LaunchDarkly::Impl::Model::FeatureFlag.new( @@ -125,7 +125,7 @@ module Impl } ), }, - LaunchDarkly::Impl::DataStore::SEGMENTS => { + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { segment1: LaunchDarkly::Impl::Model::Segment.new({ key: 'segment1', version: 1 }), segment2: LaunchDarkly::Impl::Model::Segment.new({ key: 'segment2', version: 1 }), }, @@ -139,7 +139,7 @@ module Impl flag_change_broadcaster.add_listener(listener) updated_data = { - LaunchDarkly::Impl::DataStore::FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag1: LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag1', version: 2 }), flag4: LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag4', version: 1 }), }, @@ -160,10 +160,10 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.upsert(LaunchDarkly::Impl::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag1', version: 2 })) + sink.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag1', version: 2 })) # TODO(sc-197908): Once the store starts returning a success status on upsert, the flag change notification # can start ignoring duplicate requests like this. - # sink.upsert(LaunchDarkly::Impl::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag1', version: 2 })) + # sink.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag1', version: 2 })) expect(listener.statuses.count).to eq(1) expect(listener.statuses[0].key).to eq('flag1') @@ -175,10 +175,10 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.delete(LaunchDarkly::Impl::DataStore::FEATURES, "flag1", 2) + sink.delete(LaunchDarkly::Interfaces::DataStore::FEATURES, "flag1", 2) # TODO(sc-197908): Once the store starts returning a success status on delete, the flag change notification # can start ignoring duplicate requests like this. - # sink.delete(LaunchDarkly::Impl::DataStore::FEATURES, :flag1, 2) + # sink.delete(LaunchDarkly::Interfaces::DataStore::FEATURES, :flag1, 2) expect(listener.statuses.count).to eq(1) expect(listener.statuses[0].key).to eq("flag1") @@ -190,7 +190,7 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.upsert(LaunchDarkly::Impl::DataStore::SEGMENTS, LaunchDarkly::Impl::Model::Segment.new({ key: 'segment2', version: 2 })) + sink.upsert(LaunchDarkly::Interfaces::DataStore::SEGMENTS, LaunchDarkly::Impl::Model::Segment.new({ key: 'segment2', version: 2 })) # TODO(sc-197908): Once the store starts returning a success status on upsert, the flag change notification # can start ignoring duplicate requests like this. # sink.upsert(LaunchDarkly::Impl::Model::Segment.new({ key: 'segment2', version: 2 })) @@ -203,7 +203,7 @@ module Impl describe "prerequisite flag change listener" do let(:all_data) { { - LaunchDarkly::Impl::DataStore::FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag1: LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag1', version: 1, prerequisites: [{key: 'flag2', variation: 0}] }), flag2: LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag2', version: 1, prerequisites: [{key: 'flag3', variation: 0}, {key: 'flag4', variation: 0}, {key: 'flag6', variation: 0}] }), @@ -233,7 +233,7 @@ module Impl } ), }, - LaunchDarkly::Impl::DataStore::SEGMENTS => { + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { segment1: LaunchDarkly::Impl::Model::Segment.new({ key: 'segment1', version: 1 }), segment2: LaunchDarkly::Impl::Model::Segment.new( { @@ -268,7 +268,7 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.upsert(LaunchDarkly::Impl::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag4', version: 2 })) + sink.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag4', version: 2 })) expect(listener.statuses.count).to eq(3) expect(listener.statuses[0].key).to eq('flag4') expect(listener.statuses[1].key).to eq('flag2') @@ -281,7 +281,7 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.upsert(LaunchDarkly::Impl::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ + sink.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag3', version: 2, prerequisities: [{key: 'flag4', variation: 0}] })) expect(listener.statuses.count).to eq(3) @@ -296,7 +296,7 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.upsert(LaunchDarkly::Impl::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ + sink.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Impl::Model::FeatureFlag.new({ key: 'flag2', version: 2, prerequisities: [{key: 'flag3', variation: 0}] })) expect(listener.statuses.count).to eq(2) @@ -310,7 +310,7 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.delete(LaunchDarkly::Impl::DataStore::FEATURES, "flag4", 2) + sink.delete(LaunchDarkly::Interfaces::DataStore::FEATURES, "flag4", 2) expect(listener.statuses.count).to eq(3) expect(listener.statuses[0].key).to eq('flag4') expect(listener.statuses[1].key).to eq('flag2') @@ -323,10 +323,10 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.upsert(LaunchDarkly::Impl::DataStore::SEGMENTS, LaunchDarkly::Impl::Model::Segment.new({ key: 'segment1', version: 2 })) + sink.upsert(LaunchDarkly::Interfaces::DataStore::SEGMENTS, LaunchDarkly::Impl::Model::Segment.new({ key: 'segment1', version: 2 })) # TODO(sc-197908): Once the store starts returning a success status on upsert, the flag change notification # can start ignoring duplicate requests like this. - # sink.upsert(LaunchDarkly::Impl::DataStore::SEGMENTS, LaunchDarkly::Impl::Model::Segment.new({ key: 'segment1', version: 2 })) + # sink.upsert(LaunchDarkly::Interfaces::DataStore::SEGMENTS, LaunchDarkly::Impl::Model::Segment.new({ key: 'segment1', version: 2 })) expect(listener.statuses.count).to eq(3) expect(listener.statuses[0].key).to eq('flag6') @@ -340,10 +340,10 @@ module Impl listener = ListenerSpy.new flag_change_broadcaster.add_listener(listener) - sink.delete(LaunchDarkly::Impl::DataStore::SEGMENTS, 'segment2', 2) + sink.delete(LaunchDarkly::Interfaces::DataStore::SEGMENTS, 'segment2', 2) # TODO(sc-197908): Once the store starts returning a success status on upsert, the flag change notification # can start ignoring duplicate requests like this. - # sink.delete(LaunchDarkly::Impl::DataStore::SEGMENTS, 'segment2', 2) + # sink.delete(LaunchDarkly::Interfaces::DataStore::SEGMENTS, 'segment2', 2) expect(listener.statuses.count).to eq(3) expect(listener.statuses[0].key).to eq('flag6') diff --git a/spec/impl/data_store/in_memory_feature_store_v2_spec.rb b/spec/impl/data_store/in_memory_feature_store_v2_spec.rb index fb2d3918..f627df4e 100644 --- a/spec/impl/data_store/in_memory_feature_store_v2_spec.rb +++ b/spec/impl/data_store/in_memory_feature_store_v2_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" require "ldclient-rb/impl/data_store/in_memory_feature_store" -require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/data_store/status" module LaunchDarkly module Impl @@ -37,7 +37,7 @@ module DataStore end it "returns true after set_basis" do - subject.set_basis({ FEATURES => {} }) + subject.set_basis({ LaunchDarkly::Interfaces::DataStore::FEATURES => {} }) expect(subject.initialized?).to be true end end @@ -46,113 +46,113 @@ module DataStore before do # Store items with symbol keys (as done by FDv2 protocol layer) collections = { - FEATURES => { flag_key.to_sym => flag }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag }, } subject.set_basis(collections) end it "retrieves items with string keys (critical for variation calls)" do - result = subject.get(FEATURES, flag_key) + result = subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(result).to be_a(LaunchDarkly::Impl::Model::FeatureFlag) expect(result.key).to eq(flag_key) end it "retrieves items with symbol keys" do - result = subject.get(FEATURES, flag_key.to_sym) + result = subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key.to_sym) expect(result).to be_a(LaunchDarkly::Impl::Model::FeatureFlag) expect(result.key).to eq(flag_key) end it "returns nil for non-existent keys" do - expect(subject.get(FEATURES, "nonexistent")).to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "nonexistent")).to be_nil end it "returns nil for deleted items" do deleted_flag = flag.merge(deleted: true) - collections = { FEATURES => { flag_key.to_sym => deleted_flag } } + collections = { LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => deleted_flag } } subject.set_basis(collections) - expect(subject.get(FEATURES, flag_key)).to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).to be_nil end end describe "#all" do it "returns empty hash when no data" do - expect(subject.all(FEATURES)).to eq({}) + expect(subject.all(LaunchDarkly::Interfaces::DataStore::FEATURES)).to eq({}) end it "returns all non-deleted items" do collections = { - FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag, "deleted-flag".to_sym => flag.merge(key: "deleted-flag", deleted: true), }, } subject.set_basis(collections) - result = subject.all(FEATURES) + result = subject.all(LaunchDarkly::Interfaces::DataStore::FEATURES) expect(result.keys).to contain_exactly(flag_key.to_sym) expect(result[flag_key.to_sym].key).to eq(flag_key) end it "returns items for both flags and segments" do collections = { - FEATURES => { flag_key.to_sym => flag }, - SEGMENTS => { segment_key.to_sym => segment }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag }, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { segment_key.to_sym => segment }, } subject.set_basis(collections) - expect(subject.all(FEATURES).keys).to contain_exactly(flag_key.to_sym) - expect(subject.all(SEGMENTS).keys).to contain_exactly(segment_key.to_sym) + expect(subject.all(LaunchDarkly::Interfaces::DataStore::FEATURES).keys).to contain_exactly(flag_key.to_sym) + expect(subject.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys).to contain_exactly(segment_key.to_sym) end end describe "#set_basis" do it "initializes the store with valid data" do collections = { - FEATURES => { flag_key.to_sym => flag }, - SEGMENTS => { segment_key.to_sym => segment }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag }, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { segment_key.to_sym => segment }, } result = subject.set_basis(collections) expect(result).to be true expect(subject.initialized?).to be true - expect(subject.get(FEATURES, flag_key)).not_to be_nil - expect(subject.get(SEGMENTS, segment_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment_key)).not_to be_nil end it "replaces existing data" do # Set initial data initial_collections = { - FEATURES => { flag_key.to_sym => flag }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag }, } subject.set_basis(initial_collections) # Replace with new data new_flag = flag.merge(key: "new-flag", version: 2) new_collections = { - FEATURES => { "new-flag".to_sym => new_flag }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { "new-flag".to_sym => new_flag }, } result = subject.set_basis(new_collections) expect(result).to be true - expect(subject.get(FEATURES, flag_key)).to be_nil # Old flag gone - expect(subject.get(FEATURES, "new-flag")).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).to be_nil # Old flag gone + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "new-flag")).not_to be_nil end it "clears all data before setting new data" do subject.set_basis({ - FEATURES => { flag_key.to_sym => flag }, - SEGMENTS => { segment_key.to_sym => segment }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag }, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { segment_key.to_sym => segment }, }) # Replace with data that only has flags new_collections = { - FEATURES => { "new-flag".to_sym => flag.merge(key: "new-flag") }, - SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::FEATURES => { "new-flag".to_sym => flag.merge(key: "new-flag") }, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, } subject.set_basis(new_collections) - expect(subject.all(SEGMENTS)).to be_empty + expect(subject.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS)).to be_empty end it "handles multiple flags and segments" do @@ -164,12 +164,12 @@ module DataStore segment2 = segment.merge(key: "segment-2", version: 2) collections = { - FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { "flag-1".to_sym => flag1, "flag-2".to_sym => flag2, "flag-3".to_sym => flag3, }, - SEGMENTS => { + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { "segment-1".to_sym => segment1, "segment-2".to_sym => segment2, }, @@ -178,14 +178,14 @@ module DataStore result = subject.set_basis(collections) expect(result).to be true - expect(subject.all(FEATURES).size).to eq(3) - expect(subject.all(SEGMENTS).size).to eq(2) + expect(subject.all(LaunchDarkly::Interfaces::DataStore::FEATURES).size).to eq(3) + expect(subject.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).size).to eq(2) end it "returns false and logs error on deserialization failure" do allow(LaunchDarkly::Impl::Model).to receive(:deserialize).and_raise(StandardError.new("test error")) - collections = { FEATURES => { flag_key.to_sym => flag } } + collections = { LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag } } result = subject.set_basis(collections) expect(result).to be false @@ -193,7 +193,7 @@ module DataStore end it "handles empty collections" do - result = subject.set_basis({ FEATURES => {}, SEGMENTS => {} }) + result = subject.set_basis({ LaunchDarkly::Interfaces::DataStore::FEATURES => {}, LaunchDarkly::Interfaces::DataStore::SEGMENTS => {} }) expect(result).to be true expect(subject.initialized?).to be true end @@ -203,8 +203,8 @@ module DataStore before do # Set initial data collections = { - FEATURES => { flag_key.to_sym => flag }, - SEGMENTS => { segment_key.to_sym => segment }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag }, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { segment_key.to_sym => segment }, } subject.set_basis(collections) end @@ -212,30 +212,30 @@ module DataStore it "adds new items without clearing existing data" do new_flag = flag.merge(key: "new-flag", version: 2) delta = { - FEATURES => { "new-flag".to_sym => new_flag }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { "new-flag".to_sym => new_flag }, } result = subject.apply_delta(delta) expect(result).to be true # Original flag should still exist - expect(subject.get(FEATURES, flag_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).not_to be_nil # New flag should be added - expect(subject.get(FEATURES, "new-flag")).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "new-flag")).not_to be_nil # Segment should be unchanged - expect(subject.get(SEGMENTS, segment_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment_key)).not_to be_nil end it "updates existing items" do updated_flag = flag.merge(version: 2, on: false) delta = { - FEATURES => { flag_key.to_sym => updated_flag }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => updated_flag }, } result = subject.apply_delta(delta) expect(result).to be true - result = subject.get(FEATURES, flag_key) + result = subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(result.version).to eq(2) expect(result.on).to be false end @@ -246,11 +246,11 @@ module DataStore segment2 = segment.merge(key: "segment-2", version: 2) delta = { - FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { "flag-2".to_sym => flag2, "flag-3".to_sym => flag3, }, - SEGMENTS => { + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { "segment-2".to_sym => segment2, }, } @@ -259,52 +259,52 @@ module DataStore expect(result).to be true # Original items unchanged - expect(subject.get(FEATURES, flag_key)).not_to be_nil - expect(subject.get(SEGMENTS, segment_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment_key)).not_to be_nil # New items added - expect(subject.get(FEATURES, "flag-2")).not_to be_nil - expect(subject.get(FEATURES, "flag-3")).not_to be_nil - expect(subject.get(SEGMENTS, "segment-2")).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "flag-2")).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, "flag-3")).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, "segment-2")).not_to be_nil end it "handles delete operations" do deleted_flag = { key: flag_key, version: 2, deleted: true } delta = { - FEATURES => { flag_key.to_sym => deleted_flag }, + LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => deleted_flag }, } result = subject.apply_delta(delta) expect(result).to be true # Deleted flag should return nil - expect(subject.get(FEATURES, flag_key)).to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).to be_nil end it "returns false and logs error on deserialization failure" do allow(LaunchDarkly::Impl::Model).to receive(:deserialize).and_raise(StandardError.new("test error")) - delta = { FEATURES => { "new-flag".to_sym => flag } } + delta = { LaunchDarkly::Interfaces::DataStore::FEATURES => { "new-flag".to_sym => flag } } result = subject.apply_delta(delta) expect(result).to be false # Original data should be intact - expect(subject.get(FEATURES, flag_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).not_to be_nil end it "handles empty delta" do - result = subject.apply_delta({ FEATURES => {}, SEGMENTS => {} }) + result = subject.apply_delta({ LaunchDarkly::Interfaces::DataStore::FEATURES => {}, LaunchDarkly::Interfaces::DataStore::SEGMENTS => {} }) expect(result).to be true # Original data unchanged - expect(subject.get(FEATURES, flag_key)).not_to be_nil - expect(subject.get(SEGMENTS, segment_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).not_to be_nil + expect(subject.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment_key)).not_to be_nil end end describe "thread safety" do it "handles concurrent reads and writes" do - subject.set_basis({ FEATURES => { flag_key.to_sym => flag } }) + subject.set_basis({ LaunchDarkly::Interfaces::DataStore::FEATURES => { flag_key.to_sym => flag } }) threads = [] errors = [] @@ -315,7 +315,7 @@ module DataStore begin 10.times do |j| new_flag = flag.merge(key: "flag-#{i}-#{j}", version: j + 1) - subject.apply_delta({ FEATURES => { "flag-#{i}-#{j}".to_sym => new_flag } }) + subject.apply_delta({ LaunchDarkly::Interfaces::DataStore::FEATURES => { "flag-#{i}-#{j}".to_sym => new_flag } }) end rescue => e errors << e @@ -328,8 +328,8 @@ module DataStore threads << Thread.new do begin 20.times do - subject.get(FEATURES, flag_key) - subject.all(FEATURES) + subject.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) + subject.all(LaunchDarkly::Interfaces::DataStore::FEATURES) end rescue => e errors << e diff --git a/spec/impl/data_store/store_spec.rb b/spec/impl/data_store/store_spec.rb index 546371d3..e609721e 100644 --- a/spec/impl/data_store/store_spec.rb +++ b/spec/impl/data_store/store_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" require "ldclient-rb/impl/data_store/store" require "ldclient-rb/impl/broadcaster" -require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/data_store/status" require "ldclient-rb/interfaces/data_system" module LaunchDarkly @@ -45,8 +45,8 @@ class StubPersistentStore def initialize(should_fail: false) @data = { - LaunchDarkly::Impl::DataStore::FEATURES => {}, - LaunchDarkly::Impl::DataStore::SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::FEATURES => {}, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, } @initialized = false @init_called_count = 0 @@ -67,8 +67,8 @@ def init(all_data) @init_errors << error raise error end - @data[LaunchDarkly::Impl::DataStore::FEATURES] = (all_data[LaunchDarkly::Impl::DataStore::FEATURES] || {}).dup - @data[LaunchDarkly::Impl::DataStore::SEGMENTS] = (all_data[LaunchDarkly::Impl::DataStore::SEGMENTS] || {}).dup + @data[LaunchDarkly::Interfaces::DataStore::FEATURES] = (all_data[LaunchDarkly::Interfaces::DataStore::FEATURES] || {}).dup + @data[LaunchDarkly::Interfaces::DataStore::SEGMENTS] = (all_data[LaunchDarkly::Interfaces::DataStore::SEGMENTS] || {}).dup @initialized = true end @@ -161,7 +161,7 @@ def disable_cache expect(subject.initialized?).to be true # InMemoryFeatureStoreV2's get method handles both string and symbol keys - result = subject.get_active_store.get(FEATURES, flag_key) + result = subject.get_active_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(result).not_to be_nil expect(result.key).to eq(flag_key.to_s) # key field is string end @@ -202,7 +202,7 @@ def disable_cache subject.apply(delta_change_set, false) - result = subject.get_active_store.get(FEATURES, flag_key) + result = subject.get_active_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(result.version).to eq(2) expect(result.on).to be false end @@ -235,7 +235,7 @@ def disable_cache ) subject.apply(init_change_set, false) - expect(subject.get_active_store.get(FEATURES, flag_key)).not_to be_nil + expect(subject.get_active_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).not_to be_nil # Delete the flag delete_change = LaunchDarkly::Interfaces::DataSystem::Change.new( @@ -253,7 +253,7 @@ def disable_cache ) subject.apply(delete_change_set, false) - expect(subject.get_active_store.get(FEATURES, flag_key)).to be_nil + expect(subject.get_active_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key)).to be_nil end it "broadcasts changeset to listeners" do @@ -428,7 +428,7 @@ def changed_flags expect(persistent_store.init_called_count).to eq(1) # Verify data in persistent store - stored_flag = persistent_store.get(FEATURES, flag_key) + stored_flag = persistent_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(stored_flag).not_to be_nil expect(stored_flag[:key]).to eq(flag_key.to_s) # key field is string end @@ -459,7 +459,7 @@ def changed_flags error = subject.commit expect(error).to be_nil - stored_flag = persistent_store.get(FEATURES, flag_key) + stored_flag = persistent_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(stored_flag[:key]).to eq(flag_key.to_s) # key field is string expect(stored_flag[:version]).to eq(1) end @@ -548,7 +548,7 @@ def changed_flags subject.apply(change_set, true) expect(persistent_store.init_called_count).to be >= 1 - stored_flag = persistent_store.get(FEATURES, flag_key) + stored_flag = persistent_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(stored_flag).not_to be_nil end @@ -631,7 +631,7 @@ def changed_flags expect(persistent_store.upsert_calls).not_to be_empty # Verify deleted flag was written - stored_flag = persistent_store.get(FEATURES, flag_key) + stored_flag = persistent_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag_key) expect(stored_flag).to be_nil # get returns nil for deleted items end end diff --git a/spec/impl/data_system/fdv2_datasystem_spec.rb b/spec/impl/data_system/fdv2_datasystem_spec.rb index b9bc4dd0..82d8e9ad 100644 --- a/spec/impl/data_system/fdv2_datasystem_spec.rb +++ b/spec/impl/data_system/fdv2_datasystem_spec.rb @@ -553,7 +553,7 @@ def with_data_system(initializers, synchronizers) # Verify FDv1 is serving data store = fdv2.store - flag = store.get(LaunchDarkly::Impl::DataStore::FEATURES, :fdv1flag) + flag = store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, :fdv1flag) expect(flag).not_to be_nil fdv2.stop diff --git a/spec/impl/data_system/fdv2_persistence_spec.rb b/spec/impl/data_system/fdv2_persistence_spec.rb index d629cd5b..d6807568 100644 --- a/spec/impl/data_system/fdv2_persistence_spec.rb +++ b/spec/impl/data_system/fdv2_persistence_spec.rb @@ -5,7 +5,7 @@ require "ldclient-rb/integrations/test_data_v2" require "ldclient-rb/data_system" require "ldclient-rb/impl/data_system" -require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/data_store/status" module LaunchDarkly module Impl @@ -31,8 +31,8 @@ class StubFeatureStore def initialize(initial_data = nil) @data = { - LaunchDarkly::Impl::DataStore::FEATURES => {}, - LaunchDarkly::Impl::DataStore::SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::FEATURES => {}, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, } @initialized = false @available = true @@ -46,8 +46,8 @@ def initialize(initial_data = nil) def init(all_data) @init_called_count += 1 if all_data - @data[LaunchDarkly::Impl::DataStore::FEATURES] = (all_data[LaunchDarkly::Impl::DataStore::FEATURES] || {}).dup - @data[LaunchDarkly::Impl::DataStore::SEGMENTS] = (all_data[LaunchDarkly::Impl::DataStore::SEGMENTS] || {}).dup + @data[LaunchDarkly::Interfaces::DataStore::FEATURES] = (all_data[LaunchDarkly::Interfaces::DataStore::FEATURES] || {}).dup + @data[LaunchDarkly::Interfaces::DataStore::SEGMENTS] = (all_data[LaunchDarkly::Interfaces::DataStore::SEGMENTS] || {}).dup end @initialized = true end @@ -109,8 +109,8 @@ def reset_operation_tracking def get_data_snapshot { - LaunchDarkly::Impl::DataStore::FEATURES => @data[LaunchDarkly::Impl::DataStore::FEATURES].dup, - LaunchDarkly::Impl::DataStore::SEGMENTS => @data[LaunchDarkly::Impl::DataStore::SEGMENTS].dup, + LaunchDarkly::Interfaces::DataStore::FEATURES => @data[LaunchDarkly::Interfaces::DataStore::FEATURES].dup, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => @data[LaunchDarkly::Interfaces::DataStore::SEGMENTS].dup, } end end @@ -135,8 +135,8 @@ def get_data_snapshot # Verify data from synchronizer is in the persistent store snapshot = persistent_store.get_data_snapshot - expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:flagkey) - expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES][:flagkey][:on]).to be true + expect(snapshot[LaunchDarkly::Interfaces::DataStore::FEATURES]).to have_key(:flagkey) + expect(snapshot[LaunchDarkly::Interfaces::DataStore::FEATURES][:flagkey][:on]).to be true # Reset tracking to isolate recovery behavior persistent_store.reset_operation_tracking @@ -166,8 +166,8 @@ def get_data_snapshot # Verify both flags are now in the persistent store snapshot = persistent_store.get_data_snapshot - expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:flagkey) - expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:newflag) + expect(snapshot[LaunchDarkly::Interfaces::DataStore::FEATURES]).to have_key(:flagkey) + expect(snapshot[LaunchDarkly::Interfaces::DataStore::FEATURES]).to have_key(:newflag) fdv2.stop end @@ -254,7 +254,7 @@ def get_data_snapshot # Verify the flag is in the persistent store snapshot = persistent_store.get_data_snapshot - expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:flagkey) + expect(snapshot[LaunchDarkly::Interfaces::DataStore::FEATURES]).to have_key(:flagkey) fdv2.stop end @@ -262,7 +262,7 @@ def get_data_snapshot it "works in READ_ONLY mode with persistent store" do # Pre-populate persistent store initial_data = { - LaunchDarkly::Impl::DataStore::FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { :existingflag => { key: "existingflag", version: 1, @@ -271,7 +271,7 @@ def get_data_snapshot fallthrough: { variation: 0 }, }, }, - LaunchDarkly::Impl::DataStore::SEGMENTS => {}, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => {}, } persistent_store = StubFeatureStore.new(initial_data) @@ -426,8 +426,8 @@ def get_data_snapshot # The synchronizer flag should be in the persistent store # (synchronizer does a full data set transfer, replacing initializer data) snapshot = persistent_store.get_data_snapshot - expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).not_to have_key(:"init-flag") - expect(snapshot[LaunchDarkly::Impl::DataStore::FEATURES]).to have_key(:"sync-flag") + expect(snapshot[LaunchDarkly::Interfaces::DataStore::FEATURES]).not_to have_key(:"init-flag") + expect(snapshot[LaunchDarkly::Interfaces::DataStore::FEATURES]).to have_key(:"sync-flag") fdv2.stop end diff --git a/spec/impl/evaluator_spec_base.rb b/spec/impl/evaluator_spec_base.rb index 17296fbe..62f3958f 100644 --- a/spec/impl/evaluator_spec_base.rb +++ b/spec/impl/evaluator_spec_base.rb @@ -17,7 +17,7 @@ def initialize(logger) end def with_flag(flag) - @flags[flag[:key]] = Model.deserialize(DataStore::FEATURES, flag) + @flags[flag[:key]] = Model.deserialize(LaunchDarkly::Interfaces::DataStore::FEATURES, flag) self end @@ -27,7 +27,7 @@ def with_unknown_flag(key) end def with_segment(segment) - @segments[segment[:key]] = Model.deserialize(DataStore::SEGMENTS, segment) + @segments[segment[:key]] = Model.deserialize(LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment) self end diff --git a/spec/impl/model/preprocessed_data_spec.rb b/spec/impl/model/preprocessed_data_spec.rb index bda9c31d..654266f9 100644 --- a/spec/impl/model/preprocessed_data_spec.rb +++ b/spec/impl/model/preprocessed_data_spec.rb @@ -30,7 +30,7 @@ module DataModelPreprocessing ], } flag = Model::FeatureFlag.new(original_flag) - json = Model.serialize(DataStore::FEATURES, flag) + json = Model.serialize(LaunchDarkly::Interfaces::DataStore::FEATURES, flag) parsed = JSON.parse(json, symbolize_names: true) expect(parsed).to eq(original_flag) end diff --git a/spec/impl/model/serialization_spec.rb b/spec/impl/model/serialization_spec.rb index ca4ed8dd..1df0029b 100644 --- a/spec/impl/model/serialization_spec.rb +++ b/spec/impl/model/serialization_spec.rb @@ -7,34 +7,34 @@ module Model describe "model serialization" do it "serializes flag" do flag = FlagBuilder.new("flagkey").version(1).build - json = Model.serialize(Impl::DataStore::FEATURES, flag) + json = Model.serialize(LaunchDarkly::Interfaces::DataStore::FEATURES, flag) expect(JSON.parse(json, symbolize_names: true)).to eq flag.data end it "serializes segment" do segment = SegmentBuilder.new("segkey").version(1).build - json = Model.serialize(Impl::DataStore::SEGMENTS, segment) + json = Model.serialize(LaunchDarkly::Interfaces::DataStore::SEGMENTS, segment) expect(JSON.parse(json, symbolize_names: true)).to eq segment.data end it "deserializes flag with no rules or prerequisites" do flag_in = { key: "flagkey", version: 1 } json = flag_in.to_json - flag_out = Model.deserialize(Impl::DataStore::FEATURES, json, nil) + flag_out = Model.deserialize(LaunchDarkly::Interfaces::DataStore::FEATURES, json, nil) expect(flag_out.data).to eq flag_in end it "deserializes segment" do segment_in = { key: "segkey", version: 1 } json = segment_in.to_json - segment_out = Model.deserialize(Impl::DataStore::SEGMENTS, json, nil) + segment_out = Model.deserialize(LaunchDarkly::Interfaces::DataStore::SEGMENTS, json, nil) expect(segment_out.data).to eq segment_in end # Other LaunchDarkly SDKs write a deleted item to a persistent store with only a version, # and no key of its own. The store knows the key, because it is the key the item is stored # under, so a tombstone must deserialize without one. - [ Impl::DataStore::FEATURES, Impl::DataStore::SEGMENTS ].each do |kind| + [ LaunchDarkly::Interfaces::DataStore::FEATURES, LaunchDarkly::Interfaces::DataStore::SEGMENTS ].each do |kind| it "deserializes a tombstone with no key for #{kind[:namespace]}" do item_in = { version: 99, deleted: true } item_out = Model.deserialize(kind, item_in.to_json, nil) diff --git a/spec/integrations/dynamodb_stores_spec.rb b/spec/integrations/dynamodb_stores_spec.rb index 0b9a170c..bef0bfd6 100644 --- a/spec/integrations/dynamodb_stores_spec.rb +++ b/spec/integrations/dynamodb_stores_spec.rb @@ -197,29 +197,29 @@ def create_paging_store(page_size) it "reads every page in all" do ensure_stop(tester.create_feature_store) do |store| - store.init({ LaunchDarkly::Impl::DataStore::FEATURES => flags }) + store.init({ LaunchDarkly::Interfaces::DataStore::FEATURES => flags }) end store, queries = create_paging_store(2) ensure_stop(store) do - expect(store.all(LaunchDarkly::Impl::DataStore::FEATURES).keys).to match_array(flags.keys) + expect(store.all(LaunchDarkly::Interfaces::DataStore::FEATURES).keys).to match_array(flags.keys) end expect(queries.count { |q| q[:exclusive_start_key] }).to be >= 2 end it "reads every page of existing keys in init and deletes the keys that are not in the new data" do ensure_stop(tester.create_feature_store) do |store| - store.init({ LaunchDarkly::Impl::DataStore::FEATURES => flags }) + store.init({ LaunchDarkly::Interfaces::DataStore::FEATURES => flags }) end store, queries = create_paging_store(2) ensure_stop(store) do - store.init({ LaunchDarkly::Impl::DataStore::FEATURES => { flag1: flags[:flag1] } }) + store.init({ LaunchDarkly::Interfaces::DataStore::FEATURES => { flag1: flags[:flag1] } }) end expect(queries.count { |q| q[:exclusive_start_key] }).to be >= 2 ensure_stop(tester.create_feature_store) do |reader| - expect(reader.all(LaunchDarkly::Impl::DataStore::FEATURES).keys).to eq([:flag1]) + expect(reader.all(LaunchDarkly::Interfaces::DataStore::FEATURES).keys).to eq([:flag1]) end end end diff --git a/spec/integrations/file_data_source_spec.rb b/spec/integrations/file_data_source_spec.rb index 410d8e4e..d15f122f 100644 --- a/spec/integrations/file_data_source_spec.rb +++ b/spec/integrations/file_data_source_spec.rb @@ -146,8 +146,8 @@ def with_data_source(options, initialize_to_valid = false) file = make_temp_file('{"flagValues":{"key":"value"}}') with_data_source({ paths: [ file.path ] }) do |_| expect(@store.initialized?).to eq(false) - expect(@store.all(LaunchDarkly::Impl::DataStore::FEATURES)).to eq({}) - expect(@store.all(LaunchDarkly::Impl::DataStore::SEGMENTS)).to eq({}) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::FEATURES)).to eq({}) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS)).to eq({}) end end @@ -159,8 +159,8 @@ def with_data_source(options, initialize_to_valid = false) ds.start expect(@store.initialized?).to eq(true) - expect(@store.all(LaunchDarkly::Impl::DataStore::FEATURES).keys).to eq(all_flag_keys) - expect(@store.all(LaunchDarkly::Impl::DataStore::SEGMENTS).keys).to eq(all_segment_keys) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::FEATURES).keys).to eq(all_flag_keys) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys).to eq(all_segment_keys) expect(listener.statuses.count).to eq(1) expect(listener.statuses[0].state).to eq(LaunchDarkly::Interfaces::DataSource::Status::VALID) @@ -172,8 +172,8 @@ def with_data_source(options, initialize_to_valid = false) with_data_source({ paths: [ file.path ] }) do |ds| ds.start expect(@store.initialized?).to eq(true) - expect(@store.all(LaunchDarkly::Impl::DataStore::FEATURES).keys).to eq(all_flag_keys) - expect(@store.all(LaunchDarkly::Impl::DataStore::SEGMENTS).keys).to eq(all_segment_keys) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::FEATURES).keys).to eq(all_flag_keys) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys).to eq(all_segment_keys) end end @@ -242,8 +242,8 @@ def with_data_source(options, initialize_to_valid = false) with_data_source({ paths: [ file1.path, file2.path ] }) do |ds| ds.start expect(@store.initialized?).to eq(true) - expect(@store.all(LaunchDarkly::Impl::DataStore::FEATURES).keys).to eq([ full_flag_1_key.to_sym ]) - expect(@store.all(LaunchDarkly::Impl::DataStore::SEGMENTS).keys).to eq([ full_segment_1_key.to_sym ]) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::FEATURES).keys).to eq([ full_flag_1_key.to_sym ]) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys).to eq([ full_segment_1_key.to_sym ]) end end @@ -267,7 +267,7 @@ def with_data_source(options, initialize_to_valid = false) with_data_source({ paths: [ file1.path, file2.path ] }) do |ds| ds.start expect(@store.initialized?).to eq(false) - expect(@store.all(LaunchDarkly::Impl::DataStore::FEATURES).keys).to eq([]) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::FEATURES).keys).to eq([]) end end @@ -277,11 +277,11 @@ def with_data_source(options, initialize_to_valid = false) with_data_source({ paths: [ file.path ] }) do |ds| event = ds.start expect(event.set?).to eq(true) - expect(@store.all(LaunchDarkly::Impl::DataStore::SEGMENTS).keys).to eq([]) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys).to eq([]) IO.write(file, all_properties_json) sleep(0.5) - expect(@store.all(LaunchDarkly::Impl::DataStore::SEGMENTS).keys).to eq([]) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys).to eq([]) end end @@ -292,13 +292,13 @@ def test_auto_reload(options) with_data_source(options) do |ds| event = ds.start expect(event.set?).to eq(true) - expect(@store.all(LaunchDarkly::Impl::DataStore::SEGMENTS).keys).to eq([]) + expect(@store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys).to eq([]) sleep(1) IO.write(file, all_properties_json) max_time = 10 - ok = wait_for_condition(10) { @store.all(LaunchDarkly::Impl::DataStore::SEGMENTS).keys == all_segment_keys } + ok = wait_for_condition(10) { @store.all(LaunchDarkly::Interfaces::DataStore::SEGMENTS).keys == all_segment_keys } expect(ok).to eq(true), "Waited #{max_time}s after modifying file and it did not reload" end end diff --git a/spec/integrations/redis_stores_spec.rb b/spec/integrations/redis_stores_spec.rb index dd38198b..117dc584 100644 --- a/spec/integrations/redis_stores_spec.rb +++ b/spec/integrations/redis_stores_spec.rb @@ -111,11 +111,11 @@ def make_concurrent_modifier_test_hook(other_client, flag, start_version, end_ve tester = RedisStoreTester.new({ test_hook: test_hook, logger: $null_logger }) ensure_stop(tester.create_feature_store) do |store| - store.init(LaunchDarkly::Impl::DataStore::FEATURES => { flag[:key] => flag }) + store.init(LaunchDarkly::Interfaces::DataStore::FEATURES => { flag[:key] => flag }) my_ver = { key: "foo", version: 10 } - store.upsert(LaunchDarkly::Impl::DataStore::FEATURES, my_ver) - result = store.get(LaunchDarkly::Impl::DataStore::FEATURES, flag[:key]) + store.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, my_ver) + result = store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag[:key]) expect(result[:version]).to eq 10 end end @@ -128,11 +128,11 @@ def make_concurrent_modifier_test_hook(other_client, flag, start_version, end_ve tester = RedisStoreTester.new({ test_hook: test_hook, logger: $null_logger }) ensure_stop(tester.create_feature_store) do |store| - store.init(LaunchDarkly::Impl::DataStore::FEATURES => { flag[:key] => flag }) + store.init(LaunchDarkly::Interfaces::DataStore::FEATURES => { flag[:key] => flag }) my_ver = { key: "foo", version: 2 } - store.upsert(LaunchDarkly::Impl::DataStore::FEATURES, my_ver) - result = store.get(LaunchDarkly::Impl::DataStore::FEATURES, flag[:key]) + store.upsert(LaunchDarkly::Interfaces::DataStore::FEATURES, my_ver) + result = store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, flag[:key]) expect(result[:version]).to eq 3 end end @@ -144,7 +144,7 @@ def make_concurrent_modifier_test_hook(other_client, flag, start_version, end_ve store = tester.create_feature_store begin - store.init(LaunchDarkly::Impl::DataStore::FEATURES => { }) + store.init(LaunchDarkly::Interfaces::DataStore::FEATURES => { }) store.stop expect { unowned_pool.with {} }.to raise_error(ConnectionPool::PoolShuttingDownError) @@ -159,7 +159,7 @@ def make_concurrent_modifier_test_hook(other_client, flag, start_version, end_ve store = tester.create_feature_store begin - store.init(LaunchDarkly::Impl::DataStore::FEATURES => { }) + store.init(LaunchDarkly::Interfaces::DataStore::FEATURES => { }) store.stop expect { unowned_pool.with {} }.not_to raise_error diff --git a/spec/integrations/test_data_spec.rb b/spec/integrations/test_data_spec.rb index 3c28fd4e..d4d323a5 100644 --- a/spec/integrations/test_data_spec.rb +++ b/spec/integrations/test_data_spec.rb @@ -7,7 +7,7 @@ module Integrations td = Integrations::TestData.data_source config = Config.new(send_events: false, data_source: td) client = LDClient.new('sdkKey', config) - expect(config.feature_store.all(LaunchDarkly::Impl::DataStore::FEATURES)).to eql({}) + expect(config.feature_store.all(LaunchDarkly::Interfaces::DataStore::FEATURES)).to eql({}) client.close end @@ -16,7 +16,7 @@ module Integrations td.update(td.flag('flag')) config = Config.new(send_events: false, data_source: td) client = LDClient.new('sdkKey', config) - expect(config.feature_store.get(LaunchDarkly::Impl::DataStore::FEATURES, 'flag').data).to eql({ + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, 'flag').data).to eql({ key: 'flag', variations: [true, false], fallthrough: { variation: 0 }, @@ -35,7 +35,7 @@ module Integrations config2 = Config.new(send_events: false, data_source: td) client2 = LDClient.new('sdkKey', config2) - expect(config.feature_store.get(LaunchDarkly::Impl::DataStore::FEATURES, 'flag').data).to eql({ + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, 'flag').data).to eql({ key: 'flag', variations: [true, false], fallthrough: { variation: 0 }, @@ -43,7 +43,7 @@ module Integrations on: true, version: 1, }) - expect(config2.feature_store.get(LaunchDarkly::Impl::DataStore::FEATURES, 'flag').data).to eql({ + expect(config2.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, 'flag').data).to eql({ key: 'flag', variations: [true, false], fallthrough: { variation: 0 }, @@ -54,7 +54,7 @@ module Integrations td.update(td.flag('flag').variation_for_all(false)) - expect(config.feature_store.get(LaunchDarkly::Impl::DataStore::FEATURES, 'flag').data).to eql({ + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, 'flag').data).to eql({ key: 'flag', variations: [true, false], fallthrough: { variation: 1 }, @@ -62,7 +62,7 @@ module Integrations on: true, version: 2, }) - expect(config2.feature_store.get(LaunchDarkly::Impl::DataStore::FEATURES, 'flag').data).to eql({ + expect(config2.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, 'flag').data).to eql({ key: 'flag', variations: [true, false], fallthrough: { variation: 1 }, @@ -83,22 +83,22 @@ module Integrations config = Config.new(send_events: false, data_source: td) client = LDClient.new('sdkKey', config) - expect(config.feature_store.get(LaunchDarkly::Impl::DataStore::FEATURES, 'my-flag').data).to eql({ + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, 'my-flag').data).to eql({ key: 'my-flag', version: 1000, on: true }) - expect(config.feature_store.get(LaunchDarkly::Impl::DataStore::SEGMENTS, 'my-segment').data).to eql({ + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, 'my-segment').data).to eql({ key: 'my-segment', version: 2000 }) td.use_preconfigured_flag({ key: 'my-flag', on: false }) - expect(config.feature_store.get(LaunchDarkly::Impl::DataStore::FEATURES, 'my-flag').data).to eql({ + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::FEATURES, 'my-flag').data).to eql({ key: 'my-flag', version: 1001, on: false }) td.use_preconfigured_segment({ key: 'my-segment', included: [ 'x' ] }) - expect(config.feature_store.get(LaunchDarkly::Impl::DataStore::SEGMENTS, 'my-segment').data).to eql({ + expect(config.feature_store.get(LaunchDarkly::Interfaces::DataStore::SEGMENTS, 'my-segment').data).to eql({ key: 'my-segment', version: 2001, included: [ 'x' ] }) diff --git a/spec/interfaces/data_store_spec.rb b/spec/interfaces/data_store_spec.rb index 6582e91c..1f5641c0 100644 --- a/spec/interfaces/data_store_spec.rb +++ b/spec/interfaces/data_store_spec.rb @@ -34,15 +34,6 @@ module DataStore end end - describe "Impl aliases" do - it "are the public constants" do - expect(LaunchDarkly::Impl::DataStore::FEATURES).to be(FEATURES) - expect(LaunchDarkly::Impl::DataStore::SEGMENTS).to be(SEGMENTS) - expect(LaunchDarkly::Impl::DataStore::ALL_KINDS).to be(ALL_KINDS) - expect(LaunchDarkly::Impl::DataStore::DataKind).to be(DataKind) - end - end - describe "eql?" do it "constant instances are equal to themselves" do expect(FEATURES.eql?(FEATURES)).to be true diff --git a/spec/ldclient_cached_data_warning_spec.rb b/spec/ldclient_cached_data_warning_spec.rb index de0c983b..ec0ecdaa 100644 --- a/spec/ldclient_cached_data_warning_spec.rb +++ b/spec/ldclient_cached_data_warning_spec.rb @@ -21,7 +21,7 @@ def uninitialized_source def initialized_store store = InMemoryFeatureStore.new - store.init({ Impl::DataStore::FEATURES => {}, Impl::DataStore::SEGMENTS => {} }) + store.init({ LaunchDarkly::Interfaces::DataStore::FEATURES => {}, LaunchDarkly::Interfaces::DataStore::SEGMENTS => {} }) store end diff --git a/spec/ldclient_spec.rb b/spec/ldclient_spec.rb index a97f8371..dc4906cb 100644 --- a/spec/ldclient_spec.rb +++ b/spec/ldclient_spec.rb @@ -74,7 +74,7 @@ module LaunchDarkly context "feature store data ordering" do let(:dependency_ordering_test_data) { { - Impl::DataStore::FEATURES => { + LaunchDarkly::Interfaces::DataStore::FEATURES => { a: { key: "a", prerequisites: [ { key: "b" }, { key: "c" } ] }, b: { key: "b", prerequisites: [ { key: "c" }, { key: "e" } ] }, c: { key: "c" }, @@ -82,7 +82,7 @@ module LaunchDarkly e: { key: "e" }, f: { key: "f" }, }, - Impl::DataStore::SEGMENTS => { + LaunchDarkly::Interfaces::DataStore::SEGMENTS => { o: { key: "o" }, }, } @@ -91,8 +91,8 @@ module LaunchDarkly it "passes data set to feature store in correct order on init" do store = CapturingFeatureStore.new td = Integrations::TestData.data_source - dependency_ordering_test_data[Impl::DataStore::FEATURES].each { |_, flag| td.use_preconfigured_flag(flag) } - dependency_ordering_test_data[Impl::DataStore::SEGMENTS].each { |_, segment| td.use_preconfigured_segment(segment) } + dependency_ordering_test_data[LaunchDarkly::Interfaces::DataStore::FEATURES].each { |_, flag| td.use_preconfigured_flag(flag) } + dependency_ordering_test_data[LaunchDarkly::Interfaces::DataStore::SEGMENTS].each { |_, segment| td.use_preconfigured_segment(segment) } with_client(test_config(feature_store: store, data_source: td)) do |_| data = store.received_data @@ -100,14 +100,14 @@ module LaunchDarkly expect(data.count).to eq(2) # Segments should always come first - expect(data.keys[0]).to be(Impl::DataStore::SEGMENTS) - expect(data.values[0].count).to eq(dependency_ordering_test_data[Impl::DataStore::SEGMENTS].count) + expect(data.keys[0]).to be(LaunchDarkly::Interfaces::DataStore::SEGMENTS) + expect(data.values[0].count).to eq(dependency_ordering_test_data[LaunchDarkly::Interfaces::DataStore::SEGMENTS].count) # Features should be ordered so that a flag always appears after its prerequisites, if any - expect(data.keys[1]).to be(Impl::DataStore::FEATURES) + expect(data.keys[1]).to be(LaunchDarkly::Interfaces::DataStore::FEATURES) flags_map = data.values[1] flags_list = flags_map.values - expect(flags_list.count).to eq(dependency_ordering_test_data[Impl::DataStore::FEATURES].count) + expect(flags_list.count).to eq(dependency_ordering_test_data[LaunchDarkly::Interfaces::DataStore::FEATURES].count) flags_list.each_with_index do |item, item_index| (item[:prerequisites] || []).each do |prereq| prereq = flags_map[prereq[:key].to_sym] diff --git a/spec/model_builders.rb b/spec/model_builders.rb index fd0114f3..58cf15b8 100644 --- a/spec/model_builders.rb +++ b/spec/model_builders.rb @@ -4,7 +4,7 @@ class Flags def self.from_hash(data) - LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Impl::DataStore::FEATURES, data) + LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Interfaces::DataStore::FEATURES, data) end def self.boolean_flag_with_rules(*rules) @@ -20,7 +20,7 @@ def self.boolean_flag_with_clauses(*clauses) class Segments def self.from_hash(data) - LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Impl::DataStore::SEGMENTS, data) + LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Interfaces::DataStore::SEGMENTS, data) end end @@ -226,21 +226,21 @@ def initialize end def flag(data) - f = LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Impl::DataStore::FEATURES, data) + f = LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Interfaces::DataStore::FEATURES, data) @flags[f.key.to_sym] = f self end def segment(data) - s = LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Impl::DataStore::SEGMENTS, data) + s = LaunchDarkly::Impl::Model.deserialize(LaunchDarkly::Interfaces::DataStore::SEGMENTS, data) @segments[s.key.to_sym] = s self end def to_store_data { - LaunchDarkly::Impl::DataStore::FEATURES => @flags, - LaunchDarkly::Impl::DataStore::SEGMENTS => @segments, + LaunchDarkly::Interfaces::DataStore::FEATURES => @flags, + LaunchDarkly::Interfaces::DataStore::SEGMENTS => @segments, } end From 590e24d11a7a12b0b8941152458c9a96a3093e15 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 4 Nov 2026 14:30:03 -0500 Subject: [PATCH 3/4] fix: Compare data kinds by value with == DataKind defined eql? and hash but not ==, so == fell back to identity. A kind built with the same namespace and priority, or one restored with Marshal, was not == to the SDK constant. Model.deserialize selects the model class with case/when, so such a kind deserialized to a raw Hash instead of a FeatureFlag or Segment, and ALL_KINDS.include? missed it. Inside the SDK only the frozen constants reach these paths. Store gems can now build or receive kinds themselves, so equality must be by value. --- lib/ldclient-rb/interfaces/data_store.rb | 3 ++- spec/interfaces/data_store_spec.rb | 26 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/ldclient-rb/interfaces/data_store.rb b/lib/ldclient-rb/interfaces/data_store.rb index fd1ec8bc..2bacf9f9 100644 --- a/lib/ldclient-rb/interfaces/data_store.rb +++ b/lib/ldclient-rb/interfaces/data_store.rb @@ -89,9 +89,10 @@ def get_dependency_keys_fn() FEATURE_PREREQ_FN end - def eql?(other) + def ==(other) other.is_a?(DataKind) && namespace == other.namespace && priority == other.priority end + alias_method :eql?, :== def hash [namespace, priority].hash diff --git a/spec/interfaces/data_store_spec.rb b/spec/interfaces/data_store_spec.rb index 1f5641c0..a35f789e 100644 --- a/spec/interfaces/data_store_spec.rb +++ b/spec/interfaces/data_store_spec.rb @@ -64,6 +64,32 @@ module DataStore end end + describe "==" do + it "same constructions are equal" do + expect(DataKind.new(namespace: "features", priority: 1)).to eq FEATURES + expect(DataKind.new(namespace: "segments", priority: 0)).to eq SEGMENTS + end + + it "a kind that went through Marshal is equal" do + expect(Marshal.load(Marshal.dump(FEATURES))).to eq FEATURES + end + + it "distinct kinds are not equal" do + expect(DataKind.new(namespace: "features", priority: 2)).not_to eq FEATURES + expect(FEATURES).not_to eq SEGMENTS + expect(FEATURES).not_to eq "features" + end + + it "ALL_KINDS includes an equal kind" do + expect(ALL_KINDS).to include(DataKind.new(namespace: "segments", priority: 0)) + end + + it "an equal kind deserializes to the model class" do + kind = DataKind.new(namespace: "features", priority: 1) + expect(kind.deserialize('{"key":"flag","version":1}')).to be_a(LaunchDarkly::Impl::Model::FeatureFlag) + end + end + describe "hash" do it "constant instances are equal to themselves" do expect(FEATURES.hash).to eq FEATURES.hash From 5e73cd1bce7d2f74a0736b46e764ac2f87f076d5 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 4 Nov 2026 14:30:26 -0500 Subject: [PATCH 4/4] fix: Load the data kind dependencies from the public data store file A store gem that required only ldclient-rb/interfaces/data_store got a NameError from DataKind#serialize and #deserialize, because the file did not load the model serialization code. The serialization code did not load json, and the FeatureFlag and Segment models did not load LDContext, so each of those failed next. Each file now requires what it uses, and a spec loads the public file alone in a separate process. --- lib/ldclient-rb/impl/model/feature_flag.rb | 1 + lib/ldclient-rb/impl/model/segment.rb | 1 + lib/ldclient-rb/impl/model/serialization.rb | 2 ++ lib/ldclient-rb/interfaces/data_store.rb | 2 ++ spec/interfaces/data_store_spec.rb | 22 +++++++++++++++++++++ 5 files changed, 28 insertions(+) diff --git a/lib/ldclient-rb/impl/model/feature_flag.rb b/lib/ldclient-rb/impl/model/feature_flag.rb index b53b13e3..4fda0f37 100644 --- a/lib/ldclient-rb/impl/model/feature_flag.rb +++ b/lib/ldclient-rb/impl/model/feature_flag.rb @@ -1,5 +1,6 @@ require "ldclient-rb/impl/evaluator_helpers" require "ldclient-rb/impl/model/clause" +require "ldclient-rb/context" require "set" # See serialization.rb for implementation notes on the data model classes. diff --git a/lib/ldclient-rb/impl/model/segment.rb b/lib/ldclient-rb/impl/model/segment.rb index 83c1dc90..a361e37f 100644 --- a/lib/ldclient-rb/impl/model/segment.rb +++ b/lib/ldclient-rb/impl/model/segment.rb @@ -1,5 +1,6 @@ require "ldclient-rb/impl/model/clause" require "ldclient-rb/impl/model/preprocessed_data" +require "ldclient-rb/context" require "set" # See serialization.rb for implementation notes on the data model classes. diff --git a/lib/ldclient-rb/impl/model/serialization.rb b/lib/ldclient-rb/impl/model/serialization.rb index 14df5ee4..f7df32ba 100644 --- a/lib/ldclient-rb/impl/model/serialization.rb +++ b/lib/ldclient-rb/impl/model/serialization.rb @@ -1,3 +1,5 @@ +require "json" + require "ldclient-rb/impl/model/feature_flag" require "ldclient-rb/impl/model/preprocessed_data" require "ldclient-rb/impl/model/segment" diff --git a/lib/ldclient-rb/interfaces/data_store.rb b/lib/ldclient-rb/interfaces/data_store.rb index 2bacf9f9..f9d266dc 100644 --- a/lib/ldclient-rb/interfaces/data_store.rb +++ b/lib/ldclient-rb/interfaces/data_store.rb @@ -1,3 +1,5 @@ +require "ldclient-rb/impl/model/serialization" + module LaunchDarkly module Interfaces module DataStore diff --git a/spec/interfaces/data_store_spec.rb b/spec/interfaces/data_store_spec.rb index a35f789e..7cce5557 100644 --- a/spec/interfaces/data_store_spec.rb +++ b/spec/interfaces/data_store_spec.rb @@ -1,4 +1,5 @@ require "spec_helper" +require "open3" module LaunchDarkly module Interfaces @@ -32,6 +33,27 @@ module DataStore it "deserializes nil to nil" do expect(FEATURES.deserialize(nil)).to be_nil end + + it "works when the application requires only this file" do + script = <<~RUBY + require "ldclient-rb/interfaces/data_store" + kinds = LaunchDarkly::Interfaces::DataStore + flag = { key: "f", version: 1, on: true, variations: [true, false], offVariation: 1, + fallthrough: { variation: 0 }, targets: [{ values: ["a"], variation: 0 }], + contextTargets: [{ contextKind: "org", values: ["b"], variation: 1 }], + prerequisites: [{ key: "p", variation: 0 }], + rules: [{ id: "r", clauses: [{ attribute: "name", op: "in", values: ["x"] }], variation: 0 }] } + segment = { key: "s", version: 1, included: ["a"], + includedContexts: [{ contextKind: "org", values: ["b"] }], + rules: [{ id: "r", clauses: [{ contextKind: "user", attribute: "name", op: "in", values: ["x"] }] }] } + puts kinds::FEATURES.deserialize(kinds::FEATURES.serialize(flag)).class + puts kinds::SEGMENTS.deserialize(kinds::SEGMENTS.serialize(segment)).class + RUBY + lib = File.expand_path("../../lib", __dir__) + out, status = Open3.capture2e(RbConfig.ruby, "-I", lib, "-e", script) + expect(status.success?).to be(true), out + expect(out.split("\n")).to eq ["LaunchDarkly::Impl::Model::FeatureFlag", "LaunchDarkly::Impl::Model::Segment"] + end end describe "eql?" do