openfeature-sdk-sorbet 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96650d45be9bb86f0349e27d1ad9d56c748659b3d075b6c2cc6c769fff642571
4
- data.tar.gz: 370bd63d0ffb1143eec2de19d3b70e5e0b5033b4dcd0f08117dfbaeee7c1f8cd
3
+ metadata.gz: b0df7fa6e69c0e30dd01d5e5f0aad7b5543787ed2ab7a11ab35d7b034ffe1dc6
4
+ data.tar.gz: 22de53663fcd170d801f9ed12e0fb7bef01618b685ca61f2dbe486801d5fdcc1
5
5
  SHA512:
6
- metadata.gz: 915afef5bdae8110b53c4e96dc9494955636753c1170349eb25f432760590fc75004ebd6f8c47bd3359da982b81fbae0c08a7053df4b552fe760f939021bdd00
7
- data.tar.gz: 799b9745b26bf0eac7cdf5e54b237d63abe8f2af5e238bcaf554bd368105dc880a7cc643e5565529053baa4a7041cbc61a5c95eafcc56331ad25b6c2527883e9
6
+ metadata.gz: b31be23ed317d0e8d1418085f2e4adb655030d023a08cf1eb3ef450fa5aaf091319bdd6cd4af57d28d795e48711c705499aed23181c9f894919c01af8a7e8495
7
+ data.tar.gz: b3bdf0316cd4637a7970791f2d2b5d4d3407bb45cd14b613fd268ce5f4e3486d6cdbe5731c955b5c92f4b9f26196d1a514dc8f54f683f8ec776f345f102f7173
data/.rubocop.yml CHANGED
@@ -30,9 +30,7 @@ Metrics/ClassLength:
30
30
 
31
31
  Metrics/MethodLength:
32
32
  Exclude:
33
- - test/open_feature/evaluation_details_test.rb
34
- - test/open_feature/multiple_source_provider_test.rb
35
- - test/support/test_provider.rb
33
+ - test/**/*
36
34
 
37
35
  Style/AccessorGrouping:
38
36
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ 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
+
9
29
  ## [0.1.2] - 2023-05-16
10
30
 
11
31
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk-sorbet (0.1.2)
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.10827)
60
- sorbet-static (= 0.5.10827)
61
- sorbet-runtime (0.5.10827)
62
- sorbet-static (0.5.10827-universal-darwin-22)
63
- sorbet-static (0.5.10827-x86_64-linux)
64
- sorbet-static-and-runtime (0.5.10827)
65
- sorbet (= 0.5.10827)
66
- sorbet-runtime (= 0.5.10827)
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,6 +49,10 @@ 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).
@@ -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 { params(provider: Provider, name: T.nilable(String)).void }
16
- def initialize(provider:, name: nil)
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
- @hooks = T.let([], T::Array[Hook])
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.resolve_boolean_value(flag_key: flag_key, default_value: default_value, context: context).value
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(flag_key: flag_key, default_value: default_value, context: context)
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.resolve_string_value(flag_key: flag_key, default_value: default_value, context: context).value
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(flag_key: flag_key, default_value: default_value, context: context)
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.resolve_number_value(flag_key: flag_key, default_value: default_value, context: context).value
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(flag_key: flag_key, default_value: default_value, context: context)
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.resolve_number_value(flag_key: flag_key, default_value: default_value, context: context).value.to_i
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.resolve_number_value(flag_key: flag_key, default_value: default_value, context: context).value.to_f
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.resolve_structure_value(flag_key: flag_key, default_value: default_value, context: context).value
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(flag_key: flag_key, default_value: default_value, context: context)
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
- attr_reader :provider
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
- attr_reader :hooks
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 clear_hooks!
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
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.set_provider(provider)
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.add_hooks(hooks)
32
+ configuration.hooks.concat(Array(hooks))
28
33
  end
29
34
 
30
- sig { params(name: T.nilable(String)).returns(Client) }
31
- def create_client(name: nil)
32
- Client.new(provider: configuration.provider, name: name)
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.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max VelDink
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-16 00:00:00.000000000 Z
11
+ date: 2023-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sorbet-runtime
@@ -76,6 +76,7 @@ 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