openfeature-sdk 0.5.0 → 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: c91c19e8a0f7ef628c048224287a276713d704914142d4a2771bd74bf01ea68e
4
- data.tar.gz: 565d980320e40d5a573c4710d6313f038c0a58350f6d5eb1564dcd5c85148853
3
+ metadata.gz: 36540a362c2f482ce84d2aa8ba3a6efd9b5d68fe7881ed4037f1422ad85f5525
4
+ data.tar.gz: 4f7e6a764074d50467949e056b0d50be794df0a67323bb80c5e34a724236314b
5
5
  SHA512:
6
- metadata.gz: 7d5dd24ccd3b4e61de24d7a46d02169ad7373d74776664fc0686db4d51dde98c8851c81e392043130f08a4dfd6cdb877a31c3a5ecb2d42c7bd2f5c92b1008fef
7
- data.tar.gz: f8ed5ee65b6bee9dd1e36e1373248b12f3729a5ce5a22857c4212957246f7c3b150d48d5cea6e59a7e164d06e69bb53a5d90fc883924c85aaa4f6d64b15e4764
6
+ metadata.gz: b77f6731c17b537069bbf72cf4760e88293bc59c9ae147079ff3b24ac6ac7f1b18b42b49e29fd1a04a85a6054d0a43834289561c06d6b342ba579ec23d510ef8
7
+ data.tar.gz: 077d8a3f20c396c82413dad70b9a93cff0cd27a76ff8034f67c50bfa0d08235e3424755a4cd33b81c39fed962d7a62e40d1f257d3bbc4e87bac787d0e315b3fb
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.0"
2
+ ".": "0.5.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.5.0](https://github.com/open-feature/ruby-sdk/compare/v0.4.1...v0.5.0) (2026-01-16)
4
11
 
5
12
 
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.5.0)
4
+ openfeature-sdk (0.5.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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.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" />
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 -->
@@ -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
 
@@ -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
 
@@ -40,11 +41,8 @@ module OpenFeature
40
41
  RESULT_TYPE.each do |result_type|
41
42
  SUFFIXES.each do |suffix|
42
43
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
43
- # def fetch_boolean_details(flag_key:, default_value:, evaluation_context: nil)
44
- # result = @provider.fetch_boolean_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
45
- # end
46
- def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context: nil)
47
- 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)
48
46
  #{"evaluation_details.value" if suffix == :value}
49
47
  end
50
48
  RUBY
@@ -53,12 +51,54 @@ module OpenFeature
53
51
 
54
52
  private
55
53
 
56
- 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)
57
55
  validate_default_value_type(type, default_value)
58
56
 
59
- 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
+ )
60
62
 
61
- resolution_details = @provider.send(:"fetch_#{type}_value", flag_key:, default_value:, evaluation_context: built_context)
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
94
+
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
+ )
62
102
 
63
103
  if TYPE_CLASS_MAP[type].none? { |klass| resolution_details.value.is_a?(klass) }
64
104
  resolution_details.value = default_value
@@ -66,7 +106,7 @@ module OpenFeature
66
106
  resolution_details.reason = Provider::Reason::ERROR
67
107
  end
68
108
 
69
- EvaluationDetails.new(flag_key:, resolution_details:)
109
+ EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
70
110
  end
71
111
 
72
112
  def validate_default_value_type(type, default_value)
@@ -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"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.5.0"
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.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -150,6 +150,7 @@ files:
150
150
  - ".standard.yml"
151
151
  - ".tool-versions"
152
152
  - CHANGELOG.md
153
+ - CLAUDE.md
153
154
  - CODEOWNERS
154
155
  - CODE_OF_CONDUCT.md
155
156
  - CONTRIBUTING.md
@@ -167,7 +168,11 @@ files:
167
168
  - lib/open_feature/sdk/evaluation_context_builder.rb
168
169
  - lib/open_feature/sdk/evaluation_details.rb
169
170
  - lib/open_feature/sdk/event_dispatcher.rb
171
+ - lib/open_feature/sdk/hooks.rb
170
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
171
176
  - lib/open_feature/sdk/provider.rb
172
177
  - lib/open_feature/sdk/provider/error_code.rb
173
178
  - lib/open_feature/sdk/provider/event_emitter.rb