openfeature-sdk 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac02affc7dc168be388d9d2a4be338f8e390587cc911372126fc2e25f07237fc
4
- data.tar.gz: dfa2ba4e40f5498b55d70ce5001233632007a9a10decf13f153edf08455672ad
3
+ metadata.gz: c91c19e8a0f7ef628c048224287a276713d704914142d4a2771bd74bf01ea68e
4
+ data.tar.gz: 565d980320e40d5a573c4710d6313f038c0a58350f6d5eb1564dcd5c85148853
5
5
  SHA512:
6
- metadata.gz: 413138e8e435e278376a6a8ac70a25d1673128e76ed454bfffec9e894517a61f42c8e31ef3adfc25ba71786083eb574427e0b6dfa9748dec099c807dce3b3a7d
7
- data.tar.gz: 1f494723edf1c24559e19e2083c59fffa6f988f723d46f65df1c7d334023693280f5eb3d9c680fa3a2932bb48737b9dee324c16ce37b55de98dcfe802261664f
6
+ metadata.gz: 7d5dd24ccd3b4e61de24d7a46d02169ad7373d74776664fc0686db4d51dde98c8851c81e392043130f08a4dfd6cdb877a31c3a5ecb2d42c7bd2f5c92b1008fef
7
+ data.tar.gz: f8ed5ee65b6bee9dd1e36e1373248b12f3729a5ce5a22857c4212957246f7c3b150d48d5cea6e59a7e164d06e69bb53a5d90fc883924c85aaa4f6d64b15e4764
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.1"
2
+ ".": "0.5.0"
3
3
  }
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.7
1
+ 3.4.8
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.4.7
1
+ ruby 3.4.8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/open-feature/ruby-sdk/compare/v0.4.1...v0.5.0) (2026-01-16)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * add provider eventing, remove setProvider timeout ([#207](https://github.com/open-feature/ruby-sdk/issues/207))
9
+
10
+ ### Features
11
+
12
+ * add provider eventing, remove setProvider timeout ([#207](https://github.com/open-feature/ruby-sdk/issues/207)) ([fffd615](https://github.com/open-feature/ruby-sdk/commit/fffd6155ff308c75220185de030c38eb6eeac7e8))
13
+
3
14
  ## [0.4.1](https://github.com/open-feature/ruby-sdk/compare/v0.4.0...v0.4.1) (2025-11-03)
4
15
 
5
16
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.4.1)
4
+ openfeature-sdk (0.5.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -89,12 +89,14 @@ GEM
89
89
  rubocop-performance (~> 1.20.2)
90
90
  stringio (3.1.0)
91
91
  strscan (3.1.0)
92
+ timecop (0.9.10)
92
93
  unicode-display_width (2.5.0)
93
94
 
94
95
  PLATFORMS
95
96
  arm64-darwin-21
96
97
  arm64-darwin-22
97
98
  arm64-darwin-23
99
+ arm64-darwin-24
98
100
  x64-mingw-ucrt
99
101
  x64-mingw32
100
102
  x86_64-darwin-19
@@ -112,6 +114,7 @@ DEPENDENCIES
112
114
  simplecov-cobertura (~> 2.1.0)
113
115
  standard
114
116
  standard-performance
117
+ timecop (~> 0.9.10)
115
118
 
116
119
  BUNDLED WITH
117
120
  2.3.25
data/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [yyyy] [name of copyright owner]
189
+ Copyright OpenFeature Maintainers
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -17,8 +17,8 @@
17
17
  </a>
18
18
  <!-- x-release-please-start-version -->
19
19
 
20
- <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.4.1">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.4.1&color=blue&style=for-the-badge" />
20
+ <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.5.0">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.5.0&color=blue&style=for-the-badge" />
22
22
  </a>
23
23
 
24
24
  <!-- x-release-please-end -->
@@ -104,7 +104,7 @@ object = client.fetch_object_value(flag_key: 'object_value', default_value: { na
104
104
  | ⚠️ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
105
105
  | ❌ | [Logging](#logging) | Integrate with popular logging packages. |
106
106
  | ✅ | [Domains](#domains) | Logically bind clients with providers. |
107
- | | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
107
+ | | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
108
108
  | ⚠️ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
109
109
  | ❌ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) |
110
110
  | ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
@@ -142,6 +142,7 @@ begin
142
142
  rescue OpenFeature::SDK::ProviderInitializationError => e
143
143
  puts "Provider failed to initialize: #{e.message}"
144
144
  puts "Error code: #{e.error_code}"
145
+ # original_error contains the underlying exception that caused the initialization failure
145
146
  puts "Original error: #{e.original_error}"
146
147
  end
147
148
 
@@ -163,8 +164,9 @@ end
163
164
 
164
165
  The `set_provider_and_wait` method:
165
166
  - Waits for the provider's `init` method to complete successfully
166
- - Raises `ProviderInitializationError` with `PROVIDER_FATAL` error code if initialization fails or times out
167
- - Provides access to the original error, provider instance, and error code for debugging
167
+ - Raises `ProviderInitializationError` if initialization fails or times out
168
+ - Provides access to the provider instance, error code, and original exception for debugging
169
+ - The `original_error` field contains the underlying exception that caused the initialization failure
168
170
  - Uses the same thread-safe provider switching as `set_provider`
169
171
 
170
172
  In some situations, it may be beneficial to register multiple providers in the same application.
@@ -231,15 +233,47 @@ legacy_flag_client = OpenFeature::SDK.build_client(domain: "legacy_flags")
231
233
 
232
234
  ### Eventing
233
235
 
234
- Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/51) to be worked on.
235
-
236
- <!-- Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
236
+ Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
237
237
  Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
238
238
  Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`.
239
239
 
240
- Please refer to the documentation of the provider you're using to see what events are supported. -->
240
+ Please refer to the documentation of the provider you're using to see what events are supported.
241
+
242
+ ```ruby
243
+ # Register event handlers at the API (global) level
244
+ ready_handler = ->(event_details) do
245
+ puts "Provider #{event_details[:provider].metadata.name} is ready!"
246
+ end
247
+
248
+ OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, ready_handler)
249
+
250
+ # The SDK automatically emits lifecycle events. Providers can emit additional spontaneous events
251
+ # using the EventEmitter mixin to signal internal state changes like configuration updates.
252
+ class MyEventAwareProvider
253
+ include OpenFeature::SDK::Provider::EventEmitter
241
254
 
242
- <!-- TODO: code example of a PROVIDER_CONFIGURATION_CHANGED event for the client and a PROVIDER_STALE event for the API -->
255
+ def init(evaluation_context)
256
+ # Start background process to monitor for configuration changes
257
+ # Note: SDK automatically emits PROVIDER_READY when init completes successfully
258
+ start_background_process
259
+ end
260
+
261
+ def start_background_process
262
+ Thread.new do
263
+ # Monitor for configuration changes and emit events when they occur
264
+ if configuration_changed?
265
+ emit_event(
266
+ OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
267
+ message: "Flag configuration updated"
268
+ )
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ # Remove specific handlers when no longer needed
275
+ OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, ready_handler)
276
+ ```
243
277
 
244
278
  ### Shutdown
245
279
 
@@ -51,6 +51,22 @@ module OpenFeature
51
51
  rescue
52
52
  Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
53
53
  end
54
+
55
+ def add_handler(event_type, handler)
56
+ configuration.add_handler(event_type, handler)
57
+ end
58
+
59
+ def remove_handler(event_type, handler)
60
+ configuration.remove_handler(event_type, handler)
61
+ end
62
+
63
+ def logger
64
+ configuration.logger
65
+ end
66
+
67
+ def logger=(new_logger)
68
+ configuration.logger = new_logger
69
+ end
54
70
  end
55
71
  end
56
72
  end
@@ -27,6 +27,16 @@ module OpenFeature
27
27
  @hooks = []
28
28
  end
29
29
 
30
+ def add_handler(event_type, handler = nil, &block)
31
+ actual_handler = handler || block
32
+ OpenFeature::SDK.configuration.add_client_handler(self, event_type, actual_handler)
33
+ end
34
+
35
+ def remove_handler(event_type, handler = nil, &block)
36
+ actual_handler = handler || block
37
+ OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
38
+ end
39
+
30
40
  RESULT_TYPE.each do |result_type|
31
41
  SUFFIXES.each do |suffix|
32
42
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -3,6 +3,10 @@
3
3
  require "timeout"
4
4
  require_relative "api"
5
5
  require_relative "provider_initialization_error"
6
+ require_relative "event_dispatcher"
7
+ require_relative "provider_event"
8
+ require_relative "provider_state_registry"
9
+ require_relative "provider/event_emitter"
6
10
 
7
11
  module OpenFeature
8
12
  module SDK
@@ -14,73 +18,233 @@ module OpenFeature
14
18
  extend Forwardable
15
19
 
16
20
  attr_accessor :evaluation_context, :hooks
21
+ attr_reader :logger
17
22
 
18
23
  def initialize
19
24
  @hooks = []
20
25
  @providers = {}
21
26
  @provider_mutex = Mutex.new
27
+ @logger = nil
28
+ @event_dispatcher = EventDispatcher.new(@logger)
29
+ @provider_state_registry = ProviderStateRegistry.new
30
+ @client_handlers = {}
31
+ @client_handlers_mutex = Mutex.new
22
32
  end
23
33
 
24
34
  def provider(domain: nil)
25
35
  @providers[domain] || @providers[nil]
26
36
  end
27
37
 
28
- # When switching providers, there are a few lifecycle methods that need to be taken care of.
29
- # 1. If a provider is already set, we need to call `shutdown` on it.
30
- # 2. On the new provider, call `init`.
31
- # 3. Finally, set the internal provider to the new provider
38
+ def logger=(new_logger)
39
+ @logger = new_logger
40
+ @event_dispatcher.logger = new_logger if @event_dispatcher
41
+ end
42
+
43
+ def add_handler(event_type, handler)
44
+ @event_dispatcher.add_handler(event_type, handler)
45
+ run_immediate_handler(event_type, handler, nil)
46
+ end
47
+
48
+ def remove_handler(event_type, handler)
49
+ @event_dispatcher.remove_handler(event_type, handler)
50
+ end
51
+
52
+ # @api private
53
+ def add_client_handler(client, event_type, handler)
54
+ @client_handlers_mutex.synchronize do
55
+ @client_handlers[client] ||= Hash.new { |h, k| h[k] = [] }
56
+ @client_handlers[client][event_type] << handler
57
+ end
58
+
59
+ run_immediate_handler(event_type, handler, client)
60
+ end
61
+
62
+ # @api private
63
+ def remove_client_handler(client, event_type, handler)
64
+ @client_handlers_mutex.synchronize do
65
+ return unless @client_handlers[client]
66
+ handlers = @client_handlers[client][event_type]
67
+ handlers&.delete(handler)
68
+ end
69
+ end
70
+
32
71
  def set_provider(provider, domain: nil)
72
+ set_provider_internal(provider, domain: domain, wait_for_init: false)
73
+ end
74
+
75
+ def set_provider_and_wait(provider, domain: nil)
76
+ set_provider_internal(provider, domain: domain, wait_for_init: true)
77
+ end
78
+
79
+ private
80
+
81
+ def reset
82
+ @event_dispatcher.clear_all_handlers
83
+ @client_handlers_mutex.synchronize do
84
+ @client_handlers.clear
85
+ end
86
+ @provider_state_registry.clear
33
87
  @provider_mutex.synchronize do
34
- @providers[domain].shutdown if @providers[domain].respond_to?(:shutdown)
35
- provider.init if provider.respond_to?(:init)
36
- new_providers = @providers.dup
37
- new_providers[domain] = provider
38
- @providers = new_providers
88
+ @providers.clear
39
89
  end
40
90
  end
41
91
 
42
- # Sets a provider and waits for the initialization to complete or fail.
43
- # This method ensures the provider is ready (or in error state) before returning.
44
- #
45
- # @param provider [Object] the provider to set
46
- # @param domain [String, nil] the domain for the provider (optional)
47
- # @param timeout [Integer] maximum time to wait for initialization in seconds (default: 30)
48
- # @raise [ProviderInitializationError] if the provider fails to initialize or times out
49
- def set_provider_and_wait(provider, domain: nil, timeout: 30)
92
+ def set_provider_internal(provider, domain:, wait_for_init:)
93
+ # Capture evaluation context before acquiring mutex to prevent race conditions
94
+ context_for_init = @evaluation_context
95
+
96
+ old_provider, provider_to_init = nil
97
+
50
98
  @provider_mutex.synchronize do
51
99
  old_provider = @providers[domain]
52
100
 
53
- # Shutdown old provider (ignore errors)
101
+ # Remove old provider state to prevent memory leaks
102
+ @provider_state_registry.remove_provider(old_provider)
103
+
104
+ new_providers = @providers.dup
105
+ new_providers[domain] = provider
106
+ @providers = new_providers
107
+
108
+ @provider_state_registry.set_initial_state(provider)
109
+
110
+ provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
111
+
112
+ provider_to_init = provider
113
+ end
114
+
115
+ # Shutdown old provider outside mutex to avoid blocking other operations
116
+ # Only shutdown if it's a different provider to prevent race condition
117
+ if old_provider && old_provider != provider
54
118
  begin
55
119
  old_provider.shutdown if old_provider.respond_to?(:shutdown)
56
- rescue
57
- # Ignore shutdown errors and continue with provider initialization
120
+ rescue => e
121
+ @logger&.warn("Error shutting down previous provider #{old_provider&.class&.name || "unknown"}: #{e.message}")
58
122
  end
123
+ end
59
124
 
60
- begin
61
- # Initialize new provider with timeout
62
- if provider.respond_to?(:init)
63
- Timeout.timeout(timeout) do
64
- provider.init
125
+ # Initialize provider outside the mutex to avoid blocking other operations
126
+ if wait_for_init
127
+ init_provider(provider_to_init, context_for_init, raise_on_error: true)
128
+ else
129
+ Thread.new do
130
+ init_provider(provider_to_init, context_for_init, raise_on_error: false)
131
+ end
132
+ end
133
+ end
134
+
135
+ def init_provider(provider, context, raise_on_error: false)
136
+ if provider.respond_to?(:init)
137
+ init_method = provider.method(:init)
138
+ if init_method.parameters.empty?
139
+ provider.init
140
+ else
141
+ provider.init(context)
142
+ end
143
+ end
144
+
145
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
146
+ rescue => e
147
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
148
+ error_code: Provider::ErrorCode::GENERAL,
149
+ message: e.message)
150
+
151
+ if raise_on_error
152
+ # Re-raise as ProviderInitializationError for synchronous callers
153
+ raise ProviderInitializationError.new(
154
+ "Provider #{provider.class.name} initialization failed: #{e.message}",
155
+ provider:,
156
+ error_code: Provider::ErrorCode::GENERAL,
157
+ original_error: e
158
+ )
159
+ end
160
+ end
161
+
162
+ def dispatch_provider_event(provider, event_type, details = {})
163
+ @provider_state_registry.update_state_from_event(provider, event_type, details)
164
+
165
+ provider_name = extract_provider_name(provider)
166
+
167
+ event_details = {
168
+ provider_name: provider_name
169
+ }.merge(details)
170
+
171
+ run_handlers_for_provider(provider, event_type, event_details)
172
+ end
173
+
174
+ def provider_state(provider)
175
+ @provider_state_registry.get_state(provider)
176
+ end
177
+
178
+ private
179
+
180
+ def extract_provider_name(provider)
181
+ provider.respond_to?(:metadata) ? provider.metadata.name : provider.class.name
182
+ end
183
+
184
+ def run_handlers_for_provider(provider, event_type, event_details)
185
+ # Run global handlers (API-level, no domain filtering)
186
+ @event_dispatcher.trigger_event(event_type, event_details)
187
+
188
+ # Run client handlers (domain-scoped)
189
+ @client_handlers_mutex.synchronize do
190
+ @client_handlers.each do |client, handlers_by_event|
191
+ # Check if this client should receive events from this provider
192
+ client_provider = provider(domain: client.metadata.domain)
193
+ next unless client_provider&.equal?(provider)
194
+
195
+ # Trigger handlers for this client
196
+ handlers = handlers_by_event[event_type]
197
+ handlers.each do |handler|
198
+ handler.call(event_details)
199
+ rescue => e
200
+ @logger&.error("Client event handler failed: #{e.message}")
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ def run_immediate_handler(event_type, handler, client)
207
+ status_to_event = {
208
+ ProviderState::READY => ProviderEvent::PROVIDER_READY,
209
+ ProviderState::ERROR => ProviderEvent::PROVIDER_ERROR,
210
+ ProviderState::FATAL => ProviderEvent::PROVIDER_ERROR,
211
+ ProviderState::STALE => ProviderEvent::PROVIDER_STALE
212
+ }
213
+
214
+ if client.nil?
215
+ # API-level handler: check all providers
216
+ @providers.each do |domain, provider|
217
+ next unless provider
218
+
219
+ provider_state = @provider_state_registry.get_state(provider)
220
+
221
+ if event_type == status_to_event[provider_state]
222
+ provider_name = extract_provider_name(provider)
223
+ event_details = {provider_name: provider_name}
224
+
225
+ begin
226
+ handler.call(event_details)
227
+ rescue => e
228
+ @logger&.error("Immediate API event handler failed: #{e.message}")
65
229
  end
66
230
  end
231
+ end
232
+ else
233
+ # Client-level handler: check specific provider only
234
+ client_provider = provider(domain: client.metadata.domain)
235
+ return unless client_provider
67
236
 
68
- # Set the new provider
69
- new_providers = @providers.dup
70
- new_providers[domain] = provider
71
- @providers = new_providers
72
- rescue Timeout::Error => e
73
- raise ProviderInitializationError.new(
74
- "Provider initialization timed out after #{timeout} seconds",
75
- provider:,
76
- original_error: e
77
- )
78
- rescue => e
79
- raise ProviderInitializationError.new(
80
- "Provider initialization failed: #{e.message}",
81
- provider:,
82
- original_error: e
83
- )
237
+ provider_state = @provider_state_registry.get_state(client_provider)
238
+
239
+ if event_type == status_to_event[provider_state]
240
+ provider_name = extract_provider_name(client_provider)
241
+ event_details = {provider_name: provider_name}
242
+
243
+ begin
244
+ handler.call(event_details)
245
+ rescue => e
246
+ @logger&.error("Immediate client event handler failed: #{e.message}")
247
+ end
84
248
  end
85
249
  end
86
250
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider_event"
4
+
5
+ module OpenFeature
6
+ module SDK
7
+ # Thread-safe pub-sub for provider events
8
+ class EventDispatcher
9
+ attr_writer :logger
10
+
11
+ def initialize(logger = nil)
12
+ @handlers = {}
13
+ @mutex = Mutex.new
14
+ @logger = logger
15
+ ProviderEvent::ALL_EVENTS.each { |event| @handlers[event] = [] }
16
+ end
17
+
18
+ def add_handler(event_type, handler)
19
+ raise ArgumentError, "Invalid event type: #{event_type}" unless valid_event?(event_type)
20
+ raise ArgumentError, "Handler must respond to call" unless handler.respond_to?(:call)
21
+
22
+ @mutex.synchronize do
23
+ @handlers[event_type] << handler
24
+ end
25
+ end
26
+
27
+ def remove_handler(event_type, handler)
28
+ return unless valid_event?(event_type)
29
+
30
+ @mutex.synchronize do
31
+ @handlers[event_type].delete(handler)
32
+ end
33
+ end
34
+
35
+ def remove_all_handlers(event_type)
36
+ return unless valid_event?(event_type)
37
+
38
+ @mutex.synchronize do
39
+ @handlers[event_type].clear
40
+ end
41
+ end
42
+
43
+ def trigger_event(event_type, event_details = {})
44
+ return unless valid_event?(event_type)
45
+
46
+ handlers_to_call = nil
47
+ @mutex.synchronize do
48
+ handlers_to_call = @handlers[event_type].dup
49
+ end
50
+
51
+ # Call handlers outside of mutex to avoid deadlocks
52
+ handlers_to_call.each do |handler|
53
+ handler.call(event_details)
54
+ rescue => e
55
+ @logger&.warn "Event handler failed for #{event_type}: #{e.message}\n#{e.backtrace.join("\n")}"
56
+ end
57
+ end
58
+
59
+ def handler_count(event_type)
60
+ return 0 unless valid_event?(event_type)
61
+
62
+ @mutex.synchronize do
63
+ @handlers[event_type].size
64
+ end
65
+ end
66
+
67
+ def clear_all_handlers
68
+ @mutex.synchronize do
69
+ @handlers.each_value(&:clear)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def valid_event?(event_type)
76
+ ProviderEvent::ALL_EVENTS.include?(event_type)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../provider_event"
4
+
5
+ module OpenFeature
6
+ module SDK
7
+ module Provider
8
+ # Mixin for providers that emit lifecycle events
9
+ module EventEmitter
10
+ def emit_event(event_type, details = {})
11
+ config = @configuration
12
+ return unless config
13
+
14
+ unless ::OpenFeature::SDK::ProviderEvent::ALL_EVENTS.include?(event_type)
15
+ raise ArgumentError, "Invalid event type: #{event_type}"
16
+ end
17
+
18
+ config.send(:dispatch_provider_event, self, event_type, details)
19
+ end
20
+
21
+ def configuration_attached?
22
+ !@configuration.nil?
23
+ end
24
+
25
+ private
26
+
27
+ def attach(configuration)
28
+ @configuration = configuration
29
+ end
30
+
31
+ def detach
32
+ @configuration = nil
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -12,7 +12,7 @@ module OpenFeature
12
12
  @flags = flags
13
13
  end
14
14
 
15
- def init
15
+ def init(evaluation_context = nil)
16
16
  # Intentional no-op, used for testing
17
17
  end
18
18
 
@@ -2,9 +2,20 @@ require_relative "provider/error_code"
2
2
  require_relative "provider/reason"
3
3
  require_relative "provider/resolution_details"
4
4
  require_relative "provider/provider_metadata"
5
+
6
+ # Provider interfaces
7
+ require_relative "provider/event_emitter"
8
+
9
+ # Provider implementations
5
10
  require_relative "provider/no_op_provider"
6
11
  require_relative "provider/in_memory_provider"
7
12
 
13
+ # Event system components
14
+ require_relative "provider_event"
15
+ require_relative "provider_state"
16
+ require_relative "event_dispatcher"
17
+ require_relative "provider_state_registry"
18
+
8
19
  module OpenFeature
9
20
  module SDK
10
21
  module Provider
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ # Provider Event Types
6
+ #
7
+ # Defines the standard event types that providers can emit during their lifecycle.
8
+ # These events correspond to the OpenFeature specification events:
9
+ # https://openfeature.dev/specification/sections/events/
10
+ #
11
+ module ProviderEvent
12
+ PROVIDER_READY = "PROVIDER_READY"
13
+ PROVIDER_ERROR = "PROVIDER_ERROR"
14
+ PROVIDER_CONFIGURATION_CHANGED = "PROVIDER_CONFIGURATION_CHANGED"
15
+ PROVIDER_STALE = "PROVIDER_STALE"
16
+
17
+ ALL_EVENTS = [
18
+ PROVIDER_READY,
19
+ PROVIDER_ERROR,
20
+ PROVIDER_CONFIGURATION_CHANGED,
21
+ PROVIDER_STALE
22
+ ].freeze
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ # Provider State Types
6
+ #
7
+ # Defines the standard states that providers can be in during their lifecycle.
8
+ # These states correspond to the OpenFeature specification provider states:
9
+ # https://openfeature.dev/specification/types#provider-status
10
+ #
11
+ module ProviderState
12
+ NOT_READY = "NOT_READY"
13
+ READY = "READY"
14
+ ERROR = "ERROR"
15
+ STALE = "STALE"
16
+ FATAL = "FATAL"
17
+
18
+ ALL_STATES = [
19
+ NOT_READY,
20
+ READY,
21
+ ERROR,
22
+ STALE,
23
+ FATAL
24
+ ].freeze
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider_state"
4
+ require_relative "provider_event"
5
+ require_relative "provider/error_code"
6
+
7
+ module OpenFeature
8
+ module SDK
9
+ # Tracks provider states
10
+ class ProviderStateRegistry
11
+ def initialize
12
+ @states = {}
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def set_initial_state(provider, state = ProviderState::NOT_READY)
17
+ return unless provider
18
+
19
+ @mutex.synchronize do
20
+ @states[provider.object_id] = state
21
+ end
22
+ end
23
+
24
+ def update_state_from_event(provider, event_type, event_details = nil)
25
+ return ProviderState::NOT_READY unless provider
26
+
27
+ new_state = state_from_event(event_type, event_details)
28
+
29
+ # Only update state if the event should cause a state change
30
+ if new_state
31
+ @mutex.synchronize do
32
+ @states[provider.object_id] = new_state
33
+ end
34
+ new_state
35
+ else
36
+ # Return current state without changing it
37
+ get_state(provider)
38
+ end
39
+ end
40
+
41
+ def get_state(provider)
42
+ return ProviderState::NOT_READY unless provider
43
+
44
+ @mutex.synchronize do
45
+ @states[provider.object_id] || ProviderState::NOT_READY
46
+ end
47
+ end
48
+
49
+ def remove_provider(provider)
50
+ return unless provider
51
+
52
+ @mutex.synchronize do
53
+ @states.delete(provider.object_id)
54
+ end
55
+ end
56
+
57
+ def ready?(provider)
58
+ get_state(provider) == ProviderState::READY
59
+ end
60
+
61
+ def error?(provider)
62
+ state = get_state(provider)
63
+ [ProviderState::ERROR, ProviderState::FATAL].include?(state)
64
+ end
65
+
66
+ def clear
67
+ @mutex.synchronize do
68
+ @states.clear
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def state_from_event(event_type, event_details = nil)
75
+ case event_type
76
+ when ProviderEvent::PROVIDER_READY
77
+ ProviderState::READY
78
+ when ProviderEvent::PROVIDER_STALE
79
+ ProviderState::STALE
80
+ when ProviderEvent::PROVIDER_ERROR
81
+ state_from_error_event(event_details)
82
+ when ProviderEvent::PROVIDER_CONFIGURATION_CHANGED
83
+ nil # No state change per OpenFeature spec Requirement 5.3.5
84
+ else
85
+ nil # No state change for unknown events - conservative default
86
+ end
87
+ end
88
+
89
+ def state_from_error_event(event_details)
90
+ error_code = event_details&.dig(:error_code)
91
+ if error_code == Provider::ErrorCode::PROVIDER_FATAL
92
+ ProviderState::FATAL
93
+ else
94
+ ProviderState::ERROR
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openfeature-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -121,6 +121,20 @@ dependencies:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
123
  version: 2.1.0
124
+ - !ruby/object:Gem::Dependency
125
+ name: timecop
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 0.9.10
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.9.10
124
138
  description: Ruby SDK for an the specifications for the open standard of feature flag
125
139
  management
126
140
  email:
@@ -152,21 +166,26 @@ files:
152
166
  - lib/open_feature/sdk/evaluation_context.rb
153
167
  - lib/open_feature/sdk/evaluation_context_builder.rb
154
168
  - lib/open_feature/sdk/evaluation_details.rb
169
+ - lib/open_feature/sdk/event_dispatcher.rb
155
170
  - lib/open_feature/sdk/hooks/hints.rb
156
171
  - lib/open_feature/sdk/provider.rb
157
172
  - lib/open_feature/sdk/provider/error_code.rb
173
+ - lib/open_feature/sdk/provider/event_emitter.rb
158
174
  - lib/open_feature/sdk/provider/in_memory_provider.rb
159
175
  - lib/open_feature/sdk/provider/no_op_provider.rb
160
176
  - lib/open_feature/sdk/provider/provider_metadata.rb
161
177
  - lib/open_feature/sdk/provider/reason.rb
162
178
  - lib/open_feature/sdk/provider/resolution_details.rb
179
+ - lib/open_feature/sdk/provider_event.rb
163
180
  - lib/open_feature/sdk/provider_initialization_error.rb
181
+ - lib/open_feature/sdk/provider_state.rb
182
+ - lib/open_feature/sdk/provider_state_registry.rb
164
183
  - lib/open_feature/sdk/version.rb
165
184
  - release-please-config.json
166
185
  - renovate.json
167
186
  homepage: https://github.com/open-feature/openfeature-ruby
168
187
  licenses:
169
- - Apache-2.0'
188
+ - Apache-2.0
170
189
  metadata:
171
190
  homepage_uri: https://github.com/open-feature/openfeature-ruby
172
191
  source_code_uri: https://github.com/open-feature/openfeature-ruby