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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/.ruby-version +1 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +51 -0
- data/Gemfile.lock +58 -43
- data/README.md +4 -5
- data/lib/open_feature/sdk/api.rb +1 -0
- data/lib/open_feature/sdk/client.rb +49 -9
- data/lib/open_feature/sdk/client_metadata.rb +2 -0
- data/lib/open_feature/sdk/evaluation_context.rb +2 -0
- data/lib/open_feature/sdk/evaluation_context_builder.rb +2 -0
- data/lib/open_feature/sdk/evaluation_details.rb +2 -0
- data/lib/open_feature/sdk/hooks/hints.rb +2 -0
- data/lib/open_feature/sdk/hooks/hook.rb +34 -0
- data/lib/open_feature/sdk/hooks/hook_context.rb +29 -0
- data/lib/open_feature/sdk/hooks/hook_executor.rb +102 -0
- data/lib/open_feature/sdk/hooks.rb +6 -0
- data/lib/open_feature/sdk/provider/error_code.rb +2 -0
- data/lib/open_feature/sdk/provider/in_memory_provider.rb +2 -0
- data/lib/open_feature/sdk/provider/provider_metadata.rb +2 -0
- data/lib/open_feature/sdk/provider/reason.rb +2 -0
- data/lib/open_feature/sdk/provider/resolution_details.rb +2 -0
- data/lib/open_feature/sdk/provider.rb +2 -0
- data/lib/open_feature/sdk/version.rb +1 -1
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af657f35506e7f76184269c57877bbe87cb75f03a2e697e228e9b48f0b2bb17c
|
|
4
|
+
data.tar.gz: 57944d4c3a74838e73a1f55b4f06e9b82ec4b5186c76ad000577504b30d72d88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8dc359e5bb318d79e2fcb48bc0c393c055578cf0a331b02bc61f82910b3da973eaf487457db80d9cd73f12303b15a77cbef9062481933c3d7b4c305de0ed1cc2
|
|
7
|
+
data.tar.gz: c733ce2f12dbd64ad9fd4e42a5c6cd17c805aa0ab746408c222aee42e6af5ba385ff92baf6cffbf79e4e949dbd7e6b5b2c67937694717ef5b41396353b9f9391
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4.0.1
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby
|
|
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.
|
|
4
|
+
openfeature-sdk (0.6.0)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
|
-
ast (2.4.
|
|
10
|
-
|
|
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.
|
|
14
|
-
docile (1.4.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
20
|
-
language_server-protocol (3.17.0.
|
|
23
|
+
json (2.18.1)
|
|
24
|
+
language_server-protocol (3.17.0.5)
|
|
21
25
|
lint_roller (1.1.0)
|
|
22
|
-
markly (0.
|
|
23
|
-
parallel (1.
|
|
24
|
-
parser (3.3.
|
|
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
|
-
|
|
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.
|
|
38
|
+
racc (1.8.1)
|
|
30
39
|
rainbow (3.1.1)
|
|
31
|
-
rake (13.1
|
|
32
|
-
rdoc (
|
|
40
|
+
rake (13.3.1)
|
|
41
|
+
rdoc (7.2.0)
|
|
42
|
+
erb
|
|
33
43
|
psych (>= 4.0.0)
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
tsort
|
|
45
|
+
regexp_parser (2.11.3)
|
|
46
|
+
reline (0.6.3)
|
|
36
47
|
io-console (~> 0.5)
|
|
37
|
-
rexml (3.
|
|
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
|
+
rubocop (1.84.2)
|
|
53
63
|
json (~> 2.3)
|
|
54
|
-
language_server-protocol (
|
|
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 (>=
|
|
59
|
-
|
|
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, <
|
|
63
|
-
rubocop-ast (1.
|
|
64
|
-
parser (>= 3.3.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 (
|
|
85
|
+
simplecov-cobertura (3.1.0)
|
|
74
86
|
rexml
|
|
75
87
|
simplecov (~> 0.19)
|
|
76
|
-
simplecov-html (0.
|
|
88
|
+
simplecov-html (0.13.2)
|
|
77
89
|
simplecov_json_formatter (0.1.4)
|
|
78
|
-
standard (1.
|
|
90
|
+
standard (1.54.0)
|
|
79
91
|
language_server-protocol (~> 3.17.0.2)
|
|
80
92
|
lint_roller (~> 1.0)
|
|
81
|
-
rubocop (~> 1.
|
|
93
|
+
rubocop (~> 1.84.0)
|
|
82
94
|
standard-custom (~> 1.0.0)
|
|
83
|
-
standard-performance (~> 1.
|
|
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.
|
|
99
|
+
standard-performance (1.9.0)
|
|
88
100
|
lint_roller (~> 1.1)
|
|
89
|
-
rubocop-performance (~> 1.
|
|
90
|
-
stringio (3.
|
|
91
|
-
strscan (3.1.0)
|
|
101
|
+
rubocop-performance (~> 1.26.0)
|
|
102
|
+
stringio (3.2.0)
|
|
92
103
|
timecop (0.9.10)
|
|
93
|
-
|
|
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 (~>
|
|
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.
|
|
21
|
-
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.
|
|
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.
|
|
42
|
-
| Ruby
|
|
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
|
|
data/lib/open_feature/sdk/api.rb
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -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
|
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
|
+
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:
|
|
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:
|
|
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.
|
|
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:
|
|
214
|
+
rubygems_version: 4.0.3
|
|
210
215
|
specification_version: 4
|
|
211
216
|
summary: OpenFeature SDK for Ruby
|
|
212
217
|
test_files: []
|