Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ldclient-openfeature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "ldclient-openfeature/impl/context_converter"
require_relative "ldclient-openfeature/impl/details_converter"
require_relative "ldclient-openfeature/impl/event_listeners"
require_relative "ldclient-openfeature/provider"
require_relative "ldclient-openfeature/version"

Expand Down
82 changes: 82 additions & 0 deletions lib/ldclient-openfeature/impl/event_listeners.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

require 'ldclient-rb'
require 'open_feature/sdk'

module LaunchDarkly
module OpenFeature
module Impl
#
# Translates LaunchDarkly data source status changes into OpenFeature provider events.
#
class DataSourceStatusListener
#
# @param provider [LaunchDarkly::OpenFeature::Provider]
#
def initialize(provider)
@provider = provider
end

#
# @param status [LaunchDarkly::Interfaces::DataSource::Status]
#
# @return [void]
#
def update(status)
case status.state
when ::LaunchDarkly::Interfaces::DataSource::Status::VALID
@provider.emit_event(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY)
when ::LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED
@provider.emit_event(
::OpenFeature::SDK::ProviderEvent::PROVIDER_STALE,
message: message(status, "the data source has been interrupted")
)
when ::LaunchDarkly::Interfaces::DataSource::Status::OFF
@provider.emit_event(
::OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside: We don't say fatal, because then OF short-circuits evaluations.

error_code: ::OpenFeature::SDK::Provider::ErrorCode::GENERAL,
message: message(status, "the data source has been permanently shut down")
)
end
end

#
# @param status [LaunchDarkly::Interfaces::DataSource::Status]
# @param fallback [String]
#
# @return [String]
#
private def message(status, fallback)
error = status.last_error
return fallback if error.nil?

"#{fallback}: #{error.kind} #{error.status_code} #{error.message}".strip
end
end

#
# Translates LaunchDarkly flag change events into OpenFeature configuration changed events.
#
class FlagChangeListener
#
# @param provider [LaunchDarkly::OpenFeature::Provider]
#
def initialize(provider)
@provider = provider
end

#
# @param flag_change [LaunchDarkly::Interfaces::FlagChange]
#
# @return [void]
#
def update(flag_change)
@provider.emit_event(
::OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
flags_changed: [flag_change.key]
)
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/ldclient-openfeature/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
module LaunchDarkly
module OpenFeature
class Provider
include ::OpenFeature::SDK::Provider::EventEmitter

#
# Retrieve metadata information describing this provider.
#
Expand Down Expand Up @@ -37,6 +39,9 @@ def initialize(sdk_key, config = LaunchDarkly::Config.default, wait_for_seconds
@details_converter = Impl::ResolutionDetailsConverter.new

@metadata = ::OpenFeature::SDK::Provider::ProviderMetadata.new(name: "launchdarkly-openfeature-server").freeze

@client.data_source_status_provider.add_listener(Impl::DataSourceStatusListener.new(self))
@client.flag_tracker.add_listener(Impl::FlagChangeListener.new(self))
end

def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
Expand Down
42 changes: 42 additions & 0 deletions spec/impl/data_source_status_listener_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

RSpec.describe LaunchDarkly::OpenFeature::Impl::DataSourceStatusListener do
let(:provider) { double(emit_event: nil) }
let(:listener) { described_class.new(provider) }

def status(state, error = nil)
LaunchDarkly::Interfaces::DataSource::Status.new(state, Time.now, error)
end

it "a valid data source is ready" do
listener.update(status(LaunchDarkly::Interfaces::DataSource::Status::VALID))

expect(provider).to have_received(:emit_event).with(OpenFeature::SDK::ProviderEvent::PROVIDER_READY)
end

it "an interrupted data source is stale" do
listener.update(status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED))

expect(provider).to have_received(:emit_event)
.with(OpenFeature::SDK::ProviderEvent::PROVIDER_STALE, hash_including(:message))
end

it "a shut down data source is an error" do
error = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, 401, "Unauthorized", Time.now
)

listener.update(status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error))

expect(provider).to have_received(:emit_event).with(
OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR,
hash_including(error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, message: /401/)
)
end

it "an initializing data source does not emit an event" do
listener.update(status(LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING))

expect(provider).not_to have_received(:emit_event)
end
end
14 changes: 14 additions & 0 deletions spec/impl/flag_change_listener_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

RSpec.describe LaunchDarkly::OpenFeature::Impl::FlagChangeListener do
let(:provider) { double(emit_event: nil) }

it "a flag change is a configuration change" do
described_class.new(provider).update(LaunchDarkly::Interfaces::FlagChange.new("flag-key"))

expect(provider).to have_received(:emit_event).with(
OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
flags_changed: ["flag-key"]
)
end
end
Loading