openfeature-sdk-sorbet 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -1
- data/CHANGELOG.md +26 -0
- data/Gemfile.lock +9 -9
- data/README.md +24 -1
- data/lib/open_feature/client.rb +63 -13
- data/lib/open_feature/configuration.rb +9 -13
- data/lib/open_feature/evaluation_context.rb +40 -0
- data/lib/open_feature/evaluation_context_builder.rb +26 -0
- data/lib/open_feature/multiple_source_provider.rb +121 -0
- data/lib/open_feature.rb +21 -5
- metadata +9 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0df7fa6e69c0e30dd01d5e5f0aad7b5543787ed2ab7a11ab35d7b034ffe1dc6
|
4
|
+
data.tar.gz: 22de53663fcd170d801f9ed12e0fb7bef01618b685ca61f2dbe486801d5fdcc1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b31be23ed317d0e8d1418085f2e4adb655030d023a08cf1eb3ef450fa5aaf091319bdd6cd4af57d28d795e48711c705499aed23181c9f894919c01af8a7e8495
|
7
|
+
data.tar.gz: b3bdf0316cd4637a7970791f2d2b5d4d3407bb45cd14b613fd268ce5f4e3486d6cdbe5731c955b5c92f4b9f26196d1a514dc8f54f683f8ec776f345f102f7173
|
data/.rubocop.yml
CHANGED
@@ -19,16 +19,18 @@ Layout/LineLength:
|
|
19
19
|
|
20
20
|
Lint/UnusedMethodArgument:
|
21
21
|
Exclude:
|
22
|
+
- lib/open_feature/multiple_source_provider.rb
|
22
23
|
- lib/open_feature/no_op_provider.rb
|
23
24
|
|
24
25
|
Metrics/ClassLength:
|
25
26
|
Exclude:
|
26
27
|
- lib/open_feature/client.rb
|
27
28
|
- test/open_feature/client_test.rb
|
29
|
+
- test/open_feature/multiple_source_provider_test.rb
|
28
30
|
|
29
31
|
Metrics/MethodLength:
|
30
32
|
Exclude:
|
31
|
-
- test
|
33
|
+
- test/**/*
|
32
34
|
|
33
35
|
Style/AccessorGrouping:
|
34
36
|
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.2.0] - 2023-05-17
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- Added ability to set evaluation context globally on the `Configuration` singleton, i.e. `OpenFeature::Configuration.instance.evaluation_context = OpenFeature::EvaluationContext.new(fields: { "globally" => "available" })`.
|
14
|
+
- Added ability to set evaluation context globally on the `OpenFeature` module, i.e. `OpenFeature.set_evaluation_context(OpenFeature::EvaluationContext.new(fields: { "globally" => "available" }))`.
|
15
|
+
- Added ability to set evaluation context on a `Client` instance, i.e. `client.evaluation_context = OpenFeature::EvaluationContext.new(fields: { "client" => "available" })`.
|
16
|
+
- Added ability to set hooks and evaluation context on `Client` initialization, i.e. `OpenFeature.create_client(name: "my_client", evaluation_context: OpenFeature::EvaluationContext.new(fields: { "client" => "available" }), hooks: OpenFeature::Hook.new)`
|
17
|
+
- Added `Configuration#reset!` to reset global configuration to the default state.
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
|
21
|
+
- Renamed `Configuration#set_provider` to `Configuration#provider=`.
|
22
|
+
- Renamed `Configuration#add_provider` to `Configuration#hooks=`.
|
23
|
+
- During flag initialization, contexts are now merged. The invocation context takes precedence over the client context which takes precedence over the global context.
|
24
|
+
|
25
|
+
### Removed
|
26
|
+
|
27
|
+
- Removed `Configuration#clear_hooks!` in favor of `Configuration#reset!`.
|
28
|
+
|
29
|
+
## [0.1.2] - 2023-05-16
|
30
|
+
|
31
|
+
### Added
|
32
|
+
|
33
|
+
- Introduced `OpenFeature::MultipleSourceProvider` to allow fetching flags from multiple sources.
|
34
|
+
|
9
35
|
## [0.1.1] - 2023-05-15
|
10
36
|
|
11
37
|
### Changed
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openfeature-sdk-sorbet (0.
|
4
|
+
openfeature-sdk-sorbet (0.2.0)
|
5
5
|
sorbet-runtime (~> 0.5)
|
6
6
|
sorbet-struct-comparable (~> 1.3)
|
7
7
|
zeitwerk (~> 2.6)
|
@@ -56,14 +56,14 @@ GEM
|
|
56
56
|
rubocop-sorbet (0.7.0)
|
57
57
|
rubocop (>= 0.90.0)
|
58
58
|
ruby-progressbar (1.13.0)
|
59
|
-
sorbet (0.5.
|
60
|
-
sorbet-static (= 0.5.
|
61
|
-
sorbet-runtime (0.5.
|
62
|
-
sorbet-static (0.5.
|
63
|
-
sorbet-static (0.5.
|
64
|
-
sorbet-static-and-runtime (0.5.
|
65
|
-
sorbet (= 0.5.
|
66
|
-
sorbet-runtime (= 0.5.
|
59
|
+
sorbet (0.5.10830)
|
60
|
+
sorbet-static (= 0.5.10830)
|
61
|
+
sorbet-runtime (0.5.10830)
|
62
|
+
sorbet-static (0.5.10830-universal-darwin-22)
|
63
|
+
sorbet-static (0.5.10830-x86_64-linux)
|
64
|
+
sorbet-static-and-runtime (0.5.10830)
|
65
|
+
sorbet (= 0.5.10830)
|
66
|
+
sorbet-runtime (= 0.5.10830)
|
67
67
|
sorbet-struct-comparable (1.3.0)
|
68
68
|
sorbet-runtime (>= 0.5)
|
69
69
|
spoom (1.2.1)
|
data/README.md
CHANGED
@@ -26,9 +26,10 @@ require "open_feature"
|
|
26
26
|
# Configure global API properties
|
27
27
|
|
28
28
|
OpenFeature.set_provider(OpenFeature::NoOpProvider.new)
|
29
|
+
OpenFeature.set_evaluation_context(OpenFeature::EvaluationContext.new(fields: { "globally" => "available" }))
|
29
30
|
OpenFeature.add_hooks([OpenFeature::Hook.new]) # experimental, not fully supported
|
30
31
|
|
31
|
-
client = OpenFeature.create_client
|
32
|
+
client = OpenFeature.create_client(evaluation_context: OpenFeature::EvaluationContext.new(fields: { "client" => "available" }))
|
32
33
|
|
33
34
|
# Fetch boolean value
|
34
35
|
# Also methods available for String, Number, Integer, Float and Structure (Hash)
|
@@ -37,6 +38,9 @@ bool_value = client.fetch_boolean_value(flag_key: "my_toggle", default_value: fa
|
|
37
38
|
# Sorbet sprinkles in type safety
|
38
39
|
bool_value = client.fetch_boolean_value(flag_key: "my_toggle", default_value: "bad!") # => raises TypeError from Sorbet, invalid default value
|
39
40
|
|
41
|
+
# Additional evaluation context can be provided during invocation
|
42
|
+
number_value = client.fetch_number_value(flag_key: "my_toggle", default_value: 1, context: OpenFeature::EvaluationContext.new(fields: { "only_this_call_site" => 10 })) # => merges client and global context
|
43
|
+
|
40
44
|
# Fetch structure evaluation details
|
41
45
|
structure_evaluation_details = client.fetch_structure_details(flag_key: "my_structure", default_value: { "a" => "fallback" }) # => EvaluationDetails(value: Hash, flag_key: "my_structure", ...)
|
42
46
|
```
|
@@ -45,10 +49,29 @@ structure_evaluation_details = client.fetch_structure_details(flag_key: "my_stru
|
|
45
49
|
|
46
50
|
The OpenFeature specification defines [Structure as a potential return type](https://openfeature.dev/specification/types#structure). This is somewhat ambiguous in Ruby, further complicated by `T::Struct` that we get from Sorbet. For now, the type I've elected here is `T.any(T::Array[T.untyped], T::Hash[T.untyped, T.untyped]` (loosely, either an Array of untyped members or a Hash with untyped keys and untyped values) for flexibility but with a little more structure than a YML or JSON parsable string. This decision might change in the future upon further interpretation or new versions of the specification.
|
47
51
|
|
52
|
+
### Evaluation Context
|
53
|
+
|
54
|
+
We support global evaluation context (set on the `OpenFeature` module), client evaluation context (set on client instances or during client initialization) and invocation evaluation context (passed in during flag evaluation). In compliance with the specification, the invocation context merges into the client context which merges into the global context. Fields in invocation context take precedence over fields in the client context which take precedence over fields in the global context.
|
55
|
+
|
48
56
|
### Provider Interface
|
49
57
|
|
50
58
|
By default, this implementation sets the provider to the `OpenFeature::NoOpProvider` which always returns the default value. It's up to the individual teams to define their own providers based on their flag source (in the future, I'll release open-source providers based on various, common vendors).
|
51
59
|
|
60
|
+
This gem also provides `OpenFeature::MultipleSourceProvider` to allow fetching flags from multiple sources. This is especially useful if your existing application has flags spread across bespoke and vendor solutions and you want to unify the evaluation sites. It can be instantiated and configured like so:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
provider = OpenFeature::MultipleSourceProvider.new(
|
64
|
+
providers: [
|
65
|
+
CustomProvider.new,
|
66
|
+
OpenFeature::NoOpProvider.new
|
67
|
+
]
|
68
|
+
)
|
69
|
+
|
70
|
+
OpenFeature.set_provider(provider)
|
71
|
+
```
|
72
|
+
|
73
|
+
#### Implementing Custom Providers
|
74
|
+
|
52
75
|
Thanks to Sorbet interfaces, it's fairly straightforward to implement a new provider. Here is an example for a JSON-based flag format on disk:
|
53
76
|
|
54
77
|
```ruby
|
data/lib/open_feature/client.rb
CHANGED
@@ -9,14 +9,25 @@ module OpenFeature
|
|
9
9
|
sig { returns(ClientMetadata) }
|
10
10
|
attr_reader :client_metadata
|
11
11
|
|
12
|
+
sig { returns(T.nilable(EvaluationContext)) }
|
13
|
+
attr_accessor :evaluation_context
|
14
|
+
|
12
15
|
sig { returns(T::Array[Hook]) }
|
13
16
|
attr_reader :hooks
|
14
17
|
|
15
|
-
sig
|
16
|
-
|
18
|
+
sig do
|
19
|
+
params(
|
20
|
+
provider: Provider,
|
21
|
+
name: T.nilable(String),
|
22
|
+
evaluation_context: T.nilable(EvaluationContext),
|
23
|
+
hooks: T::Array[Hook]
|
24
|
+
).void
|
25
|
+
end
|
26
|
+
def initialize(provider:, name: nil, evaluation_context: nil, hooks: [])
|
17
27
|
@provider = provider
|
18
28
|
@client_metadata = T.let(ClientMetadata.new(name: name), ClientMetadata)
|
19
|
-
@
|
29
|
+
@evaluation_context = evaluation_context
|
30
|
+
@hooks = hooks
|
20
31
|
end
|
21
32
|
|
22
33
|
sig { params(hooks: T.any(Hook, T::Array[Hook])).void }
|
@@ -33,7 +44,9 @@ module OpenFeature
|
|
33
44
|
).returns(T::Boolean)
|
34
45
|
end
|
35
46
|
def fetch_boolean_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
36
|
-
provider
|
47
|
+
provider
|
48
|
+
.resolve_boolean_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
|
49
|
+
.value
|
37
50
|
rescue StandardError
|
38
51
|
default_value
|
39
52
|
end
|
@@ -47,7 +60,11 @@ module OpenFeature
|
|
47
60
|
).returns(EvaluationDetails[T::Boolean])
|
48
61
|
end
|
49
62
|
def fetch_boolean_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
50
|
-
details = provider.resolve_boolean_value(
|
63
|
+
details = provider.resolve_boolean_value(
|
64
|
+
flag_key: flag_key,
|
65
|
+
default_value: default_value,
|
66
|
+
context: build_context(context)
|
67
|
+
)
|
51
68
|
|
52
69
|
EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
|
53
70
|
rescue StandardError => e
|
@@ -63,7 +80,9 @@ module OpenFeature
|
|
63
80
|
).returns(String)
|
64
81
|
end
|
65
82
|
def fetch_string_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
66
|
-
provider
|
83
|
+
provider
|
84
|
+
.resolve_string_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
|
85
|
+
.value
|
67
86
|
rescue StandardError
|
68
87
|
default_value
|
69
88
|
end
|
@@ -77,7 +96,11 @@ module OpenFeature
|
|
77
96
|
).returns(EvaluationDetails[String])
|
78
97
|
end
|
79
98
|
def fetch_string_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
80
|
-
details = provider.resolve_string_value(
|
99
|
+
details = provider.resolve_string_value(
|
100
|
+
flag_key: flag_key,
|
101
|
+
default_value: default_value,
|
102
|
+
context: build_context(context)
|
103
|
+
)
|
81
104
|
|
82
105
|
EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
|
83
106
|
rescue StandardError => e
|
@@ -93,7 +116,9 @@ module OpenFeature
|
|
93
116
|
).returns(Numeric)
|
94
117
|
end
|
95
118
|
def fetch_number_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
96
|
-
provider
|
119
|
+
provider
|
120
|
+
.resolve_number_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
|
121
|
+
.value
|
97
122
|
rescue StandardError
|
98
123
|
default_value
|
99
124
|
end
|
@@ -107,7 +132,11 @@ module OpenFeature
|
|
107
132
|
).returns(EvaluationDetails[Numeric])
|
108
133
|
end
|
109
134
|
def fetch_number_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
110
|
-
details = provider.resolve_number_value(
|
135
|
+
details = provider.resolve_number_value(
|
136
|
+
flag_key: flag_key,
|
137
|
+
default_value: default_value,
|
138
|
+
context: build_context(context)
|
139
|
+
)
|
111
140
|
|
112
141
|
EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
|
113
142
|
rescue StandardError => e
|
@@ -123,7 +152,10 @@ module OpenFeature
|
|
123
152
|
).returns(Integer)
|
124
153
|
end
|
125
154
|
def fetch_integer_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
126
|
-
provider
|
155
|
+
provider
|
156
|
+
.resolve_number_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
|
157
|
+
.value
|
158
|
+
.to_i
|
127
159
|
rescue StandardError
|
128
160
|
default_value
|
129
161
|
end
|
@@ -137,7 +169,10 @@ module OpenFeature
|
|
137
169
|
).returns(Float)
|
138
170
|
end
|
139
171
|
def fetch_float_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
140
|
-
provider
|
172
|
+
provider
|
173
|
+
.resolve_number_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
|
174
|
+
.value
|
175
|
+
.to_f
|
141
176
|
rescue StandardError
|
142
177
|
default_value
|
143
178
|
end
|
@@ -151,7 +186,9 @@ module OpenFeature
|
|
151
186
|
).returns(Structure)
|
152
187
|
end
|
153
188
|
def fetch_structure_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
154
|
-
provider
|
189
|
+
provider
|
190
|
+
.resolve_structure_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
|
191
|
+
.value
|
155
192
|
rescue StandardError
|
156
193
|
default_value
|
157
194
|
end
|
@@ -165,7 +202,11 @@ module OpenFeature
|
|
165
202
|
).returns(EvaluationDetails[Structure])
|
166
203
|
end
|
167
204
|
def fetch_structure_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
|
168
|
-
details = provider.resolve_structure_value(
|
205
|
+
details = provider.resolve_structure_value(
|
206
|
+
flag_key: flag_key,
|
207
|
+
default_value: default_value,
|
208
|
+
context: build_context(context)
|
209
|
+
)
|
169
210
|
|
170
211
|
EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
|
171
212
|
rescue StandardError => e
|
@@ -176,5 +217,14 @@ module OpenFeature
|
|
176
217
|
|
177
218
|
sig { returns(Provider) }
|
178
219
|
attr_reader :provider
|
220
|
+
|
221
|
+
sig { params(invocation_context: T.nilable(EvaluationContext)).returns(T.nilable(EvaluationContext)) }
|
222
|
+
def build_context(invocation_context)
|
223
|
+
EvaluationContextBuilder.new.call(
|
224
|
+
global_context: OpenFeature.configuration.evaluation_context,
|
225
|
+
client_context: evaluation_context,
|
226
|
+
invocation_context: invocation_context
|
227
|
+
)
|
228
|
+
end
|
179
229
|
end
|
180
230
|
end
|
@@ -13,14 +13,18 @@ module OpenFeature
|
|
13
13
|
include Singleton
|
14
14
|
|
15
15
|
sig { returns(Provider) }
|
16
|
-
|
16
|
+
attr_accessor :provider
|
17
|
+
|
18
|
+
sig { returns(T.nilable(EvaluationContext)) }
|
19
|
+
attr_accessor :evaluation_context
|
17
20
|
|
18
21
|
sig { returns(T::Array[Hook]) }
|
19
|
-
|
22
|
+
attr_accessor :hooks
|
20
23
|
|
21
24
|
sig { void }
|
22
25
|
def initialize
|
23
26
|
@provider = T.let(NoOpProvider.new, Provider)
|
27
|
+
@evaluation_context = T.let(nil, T.nilable(EvaluationContext))
|
24
28
|
@hooks = T.let([], T::Array[Hook])
|
25
29
|
end
|
26
30
|
|
@@ -29,19 +33,11 @@ module OpenFeature
|
|
29
33
|
provider.metadata
|
30
34
|
end
|
31
35
|
|
32
|
-
sig { params(provider: Provider).void }
|
33
|
-
def set_provider(provider) # rubocop:disable Naming/AccessorMethodName
|
34
|
-
@provider = provider
|
35
|
-
end
|
36
|
-
|
37
|
-
sig { params(hooks: T.any(Hook, T::Array[Hook])).void }
|
38
|
-
def add_hooks(hooks)
|
39
|
-
@hooks.concat(Array(hooks))
|
40
|
-
end
|
41
|
-
|
42
36
|
sig { void }
|
43
|
-
def
|
37
|
+
def reset!
|
38
|
+
@provider = OpenFeature::NoOpProvider.new
|
44
39
|
@hooks = []
|
40
|
+
@evaluation_context = nil
|
45
41
|
end
|
46
42
|
end
|
47
43
|
end
|
@@ -1,10 +1,50 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "date"
|
5
|
+
|
4
6
|
module OpenFeature
|
5
7
|
# Provides ambient information for the purposes of flag evaluation.
|
6
8
|
# Currently does not meet specification!
|
7
9
|
class EvaluationContext < T::Struct
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
include T::Struct::ActsAsComparable
|
13
|
+
|
14
|
+
FieldValueType = T.type_alias { T.any(T::Boolean, String, Numeric, DateTime, Structure) }
|
15
|
+
|
8
16
|
const :targeting_key, T.nilable(String)
|
17
|
+
const :fields, T::Hash[String, FieldValueType], default: {}
|
18
|
+
|
19
|
+
sig { params(method_name: Symbol).returns(T::Boolean) }
|
20
|
+
def respond_to_missing?(method_name)
|
21
|
+
fields.key?(method_name.to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
sig { params(method_name: Symbol).returns(T.nilable(FieldValueType)) }
|
25
|
+
def method_missing(method_name)
|
26
|
+
fields.fetch(method_name.to_s, nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { params(key: String, value: FieldValueType).returns(EvaluationContext) }
|
30
|
+
def add_field(key, value)
|
31
|
+
EvaluationContext.new(
|
32
|
+
targeting_key: targeting_key,
|
33
|
+
fields: fields.merge({ key => value })
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { returns(T::Hash[String, FieldValueType]) }
|
38
|
+
def to_h
|
39
|
+
targeting_key.nil? ? fields : fields.merge("targeting_key" => targeting_key)
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(overriding_context: EvaluationContext).returns(EvaluationContext) }
|
43
|
+
def merge(overriding_context)
|
44
|
+
EvaluationContext.new(
|
45
|
+
targeting_key: overriding_context.targeting_key || targeting_key,
|
46
|
+
fields: fields.merge(overriding_context.fields)
|
47
|
+
)
|
48
|
+
end
|
9
49
|
end
|
10
50
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module OpenFeature
|
5
|
+
# Used to combine evaluation contexts from different sources
|
6
|
+
class EvaluationContextBuilder
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig do
|
10
|
+
params(
|
11
|
+
global_context: T.nilable(EvaluationContext),
|
12
|
+
client_context: T.nilable(EvaluationContext),
|
13
|
+
invocation_context: T.nilable(EvaluationContext)
|
14
|
+
).returns(T.nilable(EvaluationContext))
|
15
|
+
end
|
16
|
+
def call(global_context:, client_context:, invocation_context:)
|
17
|
+
available_contexts = [global_context, client_context, invocation_context].compact
|
18
|
+
|
19
|
+
return nil if available_contexts.empty?
|
20
|
+
|
21
|
+
available_contexts.reduce(EvaluationContext.new) do |built_context, context|
|
22
|
+
built_context.merge(context)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module OpenFeature
|
5
|
+
# Used to pull from multiple providers.
|
6
|
+
# Order of the providers given to initialize matters.
|
7
|
+
# The providers will be evaluated in that order and the first
|
8
|
+
# non-error result will be used. If all sources return an error
|
9
|
+
# then the default value is used.
|
10
|
+
class MultipleSourceProvider
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
include Provider
|
14
|
+
|
15
|
+
sig { params(providers: T::Array[Provider]).void }
|
16
|
+
def initialize(providers:)
|
17
|
+
@providers = providers
|
18
|
+
end
|
19
|
+
|
20
|
+
sig { override.returns(ProviderMetadata) }
|
21
|
+
def metadata
|
22
|
+
ProviderMetadata.new(name: "Multiple Sources: #{providers.map(&:metadata).map(&:name).join(", ")}")
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { override.returns(T::Array[Hook]) }
|
26
|
+
def hooks
|
27
|
+
providers.flat_map(&:hooks)
|
28
|
+
end
|
29
|
+
|
30
|
+
sig do
|
31
|
+
override
|
32
|
+
.params(
|
33
|
+
flag_key: String,
|
34
|
+
default_value: T::Boolean,
|
35
|
+
context: T.nilable(EvaluationContext)
|
36
|
+
)
|
37
|
+
.returns(ResolutionDetails[T::Boolean])
|
38
|
+
end
|
39
|
+
def resolve_boolean_value(flag_key:, default_value:, context: nil)
|
40
|
+
resolve_from_sources(default_value: default_value) do |provider|
|
41
|
+
provider.resolve_boolean_value(flag_key: flag_key, default_value: default_value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
sig do
|
46
|
+
override
|
47
|
+
.params(
|
48
|
+
flag_key: String,
|
49
|
+
default_value: Numeric,
|
50
|
+
context: T.nilable(EvaluationContext)
|
51
|
+
)
|
52
|
+
.returns(ResolutionDetails[Numeric])
|
53
|
+
end
|
54
|
+
def resolve_number_value(flag_key:, default_value:, context: nil)
|
55
|
+
resolve_from_sources(default_value: default_value) do |provider|
|
56
|
+
provider.resolve_number_value(flag_key: flag_key, default_value: default_value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
sig do
|
61
|
+
override
|
62
|
+
.params(
|
63
|
+
flag_key: String,
|
64
|
+
default_value: Structure,
|
65
|
+
context: T.nilable(EvaluationContext)
|
66
|
+
)
|
67
|
+
.returns(ResolutionDetails[Structure])
|
68
|
+
end
|
69
|
+
def resolve_structure_value(flag_key:, default_value:, context: nil)
|
70
|
+
resolve_from_sources(default_value: default_value) do |provider|
|
71
|
+
provider.resolve_structure_value(flag_key: flag_key, default_value: default_value)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
sig do
|
76
|
+
override
|
77
|
+
.params(
|
78
|
+
flag_key: String,
|
79
|
+
default_value: String,
|
80
|
+
context: T.nilable(EvaluationContext)
|
81
|
+
)
|
82
|
+
.returns(ResolutionDetails[String])
|
83
|
+
end
|
84
|
+
def resolve_string_value(flag_key:, default_value:, context: nil)
|
85
|
+
resolve_from_sources(default_value: default_value) do |provider|
|
86
|
+
provider.resolve_string_value(flag_key: flag_key, default_value: default_value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
sig { returns(T::Array[Provider]) }
|
93
|
+
attr_reader :providers
|
94
|
+
|
95
|
+
# rubocop:disable Metrics/MethodLength
|
96
|
+
sig do
|
97
|
+
type_parameters(:U)
|
98
|
+
.params(
|
99
|
+
default_value: T.type_parameter(:U),
|
100
|
+
blk: T.proc.params(arg0: Provider).returns(ResolutionDetails[T.type_parameter(:U)])
|
101
|
+
)
|
102
|
+
.returns(ResolutionDetails[T.type_parameter(:U)])
|
103
|
+
end
|
104
|
+
def resolve_from_sources(default_value:, &blk)
|
105
|
+
successful_details = providers.each do |provider|
|
106
|
+
details = yield(provider)
|
107
|
+
|
108
|
+
break details if details.error_code.nil?
|
109
|
+
rescue StandardError
|
110
|
+
next
|
111
|
+
end
|
112
|
+
|
113
|
+
if successful_details.is_a?(ResolutionDetails)
|
114
|
+
successful_details
|
115
|
+
else
|
116
|
+
ResolutionDetails.new(value: default_value, error_code: ErrorCode::General, reason: "ERROR")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
# rubocop:enable Metrics/MethodLength
|
120
|
+
end
|
121
|
+
end
|
data/lib/open_feature.rb
CHANGED
@@ -19,17 +19,33 @@ module OpenFeature
|
|
19
19
|
|
20
20
|
sig { params(provider: Provider).void }
|
21
21
|
def set_provider(provider) # rubocop:disable Naming/AccessorMethodName
|
22
|
-
configuration.
|
22
|
+
configuration.provider = provider
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(context: EvaluationContext).void }
|
26
|
+
def set_evaluation_context(context) # rubocop:disable Naming/AccessorMethodName
|
27
|
+
configuration.evaluation_context = context
|
23
28
|
end
|
24
29
|
|
25
30
|
sig { params(hooks: T.any(Hook, T::Array[Hook])).void }
|
26
31
|
def add_hooks(hooks)
|
27
|
-
configuration.
|
32
|
+
configuration.hooks.concat(Array(hooks))
|
28
33
|
end
|
29
34
|
|
30
|
-
sig
|
31
|
-
|
32
|
-
|
35
|
+
sig do
|
36
|
+
params(
|
37
|
+
name: T.nilable(String),
|
38
|
+
evaluation_context: T.nilable(EvaluationContext),
|
39
|
+
hooks: T.nilable(T.any(Hook, T::Array[Hook]))
|
40
|
+
).returns(Client)
|
41
|
+
end
|
42
|
+
def create_client(name: nil, evaluation_context: nil, hooks: nil)
|
43
|
+
Client.new(
|
44
|
+
provider: configuration.provider,
|
45
|
+
name: name,
|
46
|
+
evaluation_context: evaluation_context,
|
47
|
+
hooks: Array(hooks)
|
48
|
+
)
|
33
49
|
end
|
34
50
|
|
35
51
|
sig { returns(Configuration) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openfeature-sdk-sorbet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max VelDink
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-05-
|
11
|
+
date: 2023-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sorbet-runtime
|
@@ -52,7 +52,7 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '2.6'
|
55
|
-
description:
|
55
|
+
description:
|
56
56
|
email:
|
57
57
|
- maxveldink@gmail.com
|
58
58
|
executables: []
|
@@ -76,10 +76,12 @@ files:
|
|
76
76
|
- lib/open_feature/configuration.rb
|
77
77
|
- lib/open_feature/error_code.rb
|
78
78
|
- lib/open_feature/evaluation_context.rb
|
79
|
+
- lib/open_feature/evaluation_context_builder.rb
|
79
80
|
- lib/open_feature/evaluation_details.rb
|
80
81
|
- lib/open_feature/evaluation_options.rb
|
81
82
|
- lib/open_feature/flag_metadata.rb
|
82
83
|
- lib/open_feature/hook.rb
|
84
|
+
- lib/open_feature/multiple_source_provider.rb
|
83
85
|
- lib/open_feature/no_op_provider.rb
|
84
86
|
- lib/open_feature/provider.rb
|
85
87
|
- lib/open_feature/provider_metadata.rb
|
@@ -129,7 +131,7 @@ metadata:
|
|
129
131
|
homepage_uri: https://github.com/maxveldink/openfeature-sdk-sorbet
|
130
132
|
source_code_uri: https://github.com/maxveldink/openfeature-sdk-sorbet
|
131
133
|
changelog_uri: https://github.com/maxveldink/openfeature-sdk-sorbet/blob/main/CHANGELOG.md
|
132
|
-
post_install_message:
|
134
|
+
post_install_message:
|
133
135
|
rdoc_options: []
|
134
136
|
require_paths:
|
135
137
|
- lib
|
@@ -144,8 +146,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
146
|
- !ruby/object:Gem::Version
|
145
147
|
version: '0'
|
146
148
|
requirements: []
|
147
|
-
rubygems_version: 3.4.
|
148
|
-
signing_key:
|
149
|
+
rubygems_version: 3.4.10
|
150
|
+
signing_key:
|
149
151
|
specification_version: 4
|
150
152
|
summary: Sorbet-aware implemention of the OpenFeature specification
|
151
153
|
test_files: []
|