openfeature-sdk 0.5.0 → 0.6.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: c91c19e8a0f7ef628c048224287a276713d704914142d4a2771bd74bf01ea68e
4
- data.tar.gz: 565d980320e40d5a573c4710d6313f038c0a58350f6d5eb1564dcd5c85148853
3
+ metadata.gz: af657f35506e7f76184269c57877bbe87cb75f03a2e697e228e9b48f0b2bb17c
4
+ data.tar.gz: 57944d4c3a74838e73a1f55b4f06e9b82ec4b5186c76ad000577504b30d72d88
5
5
  SHA512:
6
- metadata.gz: 7d5dd24ccd3b4e61de24d7a46d02169ad7373d74776664fc0686db4d51dde98c8851c81e392043130f08a4dfd6cdb877a31c3a5ecb2d42c7bd2f5c92b1008fef
7
- data.tar.gz: f8ed5ee65b6bee9dd1e36e1373248b12f3729a5ce5a22857c4212957246f7c3b150d48d5cea6e59a7e164d06e69bb53a5d90fc883924c85aaa4f6d64b15e4764
6
+ metadata.gz: 8dc359e5bb318d79e2fcb48bc0c393c055578cf0a331b02bc61f82910b3da973eaf487457db80d9cd73f12303b15a77cbef9062481933c3d7b4c305de0ed1cc2
7
+ data.tar.gz: c733ce2f12dbd64ad9fd4e42a5c6cd17c805aa0ab746408c222aee42e6af5ba385ff92baf6cffbf79e4e949dbd7e6b5b2c67937694717ef5b41396353b9f9391
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.0"
2
+ ".": "0.6.0"
3
3
  }
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.8
1
+ 4.0.1
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.4.8
1
+ ruby 4.0.1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/open-feature/ruby-sdk/compare/v0.5.1...v0.6.0) (2026-03-05)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * add Ruby 4.0 support, require minimum Ruby 3.4 ([#217](https://github.com/open-feature/ruby-sdk/issues/217))
9
+
10
+ ### Features
11
+
12
+ * add Ruby 4.0 support, require minimum Ruby 3.4 ([#217](https://github.com/open-feature/ruby-sdk/issues/217)) ([f38ba40](https://github.com/open-feature/ruby-sdk/commit/f38ba40b31beb650ba475c631947fc7969e476fa))
13
+
14
+ ## [0.5.1](https://github.com/open-feature/ruby-sdk/compare/v0.5.0...v0.5.1) (2026-03-04)
15
+
16
+
17
+ ### Features
18
+
19
+ * 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))
20
+
3
21
  ## [0.5.0](https://github.com/open-feature/ruby-sdk/compare/v0.4.1...v0.5.0) (2026-01-16)
4
22
 
5
23
 
data/CLAUDE.md ADDED
@@ -0,0 +1,51 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Overview
6
+
7
+ This is the official OpenFeature SDK for Ruby — an implementation of the [OpenFeature specification](https://openfeature.dev) providing a vendor-agnostic API for feature flag evaluation. Published as the `openfeature-sdk` gem. Requires Ruby >= 3.1.
8
+
9
+ ## Commands
10
+
11
+ - **Run all tests:** `bundle exec rspec`
12
+ - **Run a single test file:** `bundle exec rspec spec/open_feature/sdk/client_spec.rb`
13
+ - **Run a specific test by line:** `bundle exec rspec spec/open_feature/sdk/client_spec.rb:43`
14
+ - **Lint:** `bundle exec standardrb`
15
+ - **Lint with autofix:** `bundle exec standardrb --fix`
16
+ - **Default rake (tests + lint):** `bundle exec rake`
17
+
18
+ Note: Linting uses [Standard Ruby](https://github.com/standardrb/standard) (configured via the `standard` gem), which enforces double-quoted strings and its own opinionated style. There is no `.rubocop.yml` — Standard manages RuboCop configuration internally. Do not use `bundle exec rubocop` directly as a stale RuboCop server may apply different rules; always use `bundle exec standardrb`.
19
+
20
+ ## Architecture
21
+
22
+ ### Entry point and API singleton
23
+
24
+ `OpenFeature::SDK` (in `lib/open_feature/sdk.rb`) delegates all method calls to `API.instance` via `method_missing`. `API` is a Singleton that holds a `Configuration` object and builds `Client` instances.
25
+
26
+ ### Provider duck type
27
+
28
+ Providers are not subclasses — they follow a duck type interface. Any object implementing `fetch_boolean_value`, `fetch_string_value`, `fetch_number_value`, `fetch_integer_value`, `fetch_float_value`, and `fetch_object_value` (all accepting `flag_key:`, `default_value:`, `evaluation_context:`) works as a provider. Each method must return a `ResolutionDetails` struct. Two built-in providers exist: `NoOpProvider` (default) and `InMemoryProvider` (for testing). Providers may optionally implement `init(evaluation_context)`, `shutdown`, and `metadata`.
29
+
30
+ ### Client dynamic method generation
31
+
32
+ `Client` uses `class_eval` to metaprogram `fetch_<type>_value` and `fetch_<type>_details` methods from `RESULT_TYPE` and `SUFFIXES` arrays. This generates 12 public methods (6 types × 2 suffixes).
33
+
34
+ ### Evaluation context merging
35
+
36
+ `EvaluationContextBuilder` merges three layers of context with this precedence: invocation > client > API (global). Context is a hash-like object with a special `targeting_key` field.
37
+
38
+ ### Provider eventing
39
+
40
+ `Configuration` manages provider lifecycle events (READY, ERROR, STALE, CONFIGURATION_CHANGED). Providers can emit spontaneous events by including `Provider::EventEmitter`. Event handlers can be registered at API level (global) or client level (domain-scoped). `ProviderStateRegistry` tracks provider states; `EventDispatcher` manages handler registration and invocation.
41
+
42
+ ### Domain-based provider binding
43
+
44
+ Providers can be registered for specific domains. `Configuration#provider(domain:)` resolves domain-specific providers, falling back to the default (nil-domain) provider. Clients are built with an optional `domain:` that binds them to a specific provider.
45
+
46
+ ## Conventions
47
+
48
+ - All `.rb` files must have `# frozen_string_literal: true` as the first line.
49
+ - Tests live under `spec/` and mirror the `lib/` structure. `spec/specification/` contains tests mapped to OpenFeature spec requirements.
50
+ - Always sign git commits using the `-S` flag.
51
+ - Always include DCO sign-off in commits using the `-s` flag (i.e., `git commit -s -S`). This adds a `Signed-off-by` trailer required by the project's CI.
data/Gemfile.lock CHANGED
@@ -1,41 +1,51 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.5.0)
4
+ openfeature-sdk (0.6.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- ast (2.4.2)
10
- debug (1.9.2)
9
+ ast (2.4.3)
10
+ date (3.5.1)
11
+ debug (1.11.1)
11
12
  irb (~> 1.10)
12
13
  reline (>= 0.3.8)
13
- diff-lcs (1.5.1)
14
- docile (1.4.0)
15
- io-console (0.7.2)
16
- irb (1.12.0)
17
- rdoc
14
+ diff-lcs (1.6.2)
15
+ docile (1.4.1)
16
+ erb (6.0.2)
17
+ io-console (0.8.2)
18
+ irb (1.17.0)
19
+ pp (>= 0.6.0)
20
+ prism (>= 1.3.0)
21
+ rdoc (>= 4.0.0)
18
22
  reline (>= 0.4.2)
19
- json (2.7.1)
20
- language_server-protocol (3.17.0.3)
23
+ json (2.18.1)
24
+ language_server-protocol (3.17.0.5)
21
25
  lint_roller (1.1.0)
22
- markly (0.10.0)
23
- parallel (1.24.0)
24
- parser (3.3.0.5)
26
+ markly (0.15.2)
27
+ parallel (1.27.0)
28
+ parser (3.3.10.2)
25
29
  ast (~> 2.4.1)
26
30
  racc
27
- psych (5.1.2)
31
+ pp (0.6.3)
32
+ prettyprint
33
+ prettyprint (0.2.0)
34
+ prism (1.9.0)
35
+ psych (5.3.1)
36
+ date
28
37
  stringio
29
- racc (1.7.3)
38
+ racc (1.8.1)
30
39
  rainbow (3.1.1)
31
- rake (13.1.0)
32
- rdoc (6.6.3.1)
40
+ rake (13.3.1)
41
+ rdoc (7.2.0)
42
+ erb
33
43
  psych (>= 4.0.0)
34
- regexp_parser (2.9.0)
35
- reline (0.5.0)
44
+ tsort
45
+ regexp_parser (2.11.3)
46
+ reline (0.6.3)
36
47
  io-console (~> 0.5)
37
- rexml (3.3.6)
38
- strscan
48
+ rexml (3.4.4)
39
49
  rspec (3.12.0)
40
50
  rspec-core (~> 3.12.0)
41
51
  rspec-expectations (~> 3.12.0)
@@ -49,54 +59,59 @@ GEM
49
59
  diff-lcs (>= 1.2.0, < 2.0)
50
60
  rspec-support (~> 3.12.0)
51
61
  rspec-support (3.12.2)
52
- rubocop (1.62.1)
62
+ rubocop (1.84.2)
53
63
  json (~> 2.3)
54
- language_server-protocol (>= 3.17.0)
64
+ language_server-protocol (~> 3.17.0.2)
65
+ lint_roller (~> 1.1.0)
55
66
  parallel (~> 1.10)
56
67
  parser (>= 3.3.0.2)
57
68
  rainbow (>= 2.2.2, < 4.0)
58
- regexp_parser (>= 1.8, < 3.0)
59
- rexml (>= 3.2.5, < 4.0)
60
- rubocop-ast (>= 1.31.1, < 2.0)
69
+ regexp_parser (>= 2.9.3, < 3.0)
70
+ rubocop-ast (>= 1.49.0, < 2.0)
61
71
  ruby-progressbar (~> 1.7)
62
- unicode-display_width (>= 2.4.0, < 3.0)
63
- rubocop-ast (1.31.2)
64
- parser (>= 3.3.0.4)
65
- rubocop-performance (1.20.2)
66
- rubocop (>= 1.48.1, < 2.0)
67
- rubocop-ast (>= 1.30.0, < 2.0)
72
+ unicode-display_width (>= 2.4.0, < 4.0)
73
+ rubocop-ast (1.49.0)
74
+ parser (>= 3.3.7.2)
75
+ prism (~> 1.7)
76
+ rubocop-performance (1.26.1)
77
+ lint_roller (~> 1.1)
78
+ rubocop (>= 1.75.0, < 2.0)
79
+ rubocop-ast (>= 1.47.1, < 2.0)
68
80
  ruby-progressbar (1.13.0)
69
81
  simplecov (0.22.0)
70
82
  docile (~> 1.1)
71
83
  simplecov-html (~> 0.11)
72
84
  simplecov_json_formatter (~> 0.1)
73
- simplecov-cobertura (2.1.0)
85
+ simplecov-cobertura (3.1.0)
74
86
  rexml
75
87
  simplecov (~> 0.19)
76
- simplecov-html (0.12.3)
88
+ simplecov-html (0.13.2)
77
89
  simplecov_json_formatter (0.1.4)
78
- standard (1.35.1)
90
+ standard (1.54.0)
79
91
  language_server-protocol (~> 3.17.0.2)
80
92
  lint_roller (~> 1.0)
81
- rubocop (~> 1.62.0)
93
+ rubocop (~> 1.84.0)
82
94
  standard-custom (~> 1.0.0)
83
- standard-performance (~> 1.3)
95
+ standard-performance (~> 1.8)
84
96
  standard-custom (1.0.2)
85
97
  lint_roller (~> 1.0)
86
98
  rubocop (~> 1.50)
87
- standard-performance (1.3.1)
99
+ standard-performance (1.9.0)
88
100
  lint_roller (~> 1.1)
89
- rubocop-performance (~> 1.20.2)
90
- stringio (3.1.0)
91
- strscan (3.1.0)
101
+ rubocop-performance (~> 1.26.0)
102
+ stringio (3.2.0)
92
103
  timecop (0.9.10)
93
- unicode-display_width (2.5.0)
104
+ tsort (0.2.0)
105
+ unicode-display_width (3.2.0)
106
+ unicode-emoji (~> 4.1)
107
+ unicode-emoji (4.2.0)
94
108
 
95
109
  PLATFORMS
96
110
  arm64-darwin-21
97
111
  arm64-darwin-22
98
112
  arm64-darwin-23
99
113
  arm64-darwin-24
114
+ arm64-darwin-25
100
115
  x64-mingw-ucrt
101
116
  x64-mingw32
102
117
  x86_64-darwin-19
@@ -111,7 +126,7 @@ DEPENDENCIES
111
126
  rake (~> 13.0)
112
127
  rspec (~> 3.12.0)
113
128
  simplecov (~> 0.22.0)
114
- simplecov-cobertura (~> 2.1.0)
129
+ simplecov-cobertura (~> 3.0)
115
130
  standard
116
131
  standard-performance
117
132
  timecop (~> 0.9.10)
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.6.0">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.0&color=blue&style=for-the-badge" />
22
22
  </a>
23
23
 
24
24
  <!-- x-release-please-end -->
@@ -38,9 +38,8 @@
38
38
 
39
39
  | Supported Ruby Version | OS |
40
40
  | ------------ | --------------------- |
41
- | Ruby 3.1.4 | Windows, MacOS, Linux |
42
- | Ruby 3.2.3 | Windows, MacOS, Linux |
43
- | Ruby 3.3.0 | Windows, MacOS, Linux |
41
+ | Ruby 3.4.x | Windows, MacOS, Linux |
42
+ | Ruby 4.0.x | Windows, MacOS, Linux |
44
43
 
45
44
  ### Install
46
45
 
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  ClientMetadata = Struct.new(:domain, keyword_init: true)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  class EvaluationContext
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  # Used to combine evaluation contexts from different sources
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  EvaluationDetails = Struct.new(:flag_key, :resolution_details, keyword_init: true) do
@@ -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"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenFeature
2
4
  module SDK
3
5
  module Provider
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "provider/error_code"
2
4
  require_relative "provider/reason"
3
5
  require_relative "provider/resolution_details"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: 2.1.0
116
+ version: '3.0'
117
117
  type: :development
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: 2.1.0
123
+ version: '3.0'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: timecop
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -199,14 +204,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
199
204
  requirements:
200
205
  - - ">="
201
206
  - !ruby/object:Gem::Version
202
- version: '3.1'
207
+ version: '3.4'
203
208
  required_rubygems_version: !ruby/object:Gem::Requirement
204
209
  requirements:
205
210
  - - ">="
206
211
  - !ruby/object:Gem::Version
207
212
  version: '0'
208
213
  requirements: []
209
- rubygems_version: 3.6.9
214
+ rubygems_version: 4.0.3
210
215
  specification_version: 4
211
216
  summary: OpenFeature SDK for Ruby
212
217
  test_files: []