openfeature-sdk-sorbet 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -1
  3. data/.ruby-version +1 -1
  4. data/.tool-versions +1 -1
  5. data/CHANGELOG.md +12 -3
  6. data/Gemfile +1 -1
  7. data/Gemfile.lock +55 -49
  8. data/README.md +23 -6
  9. data/lib/open_feature/client.rb +90 -32
  10. data/lib/open_feature/client_metadata.rb +1 -0
  11. data/lib/open_feature/evaluation_context.rb +2 -2
  12. data/lib/open_feature/evaluation_details.rb +2 -2
  13. data/lib/open_feature/hook.rb +41 -1
  14. data/lib/open_feature/hook_context.rb +30 -0
  15. data/lib/open_feature/hooks.rb +22 -0
  16. data/lib/open_feature/multiple_source_provider.rb +28 -12
  17. data/lib/open_feature/no_op_provider.rb +2 -3
  18. data/lib/open_feature/provider.rb +16 -2
  19. data/lib/open_feature/provider_status.rb +13 -0
  20. data/lib/open_feature.rb +9 -2
  21. data/sorbet/rbi/gems/.gitattributes +1 -0
  22. data/sorbet/rbi/gems/{json@2.6.3.rbi → json@2.7.1.rbi} +80 -60
  23. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
  24. data/sorbet/rbi/gems/{minitest@5.18.0.rbi → minitest@5.21.2.rbi} +299 -258
  25. data/sorbet/rbi/gems/{parallel@1.23.0.rbi → parallel@1.24.0.rbi} +8 -1
  26. data/sorbet/rbi/gems/{parser@3.2.2.1.rbi → parser@3.3.0.5.rbi} +438 -2219
  27. data/sorbet/rbi/gems/prism@0.19.0.rbi +25199 -0
  28. data/sorbet/rbi/gems/psych@5.1.2.rbi +1731 -0
  29. data/sorbet/rbi/gems/racc@1.7.3.rbi +157 -0
  30. data/sorbet/rbi/gems/{rake@13.0.6.rbi → rake@13.1.0.rbi} +68 -65
  31. data/sorbet/rbi/gems/{rbi@0.0.16.rbi → rbi@0.1.6.rbi} +628 -755
  32. data/sorbet/rbi/gems/{regexp_parser@2.8.0.rbi → regexp_parser@2.9.0.rbi} +203 -180
  33. data/sorbet/rbi/gems/{rexml@3.2.5.rbi → rexml@3.2.6.rbi} +116 -52
  34. data/sorbet/rbi/gems/{rubocop-ast@1.28.1.rbi → rubocop-ast@1.30.0.rbi} +178 -84
  35. data/sorbet/rbi/gems/{rubocop-minitest@0.31.0.rbi → rubocop-minitest@0.34.5.rbi} +280 -232
  36. data/sorbet/rbi/gems/{rubocop-performance@1.17.1.rbi → rubocop-performance@1.20.2.rbi} +397 -172
  37. data/sorbet/rbi/gems/{rubocop-sorbet@0.7.0.rbi → rubocop-sorbet@0.7.6.rbi} +728 -261
  38. data/sorbet/rbi/gems/{rubocop@1.51.0.rbi → rubocop@1.60.2.rbi} +4006 -1936
  39. data/sorbet/rbi/gems/spoom@1.2.1.rbi +17 -56
  40. data/sorbet/rbi/gems/stringio@3.1.0.rbi +8 -0
  41. data/sorbet/rbi/gems/{tapioca@0.11.6.rbi → tapioca@0.11.17.rbi} +778 -576
  42. data/sorbet/rbi/gems/{thor@1.2.2.rbi → thor@1.3.0.rbi} +775 -395
  43. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +1 -1
  44. data/sorbet/rbi/gems/yard@0.9.34.rbi +2 -2
  45. data/sorbet/rbi/gems/{zeitwerk@2.6.8.rbi → zeitwerk@2.6.12.rbi} +78 -67
  46. data/sorbet/tapioca/config.yml +2 -2
  47. data/sorbet/tapioca/require.rb +3 -1
  48. metadata +36 -31
  49. data/openfeature-sdk-sorbet.gemspec +0 -35
  50. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +0 -1083
  51. data/sorbet/rbi/gems/irb@1.6.4.rbi +0 -342
  52. data/sorbet/rbi/gems/unparser@0.6.7.rbi +0 -4524
  53. /data/sorbet/rbi/gems/{io-console@0.6.0.rbi → io-console@0.7.2.rbi} +0 -0
  54. /data/sorbet/rbi/gems/{reline@0.3.3.rbi → reline@0.4.2.rbi} +0 -0
  55. /data/sorbet/rbi/gems/{unicode-display_width@2.4.2.rbi → unicode-display_width@2.5.0.rbi} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5c2231f2741a4d89cde28735ddfd5c509bba978559bb732a98a1864bb65dd30
4
- data.tar.gz: 54d011f1a7bc30a14647632914440ea2814eee934773996d4e70ce9119ec52bc
3
+ metadata.gz: c15c80725be63842d38491ff9c0addddcf29d063a191eadb44fe957e059ab520
4
+ data.tar.gz: 7a263495564db2fee7cb4805d82458ee70951a30ebe15bb470f205c97c32b529
5
5
  SHA512:
6
- metadata.gz: 11643994331234597fc371c44162f95445ba7e3c5b347a0af614206ebf82df2b884dae463727f68011268a9250bedd00c9c161f3428bcc1f48aeb3090d815236
7
- data.tar.gz: ee4e892b17b5661ccbab5260a32fa8c460be9ee711f83d99ddc1451e29bdf6edb23e96ef1925aad0bc57c3f77f13b6c44154d24338e591b3a5d3b49987fbf571
6
+ metadata.gz: 6cb5f8413afe5a94866d456e841fabc092c09058d0117df0847e33111cd1020b313f45c5ad512200f19e463557369eca2a846f6eb0b28b6e4f8c7fff898ce1e7
7
+ data.tar.gz: 6279c0be67ad089ee53201d451cf6349cc976e59d016c850a1d74711a47bc890aad6ba5e11ff92ec343c8277555a81834bf238d7a487275db33cd224400272e1
data/.rubocop.yml CHANGED
@@ -10,7 +10,7 @@ require:
10
10
 
11
11
  AllCops:
12
12
  NewCops: enable
13
- TargetRubyVersion: 2.7
13
+ TargetRubyVersion: 3.1
14
14
  Exclude:
15
15
  - sorbet/**/*.rbi
16
16
 
@@ -25,6 +25,7 @@ Lint/UnusedMethodArgument:
25
25
  Metrics/ClassLength:
26
26
  Exclude:
27
27
  - lib/open_feature/client.rb
28
+ - lib/open_feature/multiple_source_provider.rb
28
29
  - test/open_feature/client_test.rb
29
30
  - test/open_feature/multiple_source_provider_test.rb
30
31
 
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.2
1
+ 3.3.0
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.2.2
1
+ ruby 3.3.0
data/CHANGELOG.md CHANGED
@@ -6,11 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.2.1] - 2023-07-27
9
+ ## [0.3.0] - 2024-01-24
10
10
 
11
- ### Fixed
11
+ ### Added
12
+
13
+ - Added `OpenFeature::Provider#status` reader that returns an `OpenFeature::ProviderStatus`.
14
+ - Added `OpenFeature::Provider#init` as an overridable method on `Provider`s that can perform any initialization work for the given provider. This method accepts the global `EvaluationContext` and has a `void` return type. The default implementation is a no-op.
15
+ - Added `OpenFeature::Provider#shutdown` as an overridable method on `Provider`s that can perform any cleanup work for the given provider. This method has a `void` return type and the default implementation is a no-op.
16
+ - Added `OpenFeature.shutdown` to invoke the current provider's `shutdown` method.
17
+
18
+ ### Changed
12
19
 
13
- - Fixed an issue where the `OpenFeature::MultipleSourceProvider` did not properly pass evaluation context to the internal providers.
20
+ - *Breaking* Changed minimum supported Ruby version to 3.1.
21
+ - *Breaking* `OpenFeature::Provider` changed from a module interface to an abstract class to support default method implementations.
22
+ - *Breaking* `OpenFeature::Provider.initialize` now must accept an `OpenFeature::ProviderStatus`. Any providers may pass this in during initialization. If you need to do additional setup in `Provider.init`, we recommend you pass `OpenFeature::ProviderStatus::NotReady` here.
14
23
 
15
24
  ## [0.2.0] - 2023-05-17
16
25
 
data/Gemfile CHANGED
@@ -13,7 +13,7 @@ group :development do
13
13
  gem "rubocop-rake"
14
14
  gem "rubocop-sorbet"
15
15
  gem "sorbet"
16
- gem "spoom"
16
+ gem "spoom", "1.2.1" # remove once we go to Ruby 3+
17
17
  gem "tapioca", require: false
18
18
  end
19
19
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk-sorbet (0.2.1)
4
+ openfeature-sdk-sorbet (0.3.0)
5
5
  sorbet-runtime (~> 0.5)
6
6
  sorbet-struct-comparable (~> 1.3)
7
7
  zeitwerk (~> 2.6)
@@ -10,88 +10,94 @@ GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
12
  ast (2.4.2)
13
- debug (1.8.0)
14
- irb (>= 1.5.0)
15
- reline (>= 0.3.1)
16
- diff-lcs (1.5.0)
17
- io-console (0.6.0)
18
- irb (1.6.4)
19
- reline (>= 0.3.0)
20
- json (2.6.3)
21
- minitest (5.18.0)
13
+ debug (1.9.1)
14
+ irb (~> 1.10)
15
+ reline (>= 0.3.8)
16
+ io-console (0.7.2)
17
+ irb (1.11.1)
18
+ rdoc
19
+ reline (>= 0.4.2)
20
+ json (2.7.1)
21
+ language_server-protocol (3.17.0.3)
22
+ minitest (5.21.2)
22
23
  netrc (0.11.0)
23
- parallel (1.23.0)
24
- parser (3.2.2.1)
24
+ parallel (1.24.0)
25
+ parser (3.3.0.5)
25
26
  ast (~> 2.4.1)
27
+ racc
28
+ prism (0.19.0)
29
+ psych (5.1.2)
30
+ stringio
31
+ racc (1.7.3)
26
32
  rainbow (3.1.1)
27
- rake (13.0.6)
28
- rbi (0.0.16)
29
- ast
30
- parser (>= 2.6.4.0)
33
+ rake (13.1.0)
34
+ rbi (0.1.6)
35
+ prism (>= 0.18.0, < 0.20)
31
36
  sorbet-runtime (>= 0.5.9204)
32
- unparser
33
- regexp_parser (2.8.0)
34
- reline (0.3.3)
37
+ rdoc (6.6.2)
38
+ psych (>= 4.0.0)
39
+ regexp_parser (2.9.0)
40
+ reline (0.4.2)
35
41
  io-console (~> 0.5)
36
- rexml (3.2.5)
37
- rubocop (1.51.0)
42
+ rexml (3.2.6)
43
+ rubocop (1.60.2)
38
44
  json (~> 2.3)
45
+ language_server-protocol (>= 3.17.0)
39
46
  parallel (~> 1.10)
40
- parser (>= 3.2.0.0)
47
+ parser (>= 3.3.0.2)
41
48
  rainbow (>= 2.2.2, < 4.0)
42
49
  regexp_parser (>= 1.8, < 3.0)
43
50
  rexml (>= 3.2.5, < 4.0)
44
- rubocop-ast (>= 1.28.0, < 2.0)
51
+ rubocop-ast (>= 1.30.0, < 2.0)
45
52
  ruby-progressbar (~> 1.7)
46
53
  unicode-display_width (>= 2.4.0, < 3.0)
47
- rubocop-ast (1.28.1)
54
+ rubocop-ast (1.30.0)
48
55
  parser (>= 3.2.1.0)
49
- rubocop-minitest (0.31.0)
56
+ rubocop-minitest (0.34.5)
50
57
  rubocop (>= 1.39, < 2.0)
51
- rubocop-performance (1.17.1)
52
- rubocop (>= 1.7.0, < 2.0)
53
- rubocop-ast (>= 0.4.0)
58
+ rubocop-ast (>= 1.30.0, < 2.0)
59
+ rubocop-performance (1.20.2)
60
+ rubocop (>= 1.48.1, < 2.0)
61
+ rubocop-ast (>= 1.30.0, < 2.0)
54
62
  rubocop-rake (0.6.0)
55
63
  rubocop (~> 1.0)
56
- rubocop-sorbet (0.7.0)
64
+ rubocop-sorbet (0.7.6)
57
65
  rubocop (>= 0.90.0)
58
66
  ruby-progressbar (1.13.0)
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
+ sorbet (0.5.11218)
68
+ sorbet-static (= 0.5.11218)
69
+ sorbet-runtime (0.5.11218)
70
+ sorbet-static (0.5.11218-universal-darwin)
71
+ sorbet-static (0.5.11218-x86_64-linux)
72
+ sorbet-static-and-runtime (0.5.11218)
73
+ sorbet (= 0.5.11218)
74
+ sorbet-runtime (= 0.5.11218)
67
75
  sorbet-struct-comparable (1.3.0)
68
76
  sorbet-runtime (>= 0.5)
69
77
  spoom (1.2.1)
70
78
  sorbet (>= 0.5.10187)
71
79
  sorbet-runtime (>= 0.5.9204)
72
80
  thor (>= 0.19.2)
73
- tapioca (0.11.6)
81
+ stringio (3.1.0)
82
+ tapioca (0.11.17)
74
83
  bundler (>= 2.2.25)
75
84
  netrc (>= 0.11.0)
76
85
  parallel (>= 1.21.0)
77
- rbi (~> 0.0.0, >= 0.0.16)
78
- sorbet-static-and-runtime (>= 0.5.10187)
86
+ rbi (>= 0.1.4, < 0.2)
87
+ sorbet-static-and-runtime (>= 0.5.10820)
79
88
  spoom (~> 1.2.0, >= 1.2.0)
80
89
  thor (>= 1.2.0)
81
90
  yard-sorbet
82
- thor (1.2.2)
83
- unicode-display_width (2.4.2)
84
- unparser (0.6.7)
85
- diff-lcs (~> 1.3)
86
- parser (>= 3.2.0)
91
+ thor (1.3.0)
92
+ unicode-display_width (2.5.0)
87
93
  yard (0.9.34)
88
94
  yard-sorbet (0.8.1)
89
95
  sorbet-runtime (>= 0.5)
90
96
  yard (>= 0.9)
91
- zeitwerk (2.6.8)
97
+ zeitwerk (2.6.12)
92
98
 
93
99
  PLATFORMS
94
- arm64-darwin-22
100
+ arm64-darwin
95
101
  x86_64-linux
96
102
 
97
103
  DEPENDENCIES
@@ -106,8 +112,8 @@ DEPENDENCIES
106
112
  rubocop-sorbet
107
113
  sorbet
108
114
  sorbet-runtime
109
- spoom
115
+ spoom (= 1.2.1)
110
116
  tapioca
111
117
 
112
118
  BUNDLED WITH
113
- 2.4.12
119
+ 2.5.5
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  If an organization is not already using Sorbet, you probably don't want to introduce a dependency on `sorbet-runtime`, which this gem does. As such, this will always be a distinct implementation, separate from the official [Ruby SDK](https://github.com/open-feature/ruby-sdk).
6
6
 
7
7
  Current OpenFeature specification target version: `0.5.2`
8
- Current supported Ruby versions: `2.7.X`, `3.0.X`, `3.1.X`, `3.2.X`
8
+ Current supported Ruby versions: `3.1.X`, `3.2.X`
9
9
  Support for Evaluation Context and Hooks is not complete.
10
10
 
11
11
  ## Installation
@@ -53,7 +53,7 @@ The OpenFeature specification defines [Structure as a potential return type](htt
53
53
 
54
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
55
 
56
- ### Provider Interface
56
+ ### Provider Abstract Class
57
57
 
58
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).
59
59
 
@@ -72,13 +72,26 @@ OpenFeature.set_provider(provider)
72
72
 
73
73
  #### Implementing Custom Providers
74
74
 
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:
75
+ Thanks to Sorbet abstract classes, it's fairly straightforward to implement a new provider. Here is an example for a JSON-based flag format on disk:
76
76
 
77
77
  ```ruby
78
- class JsonFileFlagProvider
78
+ class JsonFileFlagProvider < OpenFeature::Provider
79
79
  extend T::Sig
80
80
 
81
- include OpenFeature::Provider
81
+ sig { void }
82
+ def initialize
83
+ super(OpenFeature::ProviderStatus::NotReady)
84
+ end
85
+
86
+ def init(context)
87
+ @file = File.open(context.file || "flags.json")
88
+ @status = OpenFeature::ProviderStatus::Ready
89
+ end
90
+
91
+ sig { overridable.void }
92
+ def shutdown
93
+ @file.close
94
+ end
82
95
 
83
96
  sig { override.returns(OpenFeature::ProviderMetadata) }
84
97
  def metadata
@@ -113,7 +126,11 @@ class JsonFileFlagProvider
113
126
  end
114
127
  ```
115
128
 
116
- By including the `OpenFeature::Provider` module, Sorbet will indicate what methods it's expecting and what their type signatures should be.
129
+ By inheriting from the `OpenFeature::Provider` class, Sorbet will indicate what methods it's expecting and what their type signatures should be.
130
+
131
+ ##### A note on `initialize` versus `init`
132
+
133
+ The Ruby `initialize` method is the best place to do any direct construction logic for an object, such as setting configuration values. `init` is called by OpenFeature when setting a provider and is the best place to make any HTTP requests, establish persistent connections, or any other connection logic that could potentially fail. By the end of this method, `@status` must be set to either `Ready` or `Error`.
117
134
 
118
135
  ## Development
119
136
 
@@ -25,7 +25,7 @@ module OpenFeature
25
25
  end
26
26
  def initialize(provider:, name: nil, evaluation_context: nil, hooks: [])
27
27
  @provider = provider
28
- @client_metadata = T.let(ClientMetadata.new(name: name), ClientMetadata)
28
+ @client_metadata = T.let(ClientMetadata.new(name:), ClientMetadata)
29
29
  @evaluation_context = evaluation_context
30
30
  @hooks = hooks
31
31
  end
@@ -43,10 +43,12 @@ module OpenFeature
43
43
  options: T.nilable(EvaluationOptions)
44
44
  ).returns(T::Boolean)
45
45
  end
46
- def fetch_boolean_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
47
- provider
48
- .resolve_boolean_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
49
- .value
46
+ def fetch_boolean_value(flag_key:, default_value:, context: nil, options: nil)
47
+ evaluated_context = build_context_with_before_hooks(flag_key:, default_value:,
48
+ invocation_context: context, options:,
49
+ flag_type: "Boolean")
50
+ provider.resolve_boolean_value(flag_key:, default_value:, context: evaluated_context)
51
+ .value
50
52
  rescue StandardError
51
53
  default_value
52
54
  end
@@ -61,14 +63,14 @@ module OpenFeature
61
63
  end
62
64
  def fetch_boolean_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
63
65
  details = provider.resolve_boolean_value(
64
- flag_key: flag_key,
65
- default_value: default_value,
66
+ flag_key:,
67
+ default_value:,
66
68
  context: build_context(context)
67
69
  )
68
70
 
69
- EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
71
+ EvaluationDetails.from_resolution_details(details, flag_key:)
70
72
  rescue StandardError => e
71
- EvaluationDetails.from_error(e.message, flag_key: flag_key, default_value: default_value)
73
+ EvaluationDetails.from_error(e.message, flag_key:, default_value:)
72
74
  end
73
75
 
74
76
  sig do
@@ -79,9 +81,12 @@ module OpenFeature
79
81
  options: T.nilable(EvaluationOptions)
80
82
  ).returns(String)
81
83
  end
82
- def fetch_string_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
84
+ def fetch_string_value(flag_key:, default_value:, context: nil, options: nil)
85
+ evaluated_context = build_context_with_before_hooks(flag_key:, default_value:,
86
+ invocation_context: context, options:,
87
+ flag_type: "String")
83
88
  provider
84
- .resolve_string_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
89
+ .resolve_string_value(flag_key:, default_value:, context: evaluated_context)
85
90
  .value
86
91
  rescue StandardError
87
92
  default_value
@@ -97,14 +102,14 @@ module OpenFeature
97
102
  end
98
103
  def fetch_string_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
99
104
  details = provider.resolve_string_value(
100
- flag_key: flag_key,
101
- default_value: default_value,
105
+ flag_key:,
106
+ default_value:,
102
107
  context: build_context(context)
103
108
  )
104
109
 
105
- EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
110
+ EvaluationDetails.from_resolution_details(details, flag_key:)
106
111
  rescue StandardError => e
107
- EvaluationDetails.from_error(e.message, flag_key: flag_key, default_value: default_value)
112
+ EvaluationDetails.from_error(e.message, flag_key:, default_value:)
108
113
  end
109
114
 
110
115
  sig do
@@ -115,9 +120,12 @@ module OpenFeature
115
120
  options: T.nilable(EvaluationOptions)
116
121
  ).returns(Numeric)
117
122
  end
118
- def fetch_number_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
123
+ def fetch_number_value(flag_key:, default_value:, context: nil, options: nil)
124
+ evaluated_context = build_context_with_before_hooks(flag_key:, default_value:,
125
+ invocation_context: context, options:,
126
+ flag_type: "Number")
119
127
  provider
120
- .resolve_number_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
128
+ .resolve_number_value(flag_key:, default_value:, context: evaluated_context)
121
129
  .value
122
130
  rescue StandardError
123
131
  default_value
@@ -133,14 +141,14 @@ module OpenFeature
133
141
  end
134
142
  def fetch_number_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
135
143
  details = provider.resolve_number_value(
136
- flag_key: flag_key,
137
- default_value: default_value,
144
+ flag_key:,
145
+ default_value:,
138
146
  context: build_context(context)
139
147
  )
140
148
 
141
- EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
149
+ EvaluationDetails.from_resolution_details(details, flag_key:)
142
150
  rescue StandardError => e
143
- EvaluationDetails.from_error(e.message, flag_key: flag_key, default_value: default_value)
151
+ EvaluationDetails.from_error(e.message, flag_key:, default_value:)
144
152
  end
145
153
 
146
154
  sig do
@@ -151,9 +159,12 @@ module OpenFeature
151
159
  options: T.nilable(EvaluationOptions)
152
160
  ).returns(Integer)
153
161
  end
154
- def fetch_integer_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
162
+ def fetch_integer_value(flag_key:, default_value:, context: nil, options: nil)
163
+ evaluated_context = build_context_with_before_hooks(flag_key:, default_value:,
164
+ invocation_context: context, options:,
165
+ flag_type: "Integer")
155
166
  provider
156
- .resolve_number_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
167
+ .resolve_number_value(flag_key:, default_value:, context: evaluated_context)
157
168
  .value
158
169
  .to_i
159
170
  rescue StandardError
@@ -168,9 +179,12 @@ module OpenFeature
168
179
  options: T.nilable(EvaluationOptions)
169
180
  ).returns(Float)
170
181
  end
171
- def fetch_float_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
182
+ def fetch_float_value(flag_key:, default_value:, context: nil, options: nil)
183
+ evaluated_context = build_context_with_before_hooks(flag_key:, default_value:,
184
+ invocation_context: context, options:,
185
+ flag_type: "Float")
172
186
  provider
173
- .resolve_number_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
187
+ .resolve_number_value(flag_key:, default_value:, context: evaluated_context)
174
188
  .value
175
189
  .to_f
176
190
  rescue StandardError
@@ -185,9 +199,12 @@ module OpenFeature
185
199
  options: T.nilable(EvaluationOptions)
186
200
  ).returns(Structure)
187
201
  end
188
- def fetch_structure_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
202
+ def fetch_structure_value(flag_key:, default_value:, context: nil, options: nil)
203
+ evaluated_context = build_context_with_before_hooks(flag_key:, default_value:,
204
+ invocation_context: context, options:,
205
+ flag_type: "Structure")
189
206
  provider
190
- .resolve_structure_value(flag_key: flag_key, default_value: default_value, context: build_context(context))
207
+ .resolve_structure_value(flag_key:, default_value:, context: evaluated_context)
191
208
  .value
192
209
  rescue StandardError
193
210
  default_value
@@ -203,14 +220,14 @@ module OpenFeature
203
220
  end
204
221
  def fetch_structure_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
205
222
  details = provider.resolve_structure_value(
206
- flag_key: flag_key,
207
- default_value: default_value,
223
+ flag_key:,
224
+ default_value:,
208
225
  context: build_context(context)
209
226
  )
210
227
 
211
- EvaluationDetails.from_resolution_details(details, flag_key: flag_key)
228
+ EvaluationDetails.from_resolution_details(details, flag_key:)
212
229
  rescue StandardError => e
213
- EvaluationDetails.from_error(e.message, flag_key: flag_key, default_value: default_value)
230
+ EvaluationDetails.from_error(e.message, flag_key:, default_value:)
214
231
  end
215
232
 
216
233
  private
@@ -218,12 +235,53 @@ module OpenFeature
218
235
  sig { returns(Provider) }
219
236
  attr_reader :provider
220
237
 
238
+ sig do
239
+ type_parameters(:U).params(
240
+ flag_key: String,
241
+ default_value: T.type_parameter(:U),
242
+ invocation_context: T.nilable(EvaluationContext),
243
+ options: T.nilable(EvaluationOptions),
244
+ flag_type: String
245
+ ).returns(EvaluationContext)
246
+ end
247
+ def build_context_with_before_hooks(flag_key:, default_value:, invocation_context:, options:, flag_type:)
248
+ hook_context = build_hook_context(flag_key:, default_value:,
249
+ invocation_context:, flag_type:)
250
+ OpenFeature::Hook::BeforeHook.call(hooks: build_hooks(options), context: hook_context,
251
+ hints: {})
252
+ end
253
+
254
+ sig { params(options: T.nilable(EvaluationOptions)).returns(Hooks) }
255
+ def build_hooks(options)
256
+ Hooks.new(
257
+ global: OpenFeature.configuration.hooks,
258
+ client: hooks,
259
+ invocation: (options ? options.hooks : []),
260
+ provider: provider.hooks
261
+ )
262
+ end
263
+
264
+ sig do
265
+ type_parameters(:U).params(
266
+ flag_key: String,
267
+ default_value: T.type_parameter(:U),
268
+ invocation_context: T.nilable(EvaluationContext),
269
+ flag_type: String
270
+ ).returns(HookContext[T.type_parameter(:U)])
271
+ end
272
+ def build_hook_context(flag_key:, default_value:, invocation_context:, flag_type:)
273
+ evaluation_context = build_context(invocation_context) || OpenFeature::EvaluationContext.new
274
+ HookContext.new(flag_key:, default_value:,
275
+ evaluation_context:, flag_type:,
276
+ client_metadata:, provider_metadata: provider.metadata)
277
+ end
278
+
221
279
  sig { params(invocation_context: T.nilable(EvaluationContext)).returns(T.nilable(EvaluationContext)) }
222
280
  def build_context(invocation_context)
223
281
  EvaluationContextBuilder.new.call(
224
282
  global_context: OpenFeature.configuration.evaluation_context,
225
283
  client_context: evaluation_context,
226
- invocation_context: invocation_context
284
+ invocation_context:
227
285
  )
228
286
  end
229
287
  end
@@ -4,6 +4,7 @@
4
4
  module OpenFeature
5
5
  # Defines information about a Client.
6
6
  class ClientMetadata < T::Struct
7
+ include T::Struct::ActsAsComparable
7
8
  const :name, T.nilable(String)
8
9
  end
9
10
  end
@@ -13,7 +13,7 @@ module OpenFeature
13
13
 
14
14
  FieldValueType = T.type_alias { T.any(T::Boolean, String, Numeric, DateTime, Structure) }
15
15
 
16
- const :targeting_key, T.nilable(String)
16
+ const :targeting_key, T.nilable(String), default: nil
17
17
  const :fields, T::Hash[String, FieldValueType], default: {}
18
18
 
19
19
  sig { params(method_name: Symbol).returns(T::Boolean) }
@@ -29,7 +29,7 @@ module OpenFeature
29
29
  sig { params(key: String, value: FieldValueType).returns(EvaluationContext) }
30
30
  def add_field(key, value)
31
31
  EvaluationContext.new(
32
- targeting_key: targeting_key,
32
+ targeting_key:,
33
33
  fields: fields.merge({ key => value })
34
34
  )
35
35
  end
@@ -25,7 +25,7 @@ module OpenFeature
25
25
  sig { params(details: ResolutionDetails[SelfValue], flag_key: String).returns(EvaluationDetails[SelfValue]) }
26
26
  def from_resolution_details(details, flag_key:)
27
27
  EvaluationDetails.new(
28
- flag_key: flag_key,
28
+ flag_key:,
29
29
  value: details.value,
30
30
  error_code: details.error_code,
31
31
  error_message: details.error_message,
@@ -44,7 +44,7 @@ module OpenFeature
44
44
  end
45
45
  def from_error(error_message, flag_key:, default_value:)
46
46
  EvaluationDetails.new(
47
- flag_key: flag_key,
47
+ flag_key:,
48
48
  value: default_value,
49
49
  error_code: ErrorCode::General,
50
50
  error_message: "Provider raised error: #{error_message}",
@@ -2,5 +2,45 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module OpenFeature
5
- class Hook < T::Struct; end
5
+ # See https://openfeature.dev/specification/sections/hooks
6
+ # We model Hooks as a simple ADT
7
+ module Hook
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ interface!
11
+ sealed!
12
+
13
+ # See Requirement 4.3.2 - 4.3.4
14
+ class BeforeHook
15
+ include Hook
16
+ extend T::Sig
17
+ extend T::Helpers
18
+ include OpenFeature::Hook
19
+ abstract!
20
+ sig do
21
+ abstract.params(context: OpenFeature::HookContext[T.untyped],
22
+ hints: T::Hash[String, T.untyped]).returns(OpenFeature::EvaluationContext)
23
+ end
24
+ def call(context:, hints:); end
25
+
26
+ class << self
27
+ extend T::Sig
28
+
29
+ sig do
30
+ params(hooks: Hooks, context: OpenFeature::HookContext[T.untyped],
31
+ hints: T::Hash[String, T.untyped]).returns(OpenFeature::EvaluationContext)
32
+ end
33
+ def call(hooks:, context:, hints:)
34
+ context.evaluation_context.merge(
35
+ hooks.before.reduce(context.evaluation_context) do |evaluation_context, hook|
36
+ hook.call(
37
+ context: context.with_new_evaluation_context(evaluation_context),
38
+ hints:
39
+ )
40
+ end
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
6
46
  end
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenFeature
5
+ # See https://openfeature.dev/specification/sections/hooks#41-hook-context
6
+ # See Requirement 4.1.1, 4.1.3, 4.1.4
7
+ # TODO: Requirement 4.1.2
8
+ class HookContext < T::Struct
9
+ extend T::Generic
10
+ extend T::Sig
11
+
12
+ include T::Struct::ActsAsComparable
13
+
14
+ Value = type_member
15
+ const :flag_key, String
16
+ const :flag_type, String
17
+ const :evaluation_context, EvaluationContext
18
+ const :default_value, Value
19
+ const :client_metadata, ClientMetadata
20
+ const :provider_metadata, ProviderMetadata
21
+
22
+ # Needed as opposed to .with due to https://sorbet.org/docs/tstruct#from_hash-gotchas
23
+ sig { params(new_context: EvaluationContext).returns(HookContext[Value]) }
24
+ def with_new_evaluation_context(new_context)
25
+ OpenFeature::HookContext.new(flag_key:, flag_type:, default_value:,
26
+ evaluation_context: new_context, client_metadata:,
27
+ provider_metadata:)
28
+ end
29
+ end
30
+ end