openfeature-sdk 0.4.1 → 0.5.1

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: 36540a362c2f482ce84d2aa8ba3a6efd9b5d68fe7881ed4037f1422ad85f5525
4
+ data.tar.gz: 4f7e6a764074d50467949e056b0d50be794df0a67323bb80c5e34a724236314b
5
5
  SHA512:
6
- metadata.gz: 413138e8e435e278376a6a8ac70a25d1673128e76ed454bfffec9e894517a61f42c8e31ef3adfc25ba71786083eb574427e0b6dfa9748dec099c807dce3b3a7d
7
- data.tar.gz: 1f494723edf1c24559e19e2083c59fffa6f988f723d46f65df1c7d334023693280f5eb3d9c680fa3a2932bb48737b9dee324c16ce37b55de98dcfe802261664f
6
+ metadata.gz: b77f6731c17b537069bbf72cf4760e88293bc59c9ae147079ff3b24ac6ac7f1b18b42b49e29fd1a04a85a6054d0a43834289561c06d6b342ba579ec23d510ef8
7
+ data.tar.gz: 077d8a3f20c396c82413dad70b9a93cff0cd27a76ff8034f67c50bfa0d08235e3424755a4cd33b81c39fed962d7a62e40d1f257d3bbc4e87bac787d0e315b3fb
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.1"
2
+ ".": "0.5.1"
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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1](https://github.com/open-feature/ruby-sdk/compare/v0.5.0...v0.5.1) (2026-03-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * implement hooks lifecycle (spec section 4) ([#214](https://github.com/open-feature/ruby-sdk/issues/214)) ([41c3b9e](https://github.com/open-feature/ruby-sdk/commit/41c3b9e2b73b34f50d8135122fb4592f8ec44e49))
9
+
10
+ ## [0.5.0](https://github.com/open-feature/ruby-sdk/compare/v0.4.1...v0.5.0) (2026-01-16)
11
+
12
+
13
+ ### ⚠ BREAKING CHANGES
14
+
15
+ * add provider eventing, remove setProvider timeout ([#207](https://github.com/open-feature/ruby-sdk/issues/207))
16
+
17
+ ### Features
18
+
19
+ * 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))
20
+
3
21
  ## [0.4.1](https://github.com/open-feature/ruby-sdk/compare/v0.4.0...v0.4.1) (2025-11-03)
4
22
 
5
23
 
data/CLAUDE.md ADDED
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ OpenFeature Ruby SDK — implements the [OpenFeature specification](https://openfeature.dev) (v0.8.0) for vendor-agnostic feature flag management. Published as the `openfeature-sdk` gem. Pure Ruby, no runtime dependencies. Requires Ruby >= 3.1.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ bundle install
14
+
15
+ # Run full test suite + linting (default rake task)
16
+ bundle exec rake
17
+
18
+ # Run tests only
19
+ bundle exec rspec
20
+
21
+ # Run a single test file
22
+ bundle exec rspec spec/open_feature/sdk/client_spec.rb
23
+
24
+ # Run a specific test by line number
25
+ bundle exec rspec spec/open_feature/sdk/client_spec.rb:40
26
+
27
+ # Lint (StandardRB with performance plugin)
28
+ bundle exec rake standard
29
+
30
+ # Auto-fix lint issues
31
+ bundle exec standardrb --fix
32
+ ```
33
+
34
+ ## Architecture
35
+
36
+ Entry point: `require 'open_feature/sdk'` — the `OpenFeature::SDK` module delegates all method calls to `API.instance` (Singleton) via `method_missing`.
37
+
38
+ ### Core Components
39
+
40
+ - **API** (`lib/open_feature/sdk/api.rb`) — Singleton orchestrator. Manages providers (global or domain-scoped), builds clients, stores API-level evaluation context, and registers event handlers.
41
+ - **Configuration** (`lib/open_feature/sdk/configuration.rb`) — Thread-safe provider storage. Handles provider lifecycle (init/shutdown), domain-scoped provider mapping, and event dispatching. Uses Mutex for all shared state.
42
+ - **Client** (`lib/open_feature/sdk/client.rb`) — Flag evaluation interface. Uses `class_eval` metaprogramming to generate 12 typed methods: `fetch_{boolean,string,number,integer,float,object}_value` and `fetch_*_details` variants. Merges evaluation contexts (API + client + invocation).
43
+ - **EvaluationContext** (`lib/open_feature/sdk/evaluation_context.rb`) — Key-value targeting data with a special `targeting_key`. Supports merging with precedence: invocation > client > API.
44
+
45
+ ### Provider System
46
+
47
+ - **Provider interface** — Must implement 6 `fetch_*_value` methods, optional `init(evaluation_context)` and `shutdown`. Returns `ResolutionDetails`.
48
+ - **EventEmitter** (`lib/open_feature/sdk/provider/event_emitter.rb`) — Mixin that providers include to emit lifecycle events.
49
+ - **Built-in providers**: `NoOpProvider` (default), `InMemoryProvider` (testing/examples).
50
+ - **Provider states**: `NOT_READY → READY`, with `ERROR`, `FATAL`, `STALE` transitions. Tracked per-instance via `ProviderStateRegistry` using `object_id`.
51
+ - **Initialization modes**: `set_provider` (async, background thread) or `set_provider_and_wait` (sync, raises `ProviderInitializationError` on failure).
52
+
53
+ ### Event System
54
+
55
+ - **EventDispatcher** (`lib/open_feature/sdk/event_dispatcher.rb`) — Thread-safe pub-sub. Handlers called outside mutex to prevent deadlocks. Supports API-level and client-level handlers.
56
+ - **ProviderEvent** constants: `PROVIDER_READY`, `PROVIDER_ERROR`, `PROVIDER_STALE`, `PROVIDER_CONFIGURATION_CHANGED`.
57
+
58
+ ## Test Structure
59
+
60
+ Tests in `spec/` split into two categories:
61
+ - `spec/specification/` — OpenFeature spec compliance tests, organized by requirement number (e.g., "Requirement 1.1.1")
62
+ - `spec/open_feature/` — Unit tests for individual components
63
+
64
+ Uses Timecop for time-sensitive tests (auto-reset after each test), SimpleCov for coverage.
65
+
66
+ ## Conventions
67
+
68
+ - **Linter**: StandardRB (Ruby Standard Style) with `standard-performance` plugin, targeting Ruby 3.1
69
+ - **Commits**: Conventional Commits required for PR titles (enforced by CI)
70
+ - **Releases**: Automated via release-please; changelog auto-generated
71
+ - **Threading**: All shared mutable state must be Mutex-protected. Provider storage uses immutable reassignment (`@providers = @providers.dup.merge(...)`)
72
+ - **Structs for DTOs**: `EvaluationDetails`, `ResolutionDetails`, `ClientMetadata`, `ProviderMetadata` are `Struct`-based
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.1)
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.1">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.5.1&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
 
@@ -8,6 +8,7 @@ require_relative "evaluation_context"
8
8
  require_relative "evaluation_context_builder"
9
9
  require_relative "evaluation_details"
10
10
  require_relative "client_metadata"
11
+ require_relative "hooks"
11
12
  require_relative "client"
12
13
  require_relative "provider"
13
14
 
@@ -51,6 +52,22 @@ module OpenFeature
51
52
  rescue
52
53
  Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
53
54
  end
55
+
56
+ def add_handler(event_type, handler)
57
+ configuration.add_handler(event_type, handler)
58
+ end
59
+
60
+ def remove_handler(event_type, handler)
61
+ configuration.remove_handler(event_type, handler)
62
+ end
63
+
64
+ def logger
65
+ configuration.logger
66
+ end
67
+
68
+ def logger=(new_logger)
69
+ configuration.logger = new_logger
70
+ end
54
71
  end
55
72
  end
56
73
  end
@@ -15,6 +15,7 @@ module OpenFeature
15
15
  }.freeze
16
16
  RESULT_TYPE = TYPE_CLASS_MAP.keys.freeze
17
17
  SUFFIXES = %i[value details].freeze
18
+ EMPTY_HINTS = Hooks::Hints.new.freeze
18
19
 
19
20
  attr_reader :metadata, :evaluation_context
20
21
 
@@ -27,14 +28,21 @@ module OpenFeature
27
28
  @hooks = []
28
29
  end
29
30
 
31
+ def add_handler(event_type, handler = nil, &block)
32
+ actual_handler = handler || block
33
+ OpenFeature::SDK.configuration.add_client_handler(self, event_type, actual_handler)
34
+ end
35
+
36
+ def remove_handler(event_type, handler = nil, &block)
37
+ actual_handler = handler || block
38
+ OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
39
+ end
40
+
30
41
  RESULT_TYPE.each do |result_type|
31
42
  SUFFIXES.each do |suffix|
32
43
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
33
- # def fetch_boolean_details(flag_key:, default_value:, evaluation_context: nil)
34
- # result = @provider.fetch_boolean_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
35
- # end
36
- def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context: nil)
37
- evaluation_details = fetch_details(type: :#{result_type}, flag_key:, default_value:, evaluation_context:)
44
+ def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context: nil, hooks: [], hook_hints: nil)
45
+ evaluation_details = fetch_details(type: :#{result_type}, flag_key:, default_value:, evaluation_context:, invocation_hooks: hooks, hook_hints: hook_hints)
38
46
  #{"evaluation_details.value" if suffix == :value}
39
47
  end
40
48
  RUBY
@@ -43,12 +51,54 @@ module OpenFeature
43
51
 
44
52
  private
45
53
 
46
- def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil)
54
+ def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
47
55
  validate_default_value_type(type, default_value)
48
56
 
49
- built_context = EvaluationContextBuilder.new.call(api_context: OpenFeature::SDK.evaluation_context, client_context: self.evaluation_context, invocation_context: evaluation_context)
57
+ built_context = EvaluationContextBuilder.new.call(
58
+ api_context: OpenFeature::SDK.evaluation_context,
59
+ client_context: self.evaluation_context,
60
+ invocation_context: evaluation_context
61
+ )
62
+
63
+ # Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
64
+ provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
65
+ ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
66
+
67
+ # Fast path: skip hook ceremony when no hooks are registered
68
+ if ordered_hooks.empty?
69
+ return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
70
+ end
71
+
72
+ hook_context = Hooks::HookContext.new(
73
+ flag_key: flag_key,
74
+ flag_value_type: type,
75
+ default_value: default_value,
76
+ evaluation_context: built_context,
77
+ client_metadata: @metadata,
78
+ provider_metadata: @provider.respond_to?(:metadata) ? @provider.metadata : nil
79
+ )
80
+
81
+ hints = if hook_hints.is_a?(Hooks::Hints)
82
+ hook_hints
83
+ elsif hook_hints
84
+ Hooks::Hints.new(hook_hints)
85
+ else
86
+ EMPTY_HINTS
87
+ end
88
+
89
+ executor = Hooks::HookExecutor.new(logger: OpenFeature::SDK.configuration.logger)
90
+ executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints) do |hctx|
91
+ evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
92
+ end
93
+ end
50
94
 
51
- resolution_details = @provider.send(:"fetch_#{type}_value", flag_key:, default_value:, evaluation_context: built_context)
95
+ def evaluate_flag(type:, flag_key:, default_value:, evaluation_context:)
96
+ resolution_details = @provider.send(
97
+ :"fetch_#{type}_value",
98
+ flag_key: flag_key,
99
+ default_value: default_value,
100
+ evaluation_context: evaluation_context
101
+ )
52
102
 
53
103
  if TYPE_CLASS_MAP[type].none? { |klass| resolution_details.value.is_a?(klass) }
54
104
  resolution_details.value = default_value
@@ -56,7 +106,7 @@ module OpenFeature
56
106
  resolution_details.reason = Provider::Reason::ERROR
57
107
  end
58
108
 
59
- EvaluationDetails.new(flag_key:, resolution_details:)
109
+ EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
60
110
  end
61
111
 
62
112
  def validate_default_value_type(type, default_value)
@@ -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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
4
+
3
5
  module OpenFeature
4
6
  module SDK
5
7
  module Hooks
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ module Hooks
6
+ # Module that hooks include. Provides default no-op implementations
7
+ # for all four lifecycle stages. A hook overrides the stages it cares about.
8
+ #
9
+ # Spec 4.3.1: Hooks MUST specify at least one stage.
10
+ module Hook
11
+ # Called before flag evaluation. May return an EvaluationContext
12
+ # that gets merged into the existing context (spec 4.3.2.1, 4.3.4, 4.3.5).
13
+ def before(hook_context:, hints:)
14
+ nil
15
+ end
16
+
17
+ # Called after successful flag evaluation (spec 4.3.3).
18
+ def after(hook_context:, evaluation_details:, hints:)
19
+ nil
20
+ end
21
+
22
+ # Called when an error occurs during flag evaluation (spec 4.3.6).
23
+ def error(hook_context:, exception:, hints:)
24
+ nil
25
+ end
26
+
27
+ # Called unconditionally after flag evaluation (spec 4.3.7).
28
+ def finally(hook_context:, evaluation_details:, hints:)
29
+ nil
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ module Hooks
6
+ # Provides context to hook stages during flag evaluation.
7
+ #
8
+ # Per spec 4.1.1-4.1.5:
9
+ # - flag_key, flag_value_type, default_value are immutable (4.1.3)
10
+ # - client_metadata, provider_metadata are optional (4.1.2)
11
+ # - evaluation_context is mutable (for before hooks to modify, 4.1.4.1)
12
+ class HookContext
13
+ attr_reader :flag_key, :flag_value_type, :default_value,
14
+ :client_metadata, :provider_metadata
15
+ attr_accessor :evaluation_context
16
+
17
+ def initialize(flag_key:, flag_value_type:, default_value:, evaluation_context:,
18
+ client_metadata: nil, provider_metadata: nil)
19
+ @flag_key = flag_key.freeze
20
+ @flag_value_type = flag_value_type.freeze
21
+ @default_value = default_value.freeze
22
+ @evaluation_context = evaluation_context
23
+ @client_metadata = client_metadata
24
+ @provider_metadata = provider_metadata
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ module Hooks
6
+ # Orchestrates the full hook lifecycle for flag evaluation.
7
+ #
8
+ # Hook execution order (spec 4.4.2):
9
+ # Before: API → Client → Invocation → Provider
10
+ # After/Error/Finally: Provider → Invocation → Client → API (reverse)
11
+ #
12
+ # Error handling (spec 4.4.3-4.4.7):
13
+ # - Before/after hook error → stop remaining hooks, run error hooks, return default
14
+ # - Error hook error → log, continue remaining error hooks
15
+ # - Finally hook error → log, continue remaining finally hooks
16
+ class HookExecutor
17
+ def initialize(logger: nil)
18
+ @logger = logger
19
+ end
20
+
21
+ # Executes the full hook lifecycle around the flag evaluation block.
22
+ #
23
+ # @param ordered_hooks [Array] hooks in before-order (API, Client, Invocation, Provider)
24
+ # @param hook_context [HookContext] the hook context
25
+ # @param hints [Hints] hook hints
26
+ # @param evaluate_block [Proc] the flag evaluation to wrap
27
+ # @return [EvaluationDetails] the evaluation result
28
+ def execute(ordered_hooks:, hook_context:, hints:, &evaluate_block)
29
+ evaluation_details = nil
30
+
31
+ begin
32
+ run_before_hooks(ordered_hooks, hook_context, hints)
33
+ evaluation_details = evaluate_block.call(hook_context)
34
+ run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
35
+ rescue => e
36
+ run_error_hooks(ordered_hooks, hook_context, e, hints)
37
+
38
+ evaluation_details = EvaluationDetails.new(
39
+ flag_key: hook_context.flag_key,
40
+ resolution_details: Provider::ResolutionDetails.new(
41
+ value: hook_context.default_value,
42
+ error_code: Provider::ErrorCode::GENERAL,
43
+ reason: Provider::Reason::ERROR,
44
+ error_message: e.message
45
+ )
46
+ )
47
+ ensure
48
+ run_finally_hooks(ordered_hooks, hook_context, evaluation_details, hints)
49
+ end
50
+
51
+ evaluation_details
52
+ end
53
+
54
+ private
55
+
56
+ # Spec 4.4.2: Before hooks run in order: API → Client → Invocation → Provider
57
+ # Spec 4.3.4/4.3.5: If a before hook returns an EvaluationContext, it is merged
58
+ # into the existing context for subsequent hooks and evaluation.
59
+ def run_before_hooks(hooks, hook_context, hints)
60
+ hooks.each do |hook|
61
+ next unless hook.respond_to?(:before)
62
+ result = hook.before(hook_context: hook_context, hints: hints)
63
+ if result.is_a?(EvaluationContext)
64
+ existing = hook_context.evaluation_context
65
+ hook_context.evaluation_context = existing ? existing.merge(result) : result
66
+ end
67
+ end
68
+ end
69
+
70
+ # Spec 4.4.2: After hooks run in reverse order: Provider → Invocation → Client → API
71
+ def run_after_hooks(hooks, hook_context, evaluation_details, hints)
72
+ hooks.reverse_each do |hook|
73
+ next unless hook.respond_to?(:after)
74
+ hook.after(hook_context: hook_context, evaluation_details: evaluation_details, hints: hints)
75
+ end
76
+ end
77
+
78
+ # Spec 4.4.4: Error hooks run in reverse order.
79
+ # If an error hook itself errors, log and continue remaining error hooks.
80
+ def run_error_hooks(hooks, hook_context, exception, hints)
81
+ hooks.reverse_each do |hook|
82
+ next unless hook.respond_to?(:error)
83
+ hook.error(hook_context: hook_context, exception: exception, hints: hints)
84
+ rescue => e
85
+ @logger&.error("Error hook #{hook.class.name} failed: #{e.message}")
86
+ end
87
+ end
88
+
89
+ # Spec 4.4.3: Finally hooks run in reverse order unconditionally.
90
+ # If a finally hook errors, log and continue remaining finally hooks.
91
+ def run_finally_hooks(hooks, hook_context, evaluation_details, hints)
92
+ hooks.reverse_each do |hook|
93
+ next unless hook.respond_to?(:finally)
94
+ hook.finally(hook_context: hook_context, evaluation_details: evaluation_details, hints: hints)
95
+ rescue => e
96
+ @logger&.error("Finally hook #{hook.class.name} failed: #{e.message}")
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hooks/hints"
4
+ require_relative "hooks/hook"
5
+ require_relative "hooks/hook_context"
6
+ require_relative "hooks/hook_executor"
@@ -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.1"
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.1
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:
@@ -136,6 +150,7 @@ files:
136
150
  - ".standard.yml"
137
151
  - ".tool-versions"
138
152
  - CHANGELOG.md
153
+ - CLAUDE.md
139
154
  - CODEOWNERS
140
155
  - CODE_OF_CONDUCT.md
141
156
  - CONTRIBUTING.md
@@ -152,21 +167,30 @@ files:
152
167
  - lib/open_feature/sdk/evaluation_context.rb
153
168
  - lib/open_feature/sdk/evaluation_context_builder.rb
154
169
  - lib/open_feature/sdk/evaluation_details.rb
170
+ - lib/open_feature/sdk/event_dispatcher.rb
171
+ - lib/open_feature/sdk/hooks.rb
155
172
  - lib/open_feature/sdk/hooks/hints.rb
173
+ - lib/open_feature/sdk/hooks/hook.rb
174
+ - lib/open_feature/sdk/hooks/hook_context.rb
175
+ - lib/open_feature/sdk/hooks/hook_executor.rb
156
176
  - lib/open_feature/sdk/provider.rb
157
177
  - lib/open_feature/sdk/provider/error_code.rb
178
+ - lib/open_feature/sdk/provider/event_emitter.rb
158
179
  - lib/open_feature/sdk/provider/in_memory_provider.rb
159
180
  - lib/open_feature/sdk/provider/no_op_provider.rb
160
181
  - lib/open_feature/sdk/provider/provider_metadata.rb
161
182
  - lib/open_feature/sdk/provider/reason.rb
162
183
  - lib/open_feature/sdk/provider/resolution_details.rb
184
+ - lib/open_feature/sdk/provider_event.rb
163
185
  - lib/open_feature/sdk/provider_initialization_error.rb
186
+ - lib/open_feature/sdk/provider_state.rb
187
+ - lib/open_feature/sdk/provider_state_registry.rb
164
188
  - lib/open_feature/sdk/version.rb
165
189
  - release-please-config.json
166
190
  - renovate.json
167
191
  homepage: https://github.com/open-feature/openfeature-ruby
168
192
  licenses:
169
- - Apache-2.0'
193
+ - Apache-2.0
170
194
  metadata:
171
195
  homepage_uri: https://github.com/open-feature/openfeature-ruby
172
196
  source_code_uri: https://github.com/open-feature/openfeature-ruby