openfeature-sdk-sorbet 0.1.0 → 0.1.2

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: e5bada3dd0c6a4687fb9c7f44768f668d809a4d12fdd11439d000ed77e10340e
4
- data.tar.gz: 952ebbe9649d88fd4a76cbb29af2709514d863c1be6106e73f132092a2d9d9c7
3
+ metadata.gz: 96650d45be9bb86f0349e27d1ad9d56c748659b3d075b6c2cc6c769fff642571
4
+ data.tar.gz: 370bd63d0ffb1143eec2de19d3b70e5e0b5033b4dcd0f08117dfbaeee7c1f8cd
5
5
  SHA512:
6
- metadata.gz: 26655c2337a276815db33affb69d3c53c2b48dd7176c6acf6e365caa0e7b84f8899a96a786376f1a316f5aa1618cc540f8e4ac8bb6d05c38dd1011f6e94641fb
7
- data.tar.gz: b0e88fc7f6f91e85294126fc23c9a58a45165bc4b7f3394c158413955fdd428c250edb82a915d2a78491e7a2c2141b86fda0ef5b1ba96552166c22fd17ba2c51
6
+ metadata.gz: 915afef5bdae8110b53c4e96dc9494955636753c1170349eb25f432760590fc75004ebd6f8c47bd3359da982b81fbae0c08a7053df4b552fe760f939021bdd00
7
+ data.tar.gz: 799b9745b26bf0eac7cdf5e54b237d63abe8f2af5e238bcaf554bd368105dc880a7cc643e5565529053baa4a7041cbc61a5c95eafcc56331ad25b6c2527883e9
data/.rubocop.yml CHANGED
@@ -19,16 +19,20 @@ 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
33
  - test/open_feature/evaluation_details_test.rb
34
+ - test/open_feature/multiple_source_provider_test.rb
35
+ - test/support/test_provider.rb
32
36
 
33
37
  Style/AccessorGrouping:
34
38
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.2] - 2023-05-16
10
+
11
+ ### Added
12
+
13
+ - Introduced `OpenFeature::MultipleSourceProvider` to allow fetching flags from multiple sources.
14
+
15
+ ## [0.1.1] - 2023-05-15
16
+
17
+ ### Changed
18
+
19
+ - Expanded type of structure resolver to `T.any(T::Array[T.untyped], T::Hash[T.untyped, T.untyped])`
20
+
9
21
  ## [0.1.0] - 2023-05-15
10
22
 
11
23
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk-sorbet (0.1.0)
4
+ openfeature-sdk-sorbet (0.1.2)
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.10826)
60
- sorbet-static (= 0.5.10826)
61
- sorbet-runtime (0.5.10826)
62
- sorbet-static (0.5.10826-universal-darwin-22)
63
- sorbet-static (0.5.10826-x86_64-linux)
64
- sorbet-static-and-runtime (0.5.10826)
65
- sorbet (= 0.5.10826)
66
- sorbet-runtime (= 0.5.10826)
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)
67
67
  sorbet-struct-comparable (1.3.0)
68
68
  sorbet-runtime (>= 0.5)
69
69
  spoom (1.2.1)
data/README.md CHANGED
@@ -43,12 +43,27 @@ structure_evaluation_details = client.fetch_structure_details(flag_key: "my_stru
43
43
 
44
44
  ### Note on `Structure`
45
45
 
46
- 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::Hash[T.untyped, T.untyped]` 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.
46
+ 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
47
 
48
48
  ### Provider Interface
49
49
 
50
50
  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
51
 
52
+ 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:
53
+
54
+ ```ruby
55
+ provider = OpenFeature::MultipleSourceProvider.new(
56
+ providers: [
57
+ CustomProvider.new,
58
+ OpenFeature::NoOpProvider.new
59
+ ]
60
+ )
61
+
62
+ OpenFeature.set_provider(provider)
63
+ ```
64
+
65
+ #### Implementing Custom Providers
66
+
52
67
  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
68
 
54
69
  ```ruby
@@ -145,10 +145,10 @@ module OpenFeature
145
145
  sig do
146
146
  params(
147
147
  flag_key: String,
148
- default_value: T::Hash[T.untyped, T.untyped],
148
+ default_value: Structure,
149
149
  context: T.nilable(EvaluationContext),
150
150
  options: T.nilable(EvaluationOptions)
151
- ).returns(T::Hash[T.untyped, T.untyped])
151
+ ).returns(Structure)
152
152
  end
153
153
  def fetch_structure_value(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
154
154
  provider.resolve_structure_value(flag_key: flag_key, default_value: default_value, context: context).value
@@ -159,10 +159,10 @@ module OpenFeature
159
159
  sig do
160
160
  params(
161
161
  flag_key: String,
162
- default_value: T::Hash[T.untyped, T.untyped],
162
+ default_value: Structure,
163
163
  context: T.nilable(EvaluationContext),
164
164
  options: T.nilable(EvaluationOptions)
165
- ).returns(EvaluationDetails[T::Hash[T.untyped, T.untyped]])
165
+ ).returns(EvaluationDetails[Structure])
166
166
  end
167
167
  def fetch_structure_details(flag_key:, default_value:, context: nil, options: nil) # rubocop:disable Lint/UnusedMethodArgument
168
168
  details = provider.resolve_structure_value(flag_key: flag_key, default_value: default_value, context: context)
@@ -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
@@ -65,10 +65,10 @@ module OpenFeature
65
65
  override
66
66
  .params(
67
67
  flag_key: String,
68
- default_value: T::Hash[T.untyped, T.untyped],
68
+ default_value: Structure,
69
69
  context: T.nilable(EvaluationContext)
70
70
  )
71
- .returns(ResolutionDetails[T::Hash[T.untyped, T.untyped]])
71
+ .returns(ResolutionDetails[Structure])
72
72
  end
73
73
  def resolve_structure_value(flag_key:, default_value:, context: nil)
74
74
  ResolutionDetails.new(value: default_value, reason: "DEFAULT")
@@ -51,10 +51,10 @@ module OpenFeature
51
51
  abstract
52
52
  .params(
53
53
  flag_key: String,
54
- default_value: T::Hash[T.untyped, T.untyped],
54
+ default_value: Structure,
55
55
  context: T.nilable(EvaluationContext)
56
56
  )
57
- .returns(ResolutionDetails[T::Hash[T.untyped, T.untyped]])
57
+ .returns(ResolutionDetails[Structure])
58
58
  end
59
59
  def resolve_structure_value(flag_key:, default_value:, context: nil); end
60
60
  end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module OpenFeature
5
+ Structure = T.type_alias { T.any(T::Array[T.untyped], T::Hash[T.untyped, T.untyped]) }
6
+ end
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.0
4
+ version: 0.1.2
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-15 00:00:00.000000000 Z
11
+ date: 2023-05-16 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: []
@@ -80,10 +80,12 @@ files:
80
80
  - lib/open_feature/evaluation_options.rb
81
81
  - lib/open_feature/flag_metadata.rb
82
82
  - lib/open_feature/hook.rb
83
+ - lib/open_feature/multiple_source_provider.rb
83
84
  - lib/open_feature/no_op_provider.rb
84
85
  - lib/open_feature/provider.rb
85
86
  - lib/open_feature/provider_metadata.rb
86
87
  - lib/open_feature/resolution_details.rb
88
+ - lib/open_feature/structure.rb
87
89
  - sorbet/config
88
90
  - sorbet/rbi/annotations/rainbow.rbi
89
91
  - sorbet/rbi/gems/ast@2.4.2.rbi
@@ -128,7 +130,7 @@ metadata:
128
130
  homepage_uri: https://github.com/maxveldink/openfeature-sdk-sorbet
129
131
  source_code_uri: https://github.com/maxveldink/openfeature-sdk-sorbet
130
132
  changelog_uri: https://github.com/maxveldink/openfeature-sdk-sorbet/blob/main/CHANGELOG.md
131
- post_install_message:
133
+ post_install_message:
132
134
  rdoc_options: []
133
135
  require_paths:
134
136
  - lib
@@ -143,8 +145,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
145
  - !ruby/object:Gem::Version
144
146
  version: '0'
145
147
  requirements: []
146
- rubygems_version: 3.4.12
147
- signing_key:
148
+ rubygems_version: 3.4.10
149
+ signing_key:
148
150
  specification_version: 4
149
151
  summary: Sorbet-aware implemention of the OpenFeature specification
150
152
  test_files: []