openfeature-sdk 0.6.5 → 0.6.7
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 +25 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +4 -4
- data/Gemfile.lock +63 -91
- data/README.md +27 -11
- data/lib/open_feature/sdk/api.rb +1 -0
- data/lib/open_feature/sdk/client.rb +16 -17
- data/lib/open_feature/sdk/configuration.rb +120 -37
- data/lib/open_feature/sdk/evaluation_error.rb +28 -0
- data/lib/open_feature/sdk/hooks/hook_executor.rb +5 -5
- data/lib/open_feature/sdk/hooks/logging_hook.rb +1 -2
- data/lib/open_feature/sdk/provider/in_memory_provider.rb +24 -0
- data/lib/open_feature/sdk/provider_event.rb +5 -1
- data/lib/open_feature/sdk/provider_state_registry.rb +2 -2
- data/lib/open_feature/sdk/version.rb +1 -1
- data/renovate.json +1 -1
- data/sig/open_feature/sdk/configuration.rbs +8 -1
- data/sig/open_feature/sdk/evaluation_error.rbs +11 -0
- data/sig/open_feature/sdk/provider/in_memory_provider.rbs +11 -0
- data/sig/open_feature/sdk/provider.rbs +5 -1
- data/sig/open_feature/sdk/provider_event.rbs +2 -0
- metadata +4 -3
- data/.simplecov +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93f85fdb9eb64b634a3267d0987415f3e4b9033a9d864e80478dfde83074fa7d
|
|
4
|
+
data.tar.gz: 14e94971fdae29a56d31db9d8ee44716b805b6783e0d381ac04e6348168ccf67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1e726bae4617d91ed82f676270bb51b7355e8694fac68a76af5f8ea046ed60c33d4c31b0a716827ad61edffe2849c3eb444c614975ac8a09707dcf620286c03
|
|
7
|
+
data.tar.gz: 47d740d86cac2c493058c0f35aa6c2489d89f0502e8df3eb9ba850585777934478fe2dcf500b7eac9b71c96cfb2c857bc6026e9d1214e7876bc385788fcf6f18
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.7
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby 4.0.
|
|
1
|
+
ruby 4.0.7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.7](https://github.com/open-feature/ruby-sdk/compare/v0.6.6...v0.6.7) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* pass the bound domain to provider init and support domain-scoped providers ([#345](https://github.com/open-feature/ruby-sdk/issues/345)) ([f3cf6b7](https://github.com/open-feature/ruby-sdk/commit/f3cf6b76c06297aa4c0a2a95ea1055f816269e3b))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* return defaults instead of raising from client calls ([#346](https://github.com/open-feature/ruby-sdk/issues/346)) ([481f21c](https://github.com/open-feature/ruby-sdk/commit/481f21cfe70365de92c6ece3eb92c40117a7e1c5))
|
|
14
|
+
|
|
15
|
+
## [0.6.6](https://github.com/open-feature/ruby-sdk/compare/v0.6.5...v0.6.6) (2026-10-22)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add PROVIDER_RECONCILING and PROVIDER_CONTEXT_CHANGED events ([#338](https://github.com/open-feature/ruby-sdk/issues/338)) ([2b93aa4](https://github.com/open-feature/ruby-sdk/commit/2b93aa4daf7087a818d0b758c7978e7ad3c1c589))
|
|
21
|
+
* support variant flags in InMemoryProvider ([#340](https://github.com/open-feature/ruby-sdk/issues/340)) ([1e4b168](https://github.com/open-feature/ruby-sdk/commit/1e4b168373e39522c4884e34383730d48772b95f))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* carry provider error codes through hooks ([#339](https://github.com/open-feature/ruby-sdk/issues/339)) ([b9e586c](https://github.com/open-feature/ruby-sdk/commit/b9e586c518a4b8e85facbb90e123c347b0db33e5))
|
|
27
|
+
|
|
3
28
|
## [0.6.5](https://github.com/open-feature/ruby-sdk/compare/v0.6.4...v0.6.5) (2026-03-18)
|
|
4
29
|
|
|
5
30
|
|
data/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
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.
|
|
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.4.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
data/Gemfile
CHANGED
|
@@ -12,13 +12,13 @@ group :development, :test do
|
|
|
12
12
|
gem "rspec", "~> 3.13.0"
|
|
13
13
|
gem "standard"
|
|
14
14
|
gem "standard-performance"
|
|
15
|
-
gem "simplecov", "~>
|
|
16
|
-
gem "simplecov-cobertura", "~>
|
|
17
|
-
gem "steep", "~>
|
|
15
|
+
gem "simplecov", "~> 1.3.0"
|
|
16
|
+
gem "simplecov-cobertura", "~> 4.0"
|
|
17
|
+
gem "steep", "~> 2.0"
|
|
18
18
|
gem "timecop", "~> 0.9.10"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
group :test do
|
|
22
|
-
gem "cucumber", "~>
|
|
22
|
+
gem "cucumber", "~> 11.0"
|
|
23
23
|
gem "logger"
|
|
24
24
|
end
|
data/Gemfile.lock
CHANGED
|
@@ -1,116 +1,92 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
openfeature-sdk (0.6.
|
|
4
|
+
openfeature-sdk (0.6.7)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
|
-
activesupport (8.1.2)
|
|
10
|
-
base64
|
|
11
|
-
bigdecimal
|
|
12
|
-
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
13
|
-
connection_pool (>= 2.2.5)
|
|
14
|
-
drb
|
|
15
|
-
i18n (>= 1.6, < 2)
|
|
16
|
-
json
|
|
17
|
-
logger (>= 1.4.2)
|
|
18
|
-
minitest (>= 5.1)
|
|
19
|
-
securerandom (>= 0.3)
|
|
20
|
-
tzinfo (~> 2.0, >= 2.0.5)
|
|
21
|
-
uri (>= 0.13.1)
|
|
22
9
|
ast (2.4.3)
|
|
23
10
|
base64 (0.3.0)
|
|
24
|
-
bigdecimal (4.
|
|
11
|
+
bigdecimal (4.1.3)
|
|
25
12
|
builder (3.3.0)
|
|
26
|
-
concurrent-ruby (1.3.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
cucumber (10.2.0)
|
|
13
|
+
concurrent-ruby (1.3.8)
|
|
14
|
+
csv (3.3.6)
|
|
15
|
+
cucumber (11.1.1)
|
|
30
16
|
base64 (~> 0.2)
|
|
31
17
|
builder (~> 3.2)
|
|
32
18
|
cucumber-ci-environment (> 9, < 12)
|
|
33
|
-
cucumber-core (
|
|
19
|
+
cucumber-core (>= 16.2.0, < 17)
|
|
34
20
|
cucumber-cucumber-expressions (> 17, < 20)
|
|
35
|
-
cucumber-html-formatter (> 21, <
|
|
21
|
+
cucumber-html-formatter (> 21, < 24)
|
|
36
22
|
diff-lcs (~> 1.5)
|
|
37
23
|
logger (~> 1.6)
|
|
38
24
|
mini_mime (~> 1.1)
|
|
39
25
|
multi_test (~> 1.1)
|
|
40
|
-
sys-uname (~> 1.
|
|
26
|
+
sys-uname (~> 1.5)
|
|
41
27
|
cucumber-ci-environment (11.0.0)
|
|
42
28
|
cucumber-core (16.2.0)
|
|
43
29
|
cucumber-gherkin (> 36, < 40)
|
|
44
30
|
cucumber-messages (> 31, < 33)
|
|
45
31
|
cucumber-tag-expressions (> 6, < 9)
|
|
46
|
-
cucumber-cucumber-expressions (19.0.
|
|
32
|
+
cucumber-cucumber-expressions (19.0.1)
|
|
47
33
|
bigdecimal
|
|
48
|
-
cucumber-gherkin (39.
|
|
34
|
+
cucumber-gherkin (39.1.0)
|
|
49
35
|
cucumber-messages (>= 31, < 33)
|
|
50
|
-
cucumber-html-formatter (
|
|
36
|
+
cucumber-html-formatter (23.1.0)
|
|
51
37
|
cucumber-messages (> 23, < 33)
|
|
52
|
-
cucumber-messages (32.
|
|
38
|
+
cucumber-messages (32.3.1)
|
|
53
39
|
cucumber-tag-expressions (8.1.0)
|
|
54
|
-
date (3.5.1)
|
|
55
40
|
debug (1.11.1)
|
|
56
41
|
irb (~> 1.10)
|
|
57
42
|
reline (>= 0.3.8)
|
|
58
43
|
diff-lcs (1.6.2)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
i18n (1.14.8)
|
|
65
|
-
concurrent-ruby (~> 1.0)
|
|
66
|
-
io-console (0.8.2)
|
|
67
|
-
irb (1.17.0)
|
|
44
|
+
erb (6.0.7)
|
|
45
|
+
ffi (1.17.4)
|
|
46
|
+
fileutils (1.8.1)
|
|
47
|
+
io-console (0.9.4)
|
|
48
|
+
irb (1.18.0)
|
|
68
49
|
pp (>= 0.6.0)
|
|
69
50
|
prism (>= 1.3.0)
|
|
70
51
|
rdoc (>= 4.0.0)
|
|
71
52
|
reline (>= 0.4.2)
|
|
72
|
-
json (
|
|
73
|
-
language_server-protocol (3.17.0.
|
|
53
|
+
json (3.1.0)
|
|
54
|
+
language_server-protocol (3.17.0.6)
|
|
74
55
|
lint_roller (1.1.0)
|
|
75
|
-
listen (3.10.
|
|
56
|
+
listen (3.10.1)
|
|
76
57
|
logger
|
|
77
58
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
78
59
|
rb-inotify (~> 0.9, >= 0.9.10)
|
|
79
60
|
logger (1.7.0)
|
|
80
|
-
markly (0.
|
|
61
|
+
markly (0.19.0)
|
|
81
62
|
memoist3 (1.0.0)
|
|
82
63
|
mini_mime (1.1.5)
|
|
83
|
-
minitest (6.0.2)
|
|
84
|
-
drb (~> 2.0)
|
|
85
|
-
prism (~> 1.5)
|
|
86
64
|
multi_test (1.1.0)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
parser (3.3.10.2)
|
|
65
|
+
parallel (2.3.0)
|
|
66
|
+
parser (3.3.12.0)
|
|
90
67
|
ast (~> 2.4.1)
|
|
91
68
|
racc
|
|
92
|
-
pp (0.6.
|
|
69
|
+
pp (0.6.4)
|
|
93
70
|
prettyprint
|
|
94
|
-
prettyprint (0.2.
|
|
71
|
+
prettyprint (0.2.1)
|
|
95
72
|
prism (1.9.0)
|
|
96
|
-
psych (5.3.1)
|
|
97
|
-
date
|
|
98
|
-
stringio
|
|
99
73
|
racc (1.8.1)
|
|
100
74
|
rainbow (3.1.1)
|
|
101
|
-
rake (13.
|
|
75
|
+
rake (13.4.2)
|
|
102
76
|
rb-fsevent (0.11.2)
|
|
103
77
|
rb-inotify (0.11.1)
|
|
104
78
|
ffi (~> 1.0)
|
|
105
|
-
rbs (
|
|
79
|
+
rbs (4.2.0)
|
|
106
80
|
logger
|
|
81
|
+
prism (>= 1.6.0)
|
|
107
82
|
tsort
|
|
108
|
-
rdoc (
|
|
83
|
+
rdoc (8.1.0)
|
|
109
84
|
erb
|
|
110
|
-
|
|
85
|
+
prism (>= 1.6.0)
|
|
86
|
+
rbs (>= 4.0.0)
|
|
111
87
|
tsort
|
|
112
|
-
regexp_parser (2.
|
|
113
|
-
reline (0.
|
|
88
|
+
regexp_parser (2.13.1)
|
|
89
|
+
reline (0.7.0)
|
|
114
90
|
io-console (~> 0.5)
|
|
115
91
|
rexml (3.4.4)
|
|
116
92
|
rspec (3.13.2)
|
|
@@ -126,18 +102,18 @@ GEM
|
|
|
126
102
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
127
103
|
rspec-support (~> 3.13.0)
|
|
128
104
|
rspec-support (3.13.7)
|
|
129
|
-
rubocop (1.
|
|
130
|
-
json (
|
|
105
|
+
rubocop (1.91.0)
|
|
106
|
+
json (>= 2.3)
|
|
131
107
|
language_server-protocol (~> 3.17.0.2)
|
|
132
108
|
lint_roller (~> 1.1.0)
|
|
133
|
-
parallel (
|
|
109
|
+
parallel (>= 1.10)
|
|
134
110
|
parser (>= 3.3.0.2)
|
|
135
111
|
rainbow (>= 2.2.2, < 4.0)
|
|
136
112
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
137
113
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
138
114
|
ruby-progressbar (~> 1.7)
|
|
139
115
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
140
|
-
rubocop-ast (1.
|
|
116
|
+
rubocop-ast (1.50.0)
|
|
141
117
|
parser (>= 3.3.7.2)
|
|
142
118
|
prism (~> 1.7)
|
|
143
119
|
rubocop-performance (1.26.1)
|
|
@@ -146,19 +122,14 @@ GEM
|
|
|
146
122
|
rubocop-ast (>= 1.47.1, < 2.0)
|
|
147
123
|
ruby-progressbar (1.13.0)
|
|
148
124
|
securerandom (0.4.1)
|
|
149
|
-
simplecov (
|
|
150
|
-
|
|
151
|
-
simplecov-html (~> 0.11)
|
|
152
|
-
simplecov_json_formatter (~> 0.1)
|
|
153
|
-
simplecov-cobertura (3.1.0)
|
|
125
|
+
simplecov (1.3.2)
|
|
126
|
+
simplecov-cobertura (4.0.0)
|
|
154
127
|
rexml
|
|
155
|
-
simplecov (~> 0
|
|
156
|
-
|
|
157
|
-
simplecov_json_formatter (0.1.4)
|
|
158
|
-
standard (1.54.0)
|
|
128
|
+
simplecov (~> 1.0)
|
|
129
|
+
standard (1.57.0)
|
|
159
130
|
language_server-protocol (~> 3.17.0.2)
|
|
160
131
|
lint_roller (~> 1.0)
|
|
161
|
-
rubocop (~> 1.
|
|
132
|
+
rubocop (~> 1.91.0)
|
|
162
133
|
standard-custom (~> 1.0.0)
|
|
163
134
|
standard-performance (~> 1.8)
|
|
164
135
|
standard-custom (1.0.2)
|
|
@@ -167,8 +138,7 @@ GEM
|
|
|
167
138
|
standard-performance (1.9.0)
|
|
168
139
|
lint_roller (~> 1.1)
|
|
169
140
|
rubocop-performance (~> 1.26.0)
|
|
170
|
-
steep (1.
|
|
171
|
-
activesupport (>= 5.1)
|
|
141
|
+
steep (2.1.0)
|
|
172
142
|
concurrent-ruby (>= 1.1.10)
|
|
173
143
|
csv (>= 3.0.9)
|
|
174
144
|
fileutils (>= 1.1.0)
|
|
@@ -176,29 +146,31 @@ GEM
|
|
|
176
146
|
language_server-protocol (>= 3.17.0.4, < 4.0)
|
|
177
147
|
listen (~> 3.0)
|
|
178
148
|
logger (>= 1.3.0)
|
|
179
|
-
|
|
180
|
-
|
|
149
|
+
parser (>= 3.2)
|
|
150
|
+
prism (>= 0.25.0)
|
|
181
151
|
rainbow (>= 2.2.2, < 4.0)
|
|
182
|
-
rbs (~>
|
|
152
|
+
rbs (~> 4.2)
|
|
183
153
|
securerandom (>= 0.1)
|
|
184
154
|
strscan (>= 1.0.0)
|
|
185
155
|
terminal-table (>= 2, < 5)
|
|
186
156
|
uri (>= 0.12.0)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
157
|
+
strscan (3.1.8)
|
|
158
|
+
sys-uname (1.5.1)
|
|
159
|
+
ffi (~> 1.1)
|
|
160
|
+
memoist3 (~> 1.0.0)
|
|
161
|
+
sys-uname (1.5.1-universal-mingw32)
|
|
190
162
|
ffi (~> 1.1)
|
|
191
163
|
memoist3 (~> 1.0.0)
|
|
164
|
+
win32ole
|
|
192
165
|
terminal-table (4.0.0)
|
|
193
166
|
unicode-display_width (>= 1.1.1, < 4)
|
|
194
|
-
timecop (0.9.
|
|
167
|
+
timecop (0.9.11)
|
|
195
168
|
tsort (0.2.0)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
unicode-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
uri (1.1.1)
|
|
169
|
+
unicode-display_width (3.3.0)
|
|
170
|
+
unicode-emoji (~> 4.3)
|
|
171
|
+
unicode-emoji (4.3.0)
|
|
172
|
+
uri (1.1.2)
|
|
173
|
+
win32ole (1.9.3)
|
|
202
174
|
|
|
203
175
|
PLATFORMS
|
|
204
176
|
arm64-darwin-21
|
|
@@ -214,19 +186,19 @@ PLATFORMS
|
|
|
214
186
|
x86_64-linux
|
|
215
187
|
|
|
216
188
|
DEPENDENCIES
|
|
217
|
-
cucumber (~>
|
|
189
|
+
cucumber (~> 11.0)
|
|
218
190
|
debug
|
|
219
191
|
logger
|
|
220
192
|
markly
|
|
221
193
|
openfeature-sdk!
|
|
222
194
|
rake (~> 13.0)
|
|
223
195
|
rspec (~> 3.13.0)
|
|
224
|
-
simplecov (~>
|
|
225
|
-
simplecov-cobertura (~>
|
|
196
|
+
simplecov (~> 1.3.0)
|
|
197
|
+
simplecov-cobertura (~> 4.0)
|
|
226
198
|
standard
|
|
227
199
|
standard-performance
|
|
228
|
-
steep (~>
|
|
200
|
+
steep (~> 2.0)
|
|
229
201
|
timecop (~> 0.9.10)
|
|
230
202
|
|
|
231
203
|
BUNDLED WITH
|
|
232
|
-
|
|
204
|
+
4.0.12
|
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.6.
|
|
21
|
-
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.
|
|
20
|
+
<a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.7">
|
|
21
|
+
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.7&color=blue&style=for-the-badge" />
|
|
22
22
|
</a>
|
|
23
23
|
|
|
24
24
|
<!-- x-release-please-end -->
|
|
@@ -70,7 +70,6 @@ gem install openfeature-sdk
|
|
|
70
70
|
|
|
71
71
|
```ruby
|
|
72
72
|
require 'open_feature/sdk'
|
|
73
|
-
require 'json' # For JSON.dump
|
|
74
73
|
|
|
75
74
|
# API Initialization and configuration
|
|
76
75
|
|
|
@@ -79,7 +78,13 @@ OpenFeature::SDK.configure do |config|
|
|
|
79
78
|
config.set_provider(OpenFeature::SDK::Provider::InMemoryProvider.new(
|
|
80
79
|
{
|
|
81
80
|
"flag1" => true,
|
|
82
|
-
"flag2" => 1
|
|
81
|
+
"flag2" => 1,
|
|
82
|
+
# Variant flags report a variant and reason, and can target on the evaluation context
|
|
83
|
+
"flag3" => OpenFeature::SDK::Provider::InMemoryProvider::Flag.new(
|
|
84
|
+
variants: {"on" => true, "off" => false},
|
|
85
|
+
default_variant: "off",
|
|
86
|
+
context_evaluator: ->(ctx) { "on" if ctx&.targeting_key == "beta-user" }
|
|
87
|
+
)
|
|
83
88
|
}
|
|
84
89
|
))
|
|
85
90
|
end
|
|
@@ -91,7 +96,7 @@ client = OpenFeature::SDK.build_client
|
|
|
91
96
|
bool_value = client.fetch_boolean_value(flag_key: 'boolean_flag', default_value: false)
|
|
92
97
|
|
|
93
98
|
# a details method is also available for more information about the flag evaluation
|
|
94
|
-
# see `
|
|
99
|
+
# see `EvaluationDetails` for more info
|
|
95
100
|
bool_details = client.fetch_boolean_details(flag_key: 'boolean_flag', default_value: false)
|
|
96
101
|
|
|
97
102
|
# fetching string value feature flag
|
|
@@ -158,9 +163,6 @@ rescue OpenFeature::SDK::ProviderInitializationError => e
|
|
|
158
163
|
puts "Original error: #{e.original_error}"
|
|
159
164
|
end
|
|
160
165
|
|
|
161
|
-
# With custom timeout (default is 30 seconds)
|
|
162
|
-
OpenFeature::SDK.set_provider_and_wait(my_provider, timeout: 60)
|
|
163
|
-
|
|
164
166
|
# Domain-specific provider
|
|
165
167
|
OpenFeature::SDK.set_provider_and_wait(my_provider, domain: "feature-flags")
|
|
166
168
|
|
|
@@ -299,14 +301,15 @@ legacy_flag_client = OpenFeature::SDK.build_client(domain: "legacy_flags")
|
|
|
299
301
|
|
|
300
302
|
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.
|
|
301
303
|
Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
|
|
302
|
-
Some providers
|
|
304
|
+
Some providers emit additional events: `PROVIDER_CONFIGURATION_CHANGED`, `PROVIDER_STALE`, `PROVIDER_RECONCILING` and `PROVIDER_CONTEXT_CHANGED`.
|
|
305
|
+
Handlers receive a hash with the `provider_name` and any details the provider sent, such as `message` or `error_code`.
|
|
303
306
|
|
|
304
307
|
Please refer to the documentation of the provider you're using to see what events are supported.
|
|
305
308
|
|
|
306
309
|
```ruby
|
|
307
310
|
# Register event handlers at the API (global) level
|
|
308
311
|
ready_handler = ->(event_details) do
|
|
309
|
-
puts "Provider #{event_details[:
|
|
312
|
+
puts "Provider #{event_details[:provider_name]} is ready!"
|
|
310
313
|
end
|
|
311
314
|
|
|
312
315
|
OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, ready_handler)
|
|
@@ -349,6 +352,9 @@ This should only be called when your application is in the process of shutting d
|
|
|
349
352
|
OpenFeature::SDK.shutdown
|
|
350
353
|
```
|
|
351
354
|
|
|
355
|
+
The SDK also shuts down a replaced provider once no domain uses it.
|
|
356
|
+
Events from shut-down providers are ignored.
|
|
357
|
+
|
|
352
358
|
Individual providers can implement a `shutdown` method to perform cleanup:
|
|
353
359
|
|
|
354
360
|
```ruby
|
|
@@ -440,13 +446,21 @@ You’ll then need to write the provider by implementing the `Provider` duck.
|
|
|
440
446
|
|
|
441
447
|
```ruby
|
|
442
448
|
class MyProvider
|
|
443
|
-
|
|
449
|
+
# Both arguments are optional; declare only the ones you need.
|
|
450
|
+
# `domain` is the domain the provider was bound to, or nil for the default provider.
|
|
451
|
+
def init(evaluation_context = nil, domain: nil)
|
|
444
452
|
# Perform any initialization steps with flag management system here
|
|
445
453
|
# Return value is ignored
|
|
446
454
|
# **Note** The OpenFeature spec defines a lifecycle method called `initialize` to be called when a new provider is set.
|
|
447
455
|
# To avoid conflicting with the Ruby `initialize` method, this method should be named `init` when creating a provider.
|
|
448
456
|
end
|
|
449
457
|
|
|
458
|
+
# Optional: return true if this provider keeps state for a single domain.
|
|
459
|
+
# The SDK then raises ArgumentError if the same instance is bound to a second domain.
|
|
460
|
+
def domain_scoped?
|
|
461
|
+
false
|
|
462
|
+
end
|
|
463
|
+
|
|
450
464
|
def shutdown
|
|
451
465
|
# Perform any shutdown/reclamation steps with flag management system here
|
|
452
466
|
# Return value is ignored
|
|
@@ -454,6 +468,8 @@ class MyProvider
|
|
|
454
468
|
|
|
455
469
|
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
456
470
|
# Retrieve a boolean value from provider source
|
|
471
|
+
# To signal an error, return ResolutionDetails with an error_code, or raise
|
|
472
|
+
# OpenFeature::SDK::EvaluationError.new("message", error_code: OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
|
|
457
473
|
end
|
|
458
474
|
|
|
459
475
|
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
data/lib/open_feature/sdk/api.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "configuration"
|
|
|
7
7
|
require_relative "evaluation_context"
|
|
8
8
|
require_relative "evaluation_context_builder"
|
|
9
9
|
require_relative "evaluation_details"
|
|
10
|
+
require_relative "evaluation_error"
|
|
10
11
|
require_relative "client_metadata"
|
|
11
12
|
require_relative "hooks"
|
|
12
13
|
require_relative "client"
|
|
@@ -56,12 +56,15 @@ module OpenFeature
|
|
|
56
56
|
built_context = build_evaluation_context(evaluation_context)
|
|
57
57
|
|
|
58
58
|
@provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
|
|
59
|
+
rescue
|
|
60
|
+
nil
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
RESULT_TYPE.each do |result_type|
|
|
62
64
|
SUFFIXES.each do |suffix|
|
|
63
65
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
64
66
|
def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context: nil, hooks: [], hook_hints: nil)
|
|
67
|
+
validate_default_value_type(:#{result_type}, default_value)
|
|
65
68
|
evaluation_details = fetch_details(type: :#{result_type}, flag_key:, default_value:, evaluation_context:, invocation_hooks: hooks, hook_hints: hook_hints)
|
|
66
69
|
#{"evaluation_details.value" if suffix == :value}
|
|
67
70
|
end
|
|
@@ -72,15 +75,13 @@ module OpenFeature
|
|
|
72
75
|
private
|
|
73
76
|
|
|
74
77
|
def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
|
|
75
|
-
validate_default_value_type(type, default_value)
|
|
76
|
-
|
|
77
78
|
built_context = build_evaluation_context(evaluation_context)
|
|
78
79
|
|
|
79
80
|
# Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
|
|
80
81
|
provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
|
|
81
82
|
ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
|
|
82
83
|
|
|
83
|
-
#
|
|
84
|
+
# Short-circuit NOT_READY/FATAL providers (required by evaluation_v2.feature)
|
|
84
85
|
short_circuit_code = nil
|
|
85
86
|
if OpenFeature::SDK.configuration.provider_tracked?(@provider)
|
|
86
87
|
short_circuit_code = short_circuit_error_code(provider_status)
|
|
@@ -97,17 +98,7 @@ module OpenFeature
|
|
|
97
98
|
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
98
99
|
end
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
|
|
102
|
-
rescue => e
|
|
103
|
-
resolution = Provider::ResolutionDetails.new(
|
|
104
|
-
value: default_value,
|
|
105
|
-
error_code: Provider::ErrorCode::GENERAL,
|
|
106
|
-
reason: Provider::Reason::ERROR,
|
|
107
|
-
error_message: e.message
|
|
108
|
-
)
|
|
109
|
-
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
110
|
-
end
|
|
101
|
+
return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
|
|
111
102
|
end
|
|
112
103
|
|
|
113
104
|
hook_context = Hooks::HookContext.new(
|
|
@@ -130,7 +121,6 @@ module OpenFeature
|
|
|
130
121
|
executor = Hooks::HookExecutor.new(logger: OpenFeature::SDK.configuration.logger)
|
|
131
122
|
|
|
132
123
|
if short_circuit_code
|
|
133
|
-
# Spec 1.7.6 + 1.7.7: short-circuit must still run error hooks and finally hooks
|
|
134
124
|
resolution = Provider::ResolutionDetails.new(
|
|
135
125
|
value: default_value,
|
|
136
126
|
error_code: short_circuit_code,
|
|
@@ -144,6 +134,15 @@ module OpenFeature
|
|
|
144
134
|
evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
|
|
145
135
|
end
|
|
146
136
|
end
|
|
137
|
+
rescue StandardError, NotImplementedError => e
|
|
138
|
+
# Spec 1.4.10 and 1.4.11: report through the details, never raise or log. NotImplementedError is the abstract-method idiom.
|
|
139
|
+
resolution = Provider::ResolutionDetails.new(
|
|
140
|
+
value: default_value,
|
|
141
|
+
error_code: EvaluationError.code_for(e),
|
|
142
|
+
reason: Provider::Reason::ERROR,
|
|
143
|
+
error_message: e.message
|
|
144
|
+
)
|
|
145
|
+
EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
147
146
|
end
|
|
148
147
|
|
|
149
148
|
def evaluate_flag(type:, flag_key:, default_value:, evaluation_context:)
|
|
@@ -154,9 +153,9 @@ module OpenFeature
|
|
|
154
153
|
evaluation_context: evaluation_context
|
|
155
154
|
)
|
|
156
155
|
|
|
157
|
-
if TYPE_CLASS_MAP[type].none? { |klass| resolution_details.value.is_a?(klass) }
|
|
156
|
+
if resolution_details.error_code || TYPE_CLASS_MAP[type].none? { |klass| resolution_details.value.is_a?(klass) }
|
|
158
157
|
resolution_details.value = default_value
|
|
159
|
-
resolution_details.error_code
|
|
158
|
+
resolution_details.error_code ||= Provider::ErrorCode::TYPE_MISMATCH
|
|
160
159
|
resolution_details.reason = Provider::Reason::ERROR
|
|
161
160
|
resolution_details.variant = nil
|
|
162
161
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "timeout"
|
|
4
3
|
require_relative "api"
|
|
5
4
|
require_relative "provider_initialization_error"
|
|
5
|
+
require_relative "evaluation_error"
|
|
6
6
|
require_relative "event_dispatcher"
|
|
7
7
|
require_relative "provider_event"
|
|
8
8
|
require_relative "provider_state_registry"
|
|
@@ -20,9 +20,14 @@ module OpenFeature
|
|
|
20
20
|
attr_accessor :evaluation_context, :hooks, :transaction_context_propagator
|
|
21
21
|
attr_reader :logger
|
|
22
22
|
|
|
23
|
+
# One per registration; +done+ is closed once init finishes, so waiters can pop on it
|
|
24
|
+
InitAttempt = Struct.new(:state, :error, :done)
|
|
25
|
+
private_constant :InitAttempt
|
|
26
|
+
|
|
23
27
|
def initialize
|
|
24
28
|
@hooks = []
|
|
25
29
|
@providers = {}
|
|
30
|
+
@init_attempts = {}.compare_by_identity
|
|
26
31
|
@provider_mutex = Mutex.new
|
|
27
32
|
@logger = nil
|
|
28
33
|
@event_dispatcher = EventDispatcher.new(@logger)
|
|
@@ -89,11 +94,17 @@ module OpenFeature
|
|
|
89
94
|
end
|
|
90
95
|
|
|
91
96
|
def shutdown
|
|
92
|
-
|
|
97
|
+
# Unbind before shutting down so events that arrive mid-shutdown are dropped
|
|
98
|
+
providers_to_shutdown = @provider_mutex.synchronize do
|
|
99
|
+
bound = @providers.values.uniq(&:object_id)
|
|
100
|
+
@providers = {}
|
|
101
|
+
bound
|
|
102
|
+
end
|
|
93
103
|
|
|
94
104
|
providers_to_shutdown.each do |prov|
|
|
95
|
-
# Spec 1.7.
|
|
105
|
+
# Spec 1.7.6: Set provider state to NOT_READY before shutdown
|
|
96
106
|
@provider_state_registry.set_initial_state(prov, ProviderState::NOT_READY)
|
|
107
|
+
prov.send(:detach) if prov.is_a?(Provider::EventEmitter)
|
|
97
108
|
prov.shutdown if prov.respond_to?(:shutdown)
|
|
98
109
|
rescue => e
|
|
99
110
|
@logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
|
|
@@ -112,6 +123,9 @@ module OpenFeature
|
|
|
112
123
|
@provider_state_registry.clear
|
|
113
124
|
@provider_mutex.synchronize do
|
|
114
125
|
@providers.clear
|
|
126
|
+
# Release anyone waiting on an init that shutdown cut short
|
|
127
|
+
@init_attempts.each_value { |attempt| attempt.done.close }
|
|
128
|
+
@init_attempts.clear
|
|
115
129
|
end
|
|
116
130
|
@hooks.clear
|
|
117
131
|
@evaluation_context = nil
|
|
@@ -123,31 +137,41 @@ module OpenFeature
|
|
|
123
137
|
context_for_init = @evaluation_context
|
|
124
138
|
|
|
125
139
|
old_provider = nil
|
|
140
|
+
attempt = nil
|
|
126
141
|
needs_init = false
|
|
142
|
+
init_thread = nil
|
|
127
143
|
needs_shutdown = false
|
|
128
144
|
|
|
145
|
+
# Provider code runs before taking the lock: it may call back into the SDK
|
|
146
|
+
scoped = domain_scoped?(provider)
|
|
147
|
+
|
|
129
148
|
@provider_mutex.synchronize do
|
|
149
|
+
validate_domain_scoped_binding!(provider, domain, scoped)
|
|
150
|
+
|
|
130
151
|
old_provider = @providers[domain]
|
|
131
152
|
|
|
132
153
|
new_providers = @providers.dup
|
|
133
154
|
new_providers[domain] = provider
|
|
134
155
|
@providers = new_providers
|
|
135
156
|
|
|
136
|
-
# Spec 1.1.2.2:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
needs_init = !already_active
|
|
157
|
+
# Spec 1.1.2.2: initialize once per registration; a failed init is retried on the next set
|
|
158
|
+
attempt = @init_attempts[provider]
|
|
159
|
+
needs_init = attempt.nil? || attempt.state == :failed
|
|
140
160
|
|
|
141
161
|
if needs_init
|
|
162
|
+
attempt = @init_attempts[provider] = InitAttempt.new(:running, nil, Thread::Queue.new)
|
|
142
163
|
@provider_state_registry.set_initial_state(provider)
|
|
143
164
|
provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
|
|
144
165
|
end
|
|
145
166
|
|
|
146
167
|
# Spec 1.1.2.3: Only shutdown old provider if it's no longer bound to any domain
|
|
147
168
|
if old_provider && !old_provider.equal?(provider)
|
|
148
|
-
|
|
149
|
-
needs_shutdown
|
|
150
|
-
|
|
169
|
+
needs_shutdown = !provider_bound?(old_provider)
|
|
170
|
+
if needs_shutdown
|
|
171
|
+
@init_attempts.delete(old_provider)
|
|
172
|
+
@provider_state_registry.remove_provider(old_provider)
|
|
173
|
+
old_provider.send(:detach) if old_provider.is_a?(Provider::EventEmitter)
|
|
174
|
+
end
|
|
151
175
|
end
|
|
152
176
|
end
|
|
153
177
|
|
|
@@ -163,53 +187,74 @@ module OpenFeature
|
|
|
163
187
|
# Initialize provider outside the mutex to avoid blocking other operations
|
|
164
188
|
if needs_init
|
|
165
189
|
if wait_for_init
|
|
166
|
-
init_provider(provider, context_for_init, raise_on_error: true)
|
|
190
|
+
init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: true)
|
|
167
191
|
else
|
|
168
|
-
Thread.new do
|
|
169
|
-
init_provider(provider, context_for_init, raise_on_error: false)
|
|
192
|
+
init_thread = Thread.new do
|
|
193
|
+
init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: false)
|
|
170
194
|
end
|
|
171
195
|
end
|
|
172
196
|
elsif wait_for_init
|
|
173
|
-
#
|
|
197
|
+
# Already registered: wait for an init still in flight, then report its outcome
|
|
198
|
+
attempt.done.pop
|
|
199
|
+
unless attempt.state == :succeeded
|
|
200
|
+
raise attempt.error || ProviderInitializationError.new(
|
|
201
|
+
"Provider #{provider.class.name} initialization did not complete",
|
|
202
|
+
provider:,
|
|
203
|
+
error_code: Provider::ErrorCode::GENERAL
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
174
207
|
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
|
|
175
208
|
end
|
|
209
|
+
ensure
|
|
210
|
+
# Fail an attempt no init took over (e.g. a Timeout hit first), or it would stay running forever
|
|
211
|
+
finish_init_attempt(attempt, :failed) if needs_init && !init_thread
|
|
176
212
|
end
|
|
177
213
|
|
|
178
|
-
def init_provider(provider, context, raise_on_error: false)
|
|
179
|
-
|
|
180
|
-
init_method = provider.method(:init)
|
|
181
|
-
if init_method.parameters.empty?
|
|
182
|
-
provider.init
|
|
183
|
-
else
|
|
184
|
-
provider.init(context)
|
|
185
|
-
end
|
|
186
|
-
end
|
|
214
|
+
def init_provider(provider, context, domain:, attempt:, raise_on_error: false)
|
|
215
|
+
call_init(provider, context, domain)
|
|
187
216
|
|
|
217
|
+
# Settle the attempt before handlers run, so a handler can register this provider again
|
|
218
|
+
finish_init_attempt(attempt, :succeeded)
|
|
188
219
|
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
|
|
189
220
|
rescue => e
|
|
190
|
-
# Spec 1.7.
|
|
191
|
-
error_code =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
221
|
+
# Spec 1.7.5: PROVIDER_FATAL from init makes the provider FATAL
|
|
222
|
+
error_code = EvaluationError.code_for(e)
|
|
223
|
+
error = ProviderInitializationError.new(
|
|
224
|
+
"Provider #{provider.class.name} initialization failed: #{e.message}",
|
|
225
|
+
provider:,
|
|
226
|
+
error_code: error_code,
|
|
227
|
+
original_error: e
|
|
228
|
+
)
|
|
229
|
+
finish_init_attempt(attempt, :failed, error)
|
|
196
230
|
|
|
197
231
|
dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
|
|
198
232
|
error_code: error_code,
|
|
199
233
|
message: e.message)
|
|
200
234
|
|
|
201
|
-
if raise_on_error
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
raise error if raise_on_error
|
|
236
|
+
ensure
|
|
237
|
+
# Reached unsettled only when init raised something other than a StandardError
|
|
238
|
+
finish_init_attempt(attempt, :failed)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def finish_init_attempt(attempt, state, error = nil)
|
|
242
|
+
@provider_mutex.synchronize do
|
|
243
|
+
return unless attempt.state == :running
|
|
244
|
+
|
|
245
|
+
attempt.state = state
|
|
246
|
+
attempt.error = error
|
|
208
247
|
end
|
|
248
|
+
attempt.done.close
|
|
209
249
|
end
|
|
210
250
|
|
|
211
251
|
def dispatch_provider_event(provider, event_type, details = {})
|
|
212
|
-
|
|
252
|
+
# Under the replacement lock, so a late event can't re-track a replaced or shut-down provider
|
|
253
|
+
@provider_mutex.synchronize do
|
|
254
|
+
return unless provider_bound?(provider)
|
|
255
|
+
|
|
256
|
+
@provider_state_registry.update_state_from_event(provider, event_type, details)
|
|
257
|
+
end
|
|
213
258
|
|
|
214
259
|
provider_name = extract_provider_name(provider)
|
|
215
260
|
|
|
@@ -222,10 +267,48 @@ module OpenFeature
|
|
|
222
267
|
|
|
223
268
|
private
|
|
224
269
|
|
|
270
|
+
def provider_bound?(provider)
|
|
271
|
+
@providers.each_value.any? { |p| p.equal?(provider) }
|
|
272
|
+
end
|
|
273
|
+
|
|
225
274
|
def extract_provider_name(provider)
|
|
226
275
|
provider.respond_to?(:metadata) ? provider.metadata.name : provider.class.name
|
|
227
276
|
end
|
|
228
277
|
|
|
278
|
+
def domain_scoped?(provider)
|
|
279
|
+
provider.respond_to?(:domain_scoped?) && provider.domain_scoped?
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def validate_domain_scoped_binding!(provider, domain, scoped)
|
|
283
|
+
return unless scoped && provider_bound?(provider) && !@providers[domain].equal?(provider)
|
|
284
|
+
|
|
285
|
+
raise ArgumentError, "Cannot bind domain-scoped provider to more than one domain"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def call_init(provider, context, domain)
|
|
289
|
+
return unless provider.respond_to?(:init)
|
|
290
|
+
|
|
291
|
+
parameters = provider.method(:init).parameters
|
|
292
|
+
takes_domain = init_accepts_domain?(parameters)
|
|
293
|
+
if !takes_domain && domain_scoped?(provider)
|
|
294
|
+
@logger&.warn("Provider #{extract_provider_name(provider)} is domain-scoped but its init doesn't accept domain:")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
args = init_accepts_evaluation_context?(parameters) ? [context] : []
|
|
298
|
+
provider.init(*args, **(takes_domain ? {domain:} : {}))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Anonymous `**` (from `...`) and delegators' bare `*` can't be trusted to accept `domain:`
|
|
302
|
+
def init_accepts_domain?(parameters)
|
|
303
|
+
parameters.any? do |kind, name|
|
|
304
|
+
((kind == :key || kind == :keyreq) && name == :domain) || (kind == :keyrest && name && name != :**)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def init_accepts_evaluation_context?(parameters)
|
|
309
|
+
parameters.any? { |kind, _| kind == :req || kind == :opt || kind == :rest }
|
|
310
|
+
end
|
|
311
|
+
|
|
229
312
|
def run_handlers_for_provider(provider, event_type, event_details)
|
|
230
313
|
# Run global handlers (API-level, no domain filtering)
|
|
231
314
|
@event_dispatcher.trigger_event(event_type, event_details)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "provider/error_code"
|
|
4
|
+
|
|
5
|
+
module OpenFeature
|
|
6
|
+
module SDK
|
|
7
|
+
# Raised by a provider to signal an evaluation error with an OpenFeature
|
|
8
|
+
# error code (spec 2.2.7). Any exception responding to +error_code+ is
|
|
9
|
+
# honored the same way.
|
|
10
|
+
class EvaluationError < StandardError
|
|
11
|
+
# @return [String] the OpenFeature error code
|
|
12
|
+
attr_reader :error_code
|
|
13
|
+
|
|
14
|
+
def initialize(message = nil, error_code: Provider::ErrorCode::GENERAL)
|
|
15
|
+
super(message || error_code)
|
|
16
|
+
@error_code = error_code
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The error code carried by +exception+, or GENERAL when it has none.
|
|
20
|
+
# Rescues a failing reader so callers inside a rescue block never raise (spec 1.4.10).
|
|
21
|
+
def self.code_for(exception)
|
|
22
|
+
(exception.respond_to?(:error_code) && exception.error_code) || Provider::ErrorCode::GENERAL
|
|
23
|
+
rescue
|
|
24
|
+
Provider::ErrorCode::GENERAL
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -18,15 +18,15 @@ module OpenFeature
|
|
|
18
18
|
@logger = logger
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Runs error hooks and finally hooks for a short-circuit evaluation
|
|
22
|
-
#
|
|
21
|
+
# Runs error hooks and finally hooks for a short-circuit evaluation.
|
|
22
|
+
# Before hooks and after hooks are NOT run.
|
|
23
23
|
#
|
|
24
24
|
# @param ordered_hooks [Array] hooks in before-order
|
|
25
25
|
# @param hook_context [HookContext] the hook context
|
|
26
26
|
# @param hints [Hints] hook hints
|
|
27
27
|
# @param evaluation_details [EvaluationDetails] the short-circuit result
|
|
28
28
|
def run_short_circuit(ordered_hooks:, hook_context:, hints:, evaluation_details:)
|
|
29
|
-
error =
|
|
29
|
+
error = EvaluationError.new(evaluation_details.error_message, error_code: evaluation_details.error_code)
|
|
30
30
|
run_error_hooks(ordered_hooks, hook_context, error, hints)
|
|
31
31
|
ensure
|
|
32
32
|
run_finally_hooks(ordered_hooks, hook_context, evaluation_details, hints)
|
|
@@ -49,7 +49,7 @@ module OpenFeature
|
|
|
49
49
|
# Spec 4.3.6: If evaluation resulted in an error (e.g. FLAG_NOT_FOUND,
|
|
50
50
|
# TYPE_MISMATCH), run error hooks instead of after hooks.
|
|
51
51
|
if evaluation_details.error_code
|
|
52
|
-
error =
|
|
52
|
+
error = EvaluationError.new(evaluation_details.error_message, error_code: evaluation_details.error_code)
|
|
53
53
|
run_error_hooks(ordered_hooks, hook_context, error, hints)
|
|
54
54
|
else
|
|
55
55
|
run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
|
|
@@ -61,7 +61,7 @@ module OpenFeature
|
|
|
61
61
|
flag_key: hook_context.flag_key,
|
|
62
62
|
resolution_details: Provider::ResolutionDetails.new(
|
|
63
63
|
value: hook_context.default_value,
|
|
64
|
-
error_code:
|
|
64
|
+
error_code: EvaluationError.code_for(e),
|
|
65
65
|
reason: Provider::Reason::ERROR,
|
|
66
66
|
error_message: e.message
|
|
67
67
|
)
|
|
@@ -49,8 +49,7 @@ module OpenFeature
|
|
|
49
49
|
|
|
50
50
|
def build_error_message(hook_context, exception)
|
|
51
51
|
parts = base_parts("error", hook_context)
|
|
52
|
-
|
|
53
|
-
parts << "error_code=#{sanitize(error_code)}"
|
|
52
|
+
parts << "error_code=#{sanitize(EvaluationError.code_for(exception))}"
|
|
54
53
|
parts << "error_message=#{sanitize(exception.message)}"
|
|
55
54
|
parts << "evaluation_context=#{format_context(hook_context.evaluation_context)}" if @include_evaluation_context
|
|
56
55
|
parts.join(" ")
|
|
@@ -8,6 +8,10 @@ module OpenFeature
|
|
|
8
8
|
|
|
9
9
|
NAME = "In-memory Provider"
|
|
10
10
|
|
|
11
|
+
# A flag with named variants. +context_evaluator+ receives the evaluation
|
|
12
|
+
# context and returns a variant key; anything else falls back to +default_variant+.
|
|
13
|
+
Flag = Struct.new(:variants, :default_variant, :context_evaluator, :flag_metadata, :disabled, keyword_init: true)
|
|
14
|
+
|
|
11
15
|
attr_reader :metadata
|
|
12
16
|
|
|
13
17
|
def initialize(flags = {})
|
|
@@ -70,6 +74,8 @@ module OpenFeature
|
|
|
70
74
|
return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
|
|
71
75
|
end
|
|
72
76
|
|
|
77
|
+
return resolve_variant_flag(raw_value, default_value, evaluation_context) if raw_value.is_a?(Flag)
|
|
78
|
+
|
|
73
79
|
if raw_value.respond_to?(:call)
|
|
74
80
|
value = raw_value.call(evaluation_context)
|
|
75
81
|
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
|
|
@@ -78,6 +84,24 @@ module OpenFeature
|
|
|
78
84
|
end
|
|
79
85
|
end
|
|
80
86
|
|
|
87
|
+
def resolve_variant_flag(flag, default_value, evaluation_context)
|
|
88
|
+
flag_metadata = flag.flag_metadata
|
|
89
|
+
return ResolutionDetails.new(value: default_value, reason: Reason::DISABLED, flag_metadata:) if flag.disabled
|
|
90
|
+
|
|
91
|
+
variants = flag.variants.to_h
|
|
92
|
+
unless variants.key?(flag.default_variant)
|
|
93
|
+
return ResolutionDetails.new(value: default_value, error_code: ErrorCode::PARSE_ERROR, reason: Reason::ERROR, flag_metadata:)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
matched = flag.context_evaluator&.call(evaluation_context)
|
|
97
|
+
if variants.key?(matched)
|
|
98
|
+
return ResolutionDetails.new(value: variants[matched], variant: matched, reason: Reason::TARGETING_MATCH, flag_metadata:)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
reason = flag.context_evaluator ? Reason::DEFAULT : Reason::STATIC
|
|
102
|
+
ResolutionDetails.new(value: variants[flag.default_variant], variant: flag.default_variant, reason:, flag_metadata:)
|
|
103
|
+
end
|
|
104
|
+
|
|
81
105
|
def emit_provider_changed(flag_keys)
|
|
82
106
|
return unless configuration_attached?
|
|
83
107
|
|
|
@@ -13,12 +13,16 @@ module OpenFeature
|
|
|
13
13
|
PROVIDER_ERROR = "PROVIDER_ERROR"
|
|
14
14
|
PROVIDER_CONFIGURATION_CHANGED = "PROVIDER_CONFIGURATION_CHANGED"
|
|
15
15
|
PROVIDER_STALE = "PROVIDER_STALE"
|
|
16
|
+
PROVIDER_RECONCILING = "PROVIDER_RECONCILING"
|
|
17
|
+
PROVIDER_CONTEXT_CHANGED = "PROVIDER_CONTEXT_CHANGED"
|
|
16
18
|
|
|
17
19
|
ALL_EVENTS = [
|
|
18
20
|
PROVIDER_READY,
|
|
19
21
|
PROVIDER_ERROR,
|
|
20
22
|
PROVIDER_CONFIGURATION_CHANGED,
|
|
21
|
-
PROVIDER_STALE
|
|
23
|
+
PROVIDER_STALE,
|
|
24
|
+
PROVIDER_RECONCILING,
|
|
25
|
+
PROVIDER_CONTEXT_CHANGED
|
|
22
26
|
].freeze
|
|
23
27
|
end
|
|
24
28
|
end
|
|
@@ -91,7 +91,7 @@ module OpenFeature
|
|
|
91
91
|
|
|
92
92
|
def state_from_event(event_type, event_details = nil)
|
|
93
93
|
case event_type
|
|
94
|
-
when ProviderEvent::PROVIDER_READY
|
|
94
|
+
when ProviderEvent::PROVIDER_READY, ProviderEvent::PROVIDER_CONTEXT_CHANGED
|
|
95
95
|
ProviderState::READY
|
|
96
96
|
when ProviderEvent::PROVIDER_STALE
|
|
97
97
|
ProviderState::STALE
|
|
@@ -100,7 +100,7 @@ module OpenFeature
|
|
|
100
100
|
when ProviderEvent::PROVIDER_CONFIGURATION_CHANGED
|
|
101
101
|
nil # No state change per OpenFeature spec Requirement 5.3.5
|
|
102
102
|
else
|
|
103
|
-
nil #
|
|
103
|
+
nil # PROVIDER_RECONCILING (RECONCILING status is static-context only, spec 1.7.2.1) and unknown events
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
data/renovate.json
CHANGED
|
@@ -32,8 +32,15 @@ module OpenFeature
|
|
|
32
32
|
|
|
33
33
|
def reset: () -> void
|
|
34
34
|
def set_provider_internal: (untyped provider, domain: String?, wait_for_init: bool) -> void
|
|
35
|
-
def init_provider: (untyped provider, EvaluationContext? context, ?raise_on_error: bool) -> void
|
|
35
|
+
def init_provider: (untyped provider, EvaluationContext? context, domain: String?, attempt: untyped, ?raise_on_error: bool) -> void
|
|
36
|
+
def finish_init_attempt: (untyped attempt, Symbol state, ?ProviderInitializationError? error) -> void
|
|
36
37
|
def dispatch_provider_event: (untyped provider, String event_type, ?Hash[Symbol, untyped] details) -> void
|
|
38
|
+
def provider_bound?: (untyped provider) -> bool
|
|
39
|
+
def domain_scoped?: (untyped provider) -> bool
|
|
40
|
+
def validate_domain_scoped_binding!: (untyped provider, String? domain, bool scoped) -> void
|
|
41
|
+
def call_init: (untyped provider, EvaluationContext? context, String? domain) -> void
|
|
42
|
+
def init_accepts_domain?: (Array[[Symbol, Symbol?]] parameters) -> bool
|
|
43
|
+
def init_accepts_evaluation_context?: (Array[[Symbol, Symbol?]] parameters) -> bool
|
|
37
44
|
def extract_provider_name: (untyped provider) -> String
|
|
38
45
|
def run_handlers_for_provider: (untyped provider, String event_type, Hash[Symbol, untyped] event_details) -> void
|
|
39
46
|
def run_immediate_handler: (String event_type, _ToProc handler, Client? client) -> void
|
|
@@ -6,6 +6,16 @@ module OpenFeature
|
|
|
6
6
|
|
|
7
7
|
NAME: String
|
|
8
8
|
|
|
9
|
+
class Flag < Struct[untyped]
|
|
10
|
+
attr_accessor variants: Hash[String, untyped]?
|
|
11
|
+
attr_accessor default_variant: String?
|
|
12
|
+
attr_accessor context_evaluator: (^(EvaluationContext?) -> String?)?
|
|
13
|
+
attr_accessor flag_metadata: Hash[String, untyped]?
|
|
14
|
+
attr_accessor disabled: bool?
|
|
15
|
+
|
|
16
|
+
def initialize: (?variants: Hash[String, untyped]?, ?default_variant: String?, ?context_evaluator: (^(EvaluationContext?) -> String?)?, ?flag_metadata: Hash[String, untyped]?, ?disabled: bool?) -> void
|
|
17
|
+
end
|
|
18
|
+
|
|
9
19
|
attr_reader metadata: ProviderMetadata
|
|
10
20
|
|
|
11
21
|
def initialize: (?Hash[String, untyped] flags) -> void
|
|
@@ -28,6 +38,7 @@ module OpenFeature
|
|
|
28
38
|
attr_reader flags: Hash[String, untyped]
|
|
29
39
|
|
|
30
40
|
def fetch_value: (flag_key: String, default_value: untyped, evaluation_context: EvaluationContext?) -> ResolutionDetails
|
|
41
|
+
def resolve_variant_flag: (Flag flag, untyped default_value, EvaluationContext? evaluation_context) -> ResolutionDetails
|
|
31
42
|
def emit_provider_changed: (Array[String] flag_keys) -> void
|
|
32
43
|
end
|
|
33
44
|
end
|
|
@@ -11,11 +11,15 @@ module OpenFeature
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
interface _LifecycleProvider
|
|
14
|
-
def init: (?EvaluationContext? evaluation_context) -> void
|
|
14
|
+
def init: (?EvaluationContext? evaluation_context, ?domain: String?) -> void
|
|
15
15
|
def shutdown: () -> void
|
|
16
16
|
def metadata: () -> ProviderMetadata
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
interface _DomainScopedProvider
|
|
20
|
+
def domain_scoped?: () -> bool
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
interface _TrackableProvider
|
|
20
24
|
def track: (String tracking_event_name, ?evaluation_context: EvaluationContext?, ?tracking_event_details: TrackingEventDetails?) -> void
|
|
21
25
|
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.6.
|
|
4
|
+
version: 0.6.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OpenFeature Authors
|
|
@@ -20,7 +20,6 @@ files:
|
|
|
20
20
|
- ".release-please-manifest.json"
|
|
21
21
|
- ".rspec"
|
|
22
22
|
- ".ruby-version"
|
|
23
|
-
- ".simplecov"
|
|
24
23
|
- ".standard.yml"
|
|
25
24
|
- ".tool-versions"
|
|
26
25
|
- ".yardopts"
|
|
@@ -48,6 +47,7 @@ files:
|
|
|
48
47
|
- lib/open_feature/sdk/evaluation_context.rb
|
|
49
48
|
- lib/open_feature/sdk/evaluation_context_builder.rb
|
|
50
49
|
- lib/open_feature/sdk/evaluation_details.rb
|
|
50
|
+
- lib/open_feature/sdk/evaluation_error.rb
|
|
51
51
|
- lib/open_feature/sdk/event_dispatcher.rb
|
|
52
52
|
- lib/open_feature/sdk/hooks.rb
|
|
53
53
|
- lib/open_feature/sdk/hooks/hints.rb
|
|
@@ -85,6 +85,7 @@ files:
|
|
|
85
85
|
- sig/open_feature/sdk/evaluation_context.rbs
|
|
86
86
|
- sig/open_feature/sdk/evaluation_context_builder.rbs
|
|
87
87
|
- sig/open_feature/sdk/evaluation_details.rbs
|
|
88
|
+
- sig/open_feature/sdk/evaluation_error.rbs
|
|
88
89
|
- sig/open_feature/sdk/event_dispatcher.rbs
|
|
89
90
|
- sig/open_feature/sdk/hooks/hints.rbs
|
|
90
91
|
- sig/open_feature/sdk/hooks/hook.rbs
|
|
@@ -132,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
132
133
|
- !ruby/object:Gem::Version
|
|
133
134
|
version: '0'
|
|
134
135
|
requirements: []
|
|
135
|
-
rubygems_version: 4.0.
|
|
136
|
+
rubygems_version: 4.0.20
|
|
136
137
|
specification_version: 4
|
|
137
138
|
summary: OpenFeature SDK for Ruby
|
|
138
139
|
test_files: []
|
data/.simplecov
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "simplecov-cobertura"
|
|
4
|
-
|
|
5
|
-
SimpleCov.start do
|
|
6
|
-
add_filter "/spec/"
|
|
7
|
-
minimum_coverage 90
|
|
8
|
-
if ENV["CI"] == "true"
|
|
9
|
-
formatter SimpleCov::Formatter::CoberturaFormatter
|
|
10
|
-
else
|
|
11
|
-
formatter SimpleCov::Formatter::HTMLFormatter
|
|
12
|
-
end
|
|
13
|
-
end
|