diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7f3ab495..56b4dffb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "8.4.2" + ".": "8.5.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 448158b3..c396af08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [8.5.0](https://github.com/launchdarkly/ruby-server-sdk/compare/8.4.2...8.5.0) (2024-06-10) + + +### Features + +* Support to_h and to_json methods for LDContext ([#284](https://github.com/launchdarkly/ruby-server-sdk/issues/284)) ([d3c8d40](https://github.com/launchdarkly/ruby-server-sdk/commit/d3c8d409b631f559239e48fb93eb5e4f9181254f)) + + +### Bug Fixes + +* Increment flag & segment versions when reloading from file data source ([#285](https://github.com/launchdarkly/ruby-server-sdk/issues/285)) ([7d5b051](https://github.com/launchdarkly/ruby-server-sdk/commit/7d5b051ec1b1e8990a7fb3def5798f064acd5e04)) +* Log warning if client init timeout is considered high ([#278](https://github.com/launchdarkly/ruby-server-sdk/issues/278)) ([61f4c7e](https://github.com/launchdarkly/ruby-server-sdk/commit/61f4c7e589e9d0da94e4f289e9c601aa36028c95)) + ## [8.4.2](https://github.com/launchdarkly/ruby-server-sdk/compare/8.4.1...8.4.2) (2024-05-03) diff --git a/PROVENANCE.md b/PROVENANCE.md index 4cc73457..a597331d 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -9,7 +9,7 @@ To verify SLSA provenance attestations, we recommend using [slsa-verifier](https ``` # Set the version of the SDK to verify -SDK_VERSION=8.4.2 +SDK_VERSION=8.5.0 ``` diff --git a/lib/ldclient-rb/context.rb b/lib/ldclient-rb/context.rb index 9c331a3a..c9dd4618 100644 --- a/lib/ldclient-rb/context.rb +++ b/lib/ldclient-rb/context.rb @@ -354,6 +354,49 @@ def [](key) multi_kind? ? individual_context(key.to_s) : get_value(key) end + # + # Convert the LDContext to a JSON string. + # + # @param args [Array] + # @return [String] + # + def to_json(*args) + JSON.generate(to_h, *args) + end + + # + # Convert the LDContext to a hash. If the LDContext is invalid, the hash will contain an error key with the error + # message. + # + # @return [Hash] + # + def to_h + return {error: error} unless valid? + return hash_single_kind unless multi_kind? + + hash = {kind: 'multi'} + @contexts.each do |context| + single_kind_hash = context.to_h + kind = single_kind_hash.delete(:kind) + hash[kind] = single_kind_hash + end + + hash + end + + protected def hash_single_kind + hash = attributes.nil? ? {} : attributes.clone + + hash[:kind] = kind + hash[:key] = key + + hash[:name] = name unless name.nil? + hash[:anonymous] = anonymous if anonymous + hash[:_meta] = {privateAttributes: private_attributes} unless private_attributes.empty? + + hash + end + # # Retrieve the value of any top level, addressable attribute. # diff --git a/lib/ldclient-rb/impl/integrations/file_data_source.rb b/lib/ldclient-rb/impl/integrations/file_data_source.rb index c5ee917f..1e7ad078 100644 --- a/lib/ldclient-rb/impl/integrations/file_data_source.rb +++ b/lib/ldclient-rb/impl/integrations/file_data_source.rb @@ -40,6 +40,9 @@ def initialize(data_store, data_source_update_sink, logger, options={}) @poll_interval = options[:poll_interval] || 1 @initialized = Concurrent::AtomicBoolean.new(false) @ready = Concurrent::Event.new + + @version_lock = Mutex.new + @last_version = 1 end def initialized? @@ -93,14 +96,22 @@ def load_all end def load_file(path, all_data) + version = 1 + @version_lock.synchronize { + version = @last_version + @last_version += 1 + } + parsed = parse_content(IO.read(path)) (parsed[:flags] || {}).each do |key, flag| + flag[:version] = version add_item(all_data, FEATURES, flag) end (parsed[:flagValues] || {}).each do |key, value| - add_item(all_data, FEATURES, make_flag_with_value(key.to_s, value)) + add_item(all_data, FEATURES, make_flag_with_value(key.to_s, value, version)) end (parsed[:segments] || {}).each do |key, segment| + segment[:version] = version add_item(all_data, SEGMENTS, segment) end end @@ -134,10 +145,11 @@ def add_item(all_data, kind, item) items[key] = Model.deserialize(kind, item) end - def make_flag_with_value(key, value) + def make_flag_with_value(key, value, version) { key: key, on: true, + version: version, fallthrough: { variation: 0 }, variations: [ value ], } diff --git a/lib/ldclient-rb/reference.rb b/lib/ldclient-rb/reference.rb index d25ee06b..8c248fe3 100644 --- a/lib/ldclient-rb/reference.rb +++ b/lib/ldclient-rb/reference.rb @@ -238,6 +238,16 @@ def hash ([error] + components).hash end + # + # Convert the Reference to a JSON string. + # + # @param args [Array] + # @return [String] + # + def to_json(*args) + JSON.generate(@raw_path, *args) + end + # # Performs unescaping of attribute reference path components: # diff --git a/lib/ldclient-rb/version.rb b/lib/ldclient-rb/version.rb index bb3e7898..cfe2e29d 100644 --- a/lib/ldclient-rb/version.rb +++ b/lib/ldclient-rb/version.rb @@ -1,3 +1,3 @@ module LaunchDarkly - VERSION = "8.4.2" # x-release-please-version + VERSION = "8.5.0" # x-release-please-version end diff --git a/spec/context_spec.rb b/spec/context_spec.rb index be261515..44be8e4a 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -1,4 +1,5 @@ require "ldclient-rb/context" +require "json" module LaunchDarkly describe LDContext do @@ -148,6 +149,64 @@ module LaunchDarkly expect(org_first.fully_qualified_key).to eq("org:b-org-key:user:a-user-key") end end + + describe "converts back to JSON format" do + it "single kind contexts" do + contextHash = { + key: "launchdarkly", + kind: "org", + address: { + street: "1999 Harrison St Suite 1100", + city: "Oakland", + state: "CA", + zip: "94612", + _meta: { + privateAttributes: ["city"], + }, + }, + } + context = subject.create(contextHash) + contextJson = context.to_json + backToHash = JSON.parse(contextJson, symbolize_names: true) + + expect(backToHash).to eq(contextHash) + end + + it "multi kind contexts" do + contextHash = { + kind: "multi", + "org": { + key: "launchdarkly", + address: { + street: "1999 Harrison St Suite 1100", + city: "Oakland", + state: "CA", + zip: "94612", + }, + _meta: { + privateAttributes: ["address/city"], + }, + }, + "user": { + key: "user-key", + name: "Ruby", + anonymous: true, + }, + } + context = subject.create(contextHash) + contextJson = context.to_json + backToHash = JSON.parse(contextJson, symbolize_names: true) + + expect(backToHash).to eq(contextHash) + end + + it "invalid context returns error" do + context = subject.create({ key: "", kind: "user", name: "testing" }) + expect(context.valid?).to be false + expect(context.to_h).to eq({ error: "context key must not be empty" }) + expect(context.to_json).to eq({ error: "context key must not be empty" }.to_json) + end + end end describe "context counts" do