openfeature-sdk 0.3.1 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08a030402839c73a3704f262d09a72b284c96de65c7bdfb09b3020aa180b3448'
4
- data.tar.gz: 4834664e0fced5480a5a36c3adad3ee64ab964ddd90de99fd4f477e168e78fe5
3
+ metadata.gz: ac02affc7dc168be388d9d2a4be338f8e390587cc911372126fc2e25f07237fc
4
+ data.tar.gz: dfa2ba4e40f5498b55d70ce5001233632007a9a10decf13f153edf08455672ad
5
5
  SHA512:
6
- metadata.gz: 03d9d6b4fe3af45924f87a356e3f099789c44763a696e55132f31c263c6ef1fce00a4fdabb7b3224113e23f85d5fc00b43e2cebb8f8e05608e3decaedf39d29b
7
- data.tar.gz: 9a27d780aec56d644bcda6232d91f03a9ff7022a973f8ef1adbd860a8f2e5d1ab0ab180760a6f30e4f5de22d059f1ef1fbf7473c6adafbc77a52385ea36e291e
6
+ metadata.gz: 413138e8e435e278376a6a8ac70a25d1673128e76ed454bfffec9e894517a61f42c8e31ef3adfc25ba71786083eb574427e0b6dfa9748dec099c807dce3b3a7d
7
+ data.tar.gz: 1f494723edf1c24559e19e2083c59fffa6f988f723d46f65df1c7d334023693280f5eb3d9c680fa3a2932bb48737b9dee324c16ce37b55de98dcfe802261664f
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.3.1"
2
+ ".": "0.4.1"
3
3
  }
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.0
1
+ 3.4.7
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.3.0
1
+ ruby 3.4.7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1](https://github.com/open-feature/ruby-sdk/compare/v0.4.0...v0.4.1) (2025-11-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * Add runtime type validation for default values in flag evaluation methods ([#194](https://github.com/open-feature/ruby-sdk/issues/194)) ([dc56c76](https://github.com/open-feature/ruby-sdk/commit/dc56c76f987c16b22a284513b7d0f383d38a0198))
9
+ * Add setProviderAndWait method for blocking provider initialization ([#200](https://github.com/open-feature/ruby-sdk/issues/200)) ([d92eabc](https://github.com/open-feature/ruby-sdk/commit/d92eabcb54335e21522cea0d6b10be3e842ce8e2))
10
+
11
+ ## [0.4.0](https://github.com/open-feature/ruby-sdk/compare/v0.3.1...v0.4.0) (2024-06-13)
12
+
13
+
14
+ ### ⚠ BREAKING CHANGES
15
+
16
+ * Use strings from spec for error and reason enums ([#131](https://github.com/open-feature/ruby-sdk/issues/131))
17
+
18
+ ### Features
19
+
20
+ * add hook hints ([#135](https://github.com/open-feature/ruby-sdk/issues/135)) ([51155a7](https://github.com/open-feature/ruby-sdk/commit/51155a7d9cd2c28b38accb9d9b49018bd4868040))
21
+ * Use strings from spec for error and reason enums ([#131](https://github.com/open-feature/ruby-sdk/issues/131)) ([cb2a4cd](https://github.com/open-feature/ruby-sdk/commit/cb2a4cd54059ffe7ed3484be6705ca2a9d590c1a))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * synchronize provider registration ([#136](https://github.com/open-feature/ruby-sdk/issues/136)) ([1ff6fd0](https://github.com/open-feature/ruby-sdk/commit/1ff6fd0c3732e9e074c8b30cbe4164a67286b0a4))
27
+
3
28
  ## [0.3.1](https://github.com/open-feature/ruby-sdk/compare/v0.3.0...v0.3.1) (2024-04-22)
4
29
 
5
30
 
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,98 @@
1
+ # Contributing to the OpenFeature project
2
+
3
+ ## Development
4
+
5
+ You can contribute to this project from a Windows, macOS or Linux machine.
6
+
7
+ On all platforms, the minimum requirements are:
8
+
9
+ * Git client and command line tools.
10
+ * Ruby 3.0 or higher
11
+
12
+ ## Pull Request
13
+
14
+ All contributions to the OpenFeature project are welcome via GitHub pull requests.
15
+
16
+ To create a new PR, you will need to first fork the GitHub repository and clone upstream.
17
+
18
+ ```bash
19
+ git clone https://github.com/open-feature/ruby-sdk.git openfeature-ruby-sdk
20
+ ```
21
+
22
+ Navigate to the repository folder
23
+ ```bash
24
+ cd openfeature-ruby-sdk
25
+ ```
26
+
27
+ Add your fork as an origin
28
+ ```bash
29
+ git remote add fork https://github.com/YOUR_GITHUB_USERNAME/ruby-sdk.git
30
+ ```
31
+
32
+ To start working on a new feature or bugfix, create a new branch and start working on it.
33
+
34
+ ```bash
35
+ git checkout -b feat/NAME_OF_FEATURE
36
+ # Make your changes
37
+ git commit -s
38
+ git push fork feat/NAME_OF_FEATURE
39
+ ```
40
+
41
+ Open a pull request against the main ruby-sdk repository.
42
+
43
+ ### Running checks locally
44
+
45
+ #### Unit tests
46
+
47
+ To run unit tests and other checks:
48
+
49
+ ```bash
50
+ bundle exec rake
51
+ ```
52
+
53
+ ### How to Receive Comments
54
+
55
+ * If the PR is not ready for review, please mark it as
56
+ [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/).
57
+ * Make sure all required CI checks are clear.
58
+ * Submit small, focused PRs addressing a single concern/issue.
59
+ * Make sure the PR title reflects the contribution.
60
+ * Write a summary that helps understand the change.
61
+ * Include usage examples in the summary, where applicable.
62
+
63
+ ### How to Get PRs Merged
64
+
65
+ A PR is considered to be **ready to merge** when:
66
+
67
+ * Major feedbacks are resolved.
68
+ * It has been open for review for at least one working day. This gives people
69
+ reasonable time to review.
70
+ * Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day.
71
+ * Urgent fix can take exception as long as it has been actively communicated.
72
+
73
+ Any Maintainer can merge the PR once it is **ready to merge**. Note, that some
74
+ PRs may not be merged immediately if the repo is in the process of a release and
75
+ the maintainers decided to defer the PR to the next release train.
76
+
77
+ If a PR has been stuck (e.g. there are lots of debates and people couldn't agree
78
+ on each other), the owner should try to get people aligned by:
79
+
80
+ * Consolidating the perspectives and putting a summary in the PR. It is
81
+ recommended to add a link into the PR description, which points to a comment
82
+ with a summary in the PR conversation.
83
+ * Tagging subdomain experts (by looking at the change history) in the PR asking
84
+ for suggestion.
85
+ * Reaching out to more people on the [CNCF OpenFeature Slack channel](https://cloud-native.slack.com/archives/C0344AANLA1).
86
+ * Stepping back to see if it makes sense to narrow down the scope of the PR or
87
+ split it up.
88
+ * If none of the above worked and the PR has been stuck for more than 2 weeks,
89
+ the owner should bring it to the OpenFeatures [meeting](README.md#contributing).
90
+
91
+ ## Automated Changelog
92
+
93
+ Each time a release is published the changelogs will be generated automatically using `release-please`.
94
+
95
+ ## Design Choices
96
+
97
+ As with other OpenFeature SDKs, ruby-sdk follows the
98
+ [openfeature-specification](https://github.com/open-feature/spec).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.3.1)
4
+ openfeature-sdk (0.4.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -34,7 +34,8 @@ GEM
34
34
  regexp_parser (2.9.0)
35
35
  reline (0.5.0)
36
36
  io-console (~> 0.5)
37
- rexml (3.2.6)
37
+ rexml (3.3.6)
38
+ strscan
38
39
  rspec (3.12.0)
39
40
  rspec-core (~> 3.12.0)
40
41
  rspec-expectations (~> 3.12.0)
@@ -87,6 +88,7 @@ GEM
87
88
  lint_roller (~> 1.1)
88
89
  rubocop-performance (~> 1.20.2)
89
90
  stringio (3.1.0)
91
+ strscan (3.1.0)
90
92
  unicode-display_width (2.5.0)
91
93
 
92
94
  PLATFORMS
data/README.md CHANGED
@@ -1,24 +1,48 @@
1
- # OpenFeature SDK for Ruby
2
-
3
- [![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1)
4
- [![v0.5.1](https://img.shields.io/static/v1?label=Specification&message=v0.5.1&color=yellow)](https://github.com/open-feature/spec/tree/v0.5.1)
5
- ![Ruby](https://img.shields.io/badge/ruby-%23CC342D.svg?style=for-the-badge&logo=ruby&logoColor=white)
6
- ![Build](https://github.com/open-feature/openfeature-ruby/actions/workflows/main.yml/badge.svg?branch=main)
7
- ![Gem version](https://img.shields.io/gem/v/openfeature-sdk)
8
-
9
- This is the Ruby implementation of [OpenFeature](https://openfeature.dev), a vendor-agnostic abstraction library for evaluating feature flags.
10
-
11
- We support multiple data types for flags (numbers, strings, booleans, objects) as well as hooks, which can alter the lifecycle of a flag evaluation.
12
-
13
- ## Support Matrix
14
-
15
- | Ruby Version | OS |
1
+ <!-- markdownlint-disable MD033 -->
2
+ <!-- x-hide-in-docs-start -->
3
+ <p align="center">
4
+ <picture>
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/white/openfeature-horizontal-white.svg" />
6
+ <img align="center" alt="OpenFeature Logo" src="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg" />
7
+ </picture>
8
+ </p>
9
+
10
+ <h2 align="center">OpenFeature Ruby SDK</h2>
11
+
12
+ <!-- x-hide-in-docs-end -->
13
+ <!-- The 'github-badges' class is used in the docs -->
14
+ <p align="center" class="github-badges">
15
+ <a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
16
+ <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
17
+ </a>
18
+ <!-- x-release-please-start-version -->
19
+
20
+ <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.4.1">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.4.1&color=blue&style=for-the-badge" />
22
+ </a>
23
+
24
+ <!-- x-release-please-end -->
25
+ <br/>
26
+ <a href="https://bestpractices.coreinfrastructure.org/projects/9337">
27
+ <img alt="CII Best Practices" src="https://bestpractices.coreinfrastructure.org/projects/9337/badge" />
28
+ </a>
29
+ </p>
30
+ <!-- x-hide-in-docs-start -->
31
+
32
+ [OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution.
33
+
34
+ <!-- x-hide-in-docs-end -->
35
+ ## 🚀 Quick start
36
+
37
+ ### Requirements
38
+
39
+ | Supported Ruby Version | OS |
16
40
  | ------------ | --------------------- |
17
41
  | Ruby 3.1.4 | Windows, MacOS, Linux |
18
42
  | Ruby 3.2.3 | Windows, MacOS, Linux |
19
43
  | Ruby 3.3.0 | Windows, MacOS, Linux |
20
44
 
21
- ## Installation
45
+ ### Install
22
46
 
23
47
  Install the gem and add to the application's Gemfile by executing:
24
48
 
@@ -32,7 +56,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
32
56
  gem install openfeature-sdk
33
57
  ```
34
58
 
35
- ## Usage
59
+ ### Usage
36
60
 
37
61
  ```ruby
38
62
  require 'open_feature/sdk'
@@ -48,33 +72,120 @@ OpenFeature::SDK.configure do |config|
48
72
  "flag2" => 1
49
73
  }
50
74
  ))
51
- # alternatively, you can bind multiple providers to different domains
52
- config.set_provider(OpenFeature::SDK::Provider::NoOpProvider.new, domain: "legacy_flags")
53
- # you can set a global evaluation context here
54
- config.evaluation_context = OpenFeature::SDK::EvaluationContext.new("host" => "myhost.com")
55
75
  end
56
76
 
57
77
  # Create a client
58
78
  client = OpenFeature::SDK.build_client
59
- # Create a client for a different domain, this will use the provider assigned to that domain
60
- legacy_flag_client = OpenFeature::SDK.build_client(domain: "legacy_flags")
61
- # Evaluation context can be set on a client as well
62
- client_with_context = OpenFeature::SDK.build_client(
63
- evaluation_context: OpenFeature::SDK::EvaluationContext.new("controller_name" => "admin")
64
- )
65
79
 
66
80
  # fetching boolean value feature flag
67
81
  bool_value = client.fetch_boolean_value(flag_key: 'boolean_flag', default_value: false)
68
82
 
83
+ # a details method is also available for more information about the flag evaluation
84
+ # see `ResolutionDetails` for more info
85
+ bool_details = client.fetch_boolean_details(flag_key: 'boolean_flag', default_value: false)
86
+
69
87
  # fetching string value feature flag
70
- string_value = client.fetch_string_value(flag_key: 'string_flag', default_value: false)
88
+ string_value = client.fetch_string_value(flag_key: 'string_flag', default_value: 'default')
71
89
 
72
90
  # fetching number value feature flag
73
91
  float_value = client.fetch_number_value(flag_key: 'number_value', default_value: 1.0)
74
92
  integer_value = client.fetch_number_value(flag_key: 'number_value', default_value: 1)
75
93
 
76
94
  # get an object value
77
- object = client.fetch_object_value(flag_key: 'object_value', default_value: JSON.dump({ name: 'object'}))
95
+ object = client.fetch_object_value(flag_key: 'object_value', default_value: { name: 'object'})
96
+ ```
97
+
98
+ ## 🌟 Features
99
+
100
+ | Status | Features | Description |
101
+ | ------ | --------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
102
+ | ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
103
+ | ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
104
+ | ⚠️ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
105
+ | ❌ | [Logging](#logging) | Integrate with popular logging packages. |
106
+ | ✅ | [Domains](#domains) | Logically bind clients with providers. |
107
+ | ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
108
+ | ⚠️ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
109
+ | ❌ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) |
110
+ | ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
111
+
112
+ <sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
113
+
114
+ ### Providers
115
+
116
+ [Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK.
117
+ Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Ruby) for a complete list of available providers.
118
+ If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself.
119
+
120
+ Once you've added a provider as a dependency, it can be registered with OpenFeature like this:
121
+
122
+ ```ruby
123
+ OpenFeature::SDK.configure do |config|
124
+ # your provider of choice, which will be used as the default provider
125
+ config.set_provider(OpenFeature::SDK::Provider::InMemoryProvider.new(
126
+ {
127
+ "v2_enabled" => true,
128
+ }
129
+ ))
130
+ end
131
+ ```
132
+
133
+ #### Blocking Provider Registration
134
+
135
+ If you need to ensure that a provider is fully initialized before continuing, you can use `set_provider_and_wait`:
136
+
137
+ ```ruby
138
+ # Using the SDK directly
139
+ begin
140
+ OpenFeature::SDK.set_provider_and_wait(my_provider)
141
+ puts "Provider is ready!"
142
+ rescue OpenFeature::SDK::ProviderInitializationError => e
143
+ puts "Provider failed to initialize: #{e.message}"
144
+ puts "Error code: #{e.error_code}"
145
+ puts "Original error: #{e.original_error}"
146
+ end
147
+
148
+ # With custom timeout (default is 30 seconds)
149
+ OpenFeature::SDK.set_provider_and_wait(my_provider, timeout: 60)
150
+
151
+ # Domain-specific provider
152
+ OpenFeature::SDK.set_provider_and_wait(my_provider, domain: "feature-flags")
153
+
154
+ # Via configuration block
155
+ OpenFeature::SDK.configure do |config|
156
+ begin
157
+ config.set_provider_and_wait(my_provider)
158
+ rescue OpenFeature::SDK::ProviderInitializationError => e
159
+ # Handle initialization failure
160
+ end
161
+ end
162
+ ```
163
+
164
+ The `set_provider_and_wait` method:
165
+ - Waits for the provider's `init` method to complete successfully
166
+ - Raises `ProviderInitializationError` with `PROVIDER_FATAL` error code if initialization fails or times out
167
+ - Provides access to the original error, provider instance, and error code for debugging
168
+ - Uses the same thread-safe provider switching as `set_provider`
169
+
170
+ In some situations, it may be beneficial to register multiple providers in the same application.
171
+ This is possible using [domains](#domains), which is covered in more detail below.
172
+
173
+ ### Targeting
174
+
175
+ Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location.
176
+ In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
177
+ If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
178
+
179
+ ```ruby
180
+ OpenFeature::SDK.configure do |config|
181
+ # you can set a global evaluation context here
182
+ config.evaluation_context = OpenFeature::SDK::EvaluationContext.new("host" => "myhost.com")
183
+ end
184
+
185
+ # Evaluation context can be set on a client as well
186
+ client_with_context = OpenFeature::SDK.build_client(
187
+ evaluation_context: OpenFeature::SDK::EvaluationContext.new("controller_name" => "admin")
188
+ )
78
189
 
79
190
  # Invocation evaluation context can also be passed in during flag evaluation.
80
191
  # During flag evaluation, invocation context takes precedence over client context
@@ -86,38 +197,159 @@ bool_value = client.fetch_boolean_value(
86
197
  )
87
198
  ```
88
199
 
89
- For complete documentation, visit: https://openfeature.dev/docs/category/concepts
200
+ ### Hooks
90
201
 
91
- ### Providers
202
+ Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/52) to be worked on.
203
+
204
+ <!-- [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
205
+ Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Ruby) for a complete list of available hooks.
206
+ If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.
207
+
208
+ Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. -->
209
+
210
+ <!-- TODO: code example of setting hooks at all levels -->
211
+
212
+ ### Logging
213
+
214
+ Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/148) to work on.
215
+
216
+ <!-- TODO: talk about logging config and include a code example -->
217
+
218
+ ### Domains
219
+
220
+ Clients can be assigned to a domain. A domain is a logical identifier which can be used to associate clients with a particular provider.
221
+ If a domain has no associated provider, the default provider is used.
222
+
223
+ ```ruby
224
+ OpenFeature::SDK.configure do |config|
225
+ config.set_provider(OpenFeature::SDK::Provider::NoOpProvider.new, domain: "legacy_flags")
226
+ end
92
227
 
93
- Providers are the abstraction layer between OpenFeature and different flag management systems.
228
+ # Create a client for a different domain, this will use the provider assigned to that domain
229
+ legacy_flag_client = OpenFeature::SDK.build_client(domain: "legacy_flags")
230
+ ```
231
+
232
+ ### Eventing
233
+
234
+ Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/51) to be worked on.
235
+
236
+ <!-- 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.
237
+ Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
238
+ Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`.
239
+
240
+ Please refer to the documentation of the provider you're using to see what events are supported. -->
241
+
242
+ <!-- TODO: code example of a PROVIDER_CONFIGURATION_CHANGED event for the client and a PROVIDER_STALE event for the API -->
94
243
 
95
- The `NoOpProvider` is an example of a minimalist provider. The `InMemoryProvider` is a provider that can be initialized with flags and used to store flags in process. For complete documentation on the Provider interface, visit: https://openfeature.dev/specification/sections/providers.
244
+ ### Shutdown
96
245
 
97
- In addition to the `fetch_*` methods, providers can optionally implement lifecycle methods that are invoked when the underlying provider is switched out. For example:
246
+ Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/149) to be worked on.
247
+
248
+ <!-- TODO The OpenFeature API provides a close function to perform a cleanup of all registered providers.
249
+ This should only be called when your application is in the process of shutting down.
250
+
251
+ ```ruby
252
+ class MyProvider
253
+ def shutdown
254
+ # Perform any shutdown/reclamation steps with flag management system here
255
+ # Return value is ignored
256
+ end
257
+ end
258
+ ``` -->
259
+
260
+ ### Transaction Context Propagation
261
+
262
+ Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/150) to be worked on.
263
+
264
+ <!-- Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
265
+ Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the transaction context propagator it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread). -->
266
+
267
+ <!-- TODO: code example for global shutdown -->
268
+
269
+ ## Extending
270
+
271
+ ### Develop a provider
272
+
273
+ To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency.
274
+ This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/ruby-sdk-contrib) available under the OpenFeature organization.
275
+ You’ll then need to write the provider by implementing the `Provider` duck.
98
276
 
99
277
  ```ruby
100
278
  class MyProvider
101
279
  def init
102
280
  # Perform any initialization steps with flag management system here
103
281
  # Return value is ignored
282
+ # **Note** The OpenFeature spec defines a lifecycle method called `initialize` to be called when a new provider is set.
283
+ # To avoid conflicting with the Ruby `initialize` method, this method should be named `init` when creating a provider.
104
284
  end
105
285
 
106
286
  def shutdown
107
287
  # Perform any shutdown/reclamation steps with flag management system here
108
288
  # Return value is ignored
109
289
  end
290
+
291
+ def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
292
+ # Retrieve a boolean value from provider source
293
+ end
294
+
295
+ def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
296
+ # Retrieve a string value from provider source
297
+ end
298
+
299
+ def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
300
+ # Retrieve a numeric value from provider source
301
+ end
302
+
303
+ def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
304
+ # Retrieve a integer value from provider source
305
+ end
306
+
307
+ def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
308
+ # Retrieve a float value from provider source
309
+ end
310
+
311
+ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
312
+ # Retrieve a hash value from provider source
313
+ end
110
314
  end
111
315
  ```
112
316
 
113
- **Note** The OpenFeature spec defines a lifecycle method called `initialize` to be called when a new provider is set. To avoid conflicting with the Ruby `initialize` method, this method should be named `init` when creating a provider.
317
+ > Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs!
318
+
319
+ ### Develop a hook
320
+
321
+ Coming Soon! [Issue available](https://github.com/open-feature/ruby-sdk/issues/52) to be worked on.
322
+
323
+ <!-- To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
324
+ This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/ruby-sdk-contrib) available under the OpenFeature organization.
325
+ Implement your own hook by conforming to the `Hook interface`.
326
+ To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined.
327
+ To avoid defining empty functions, make use of the `UnimplementedHook` struct (which already implements all the empty functions). -->
328
+
329
+ <!-- TODO: code example of hook implementation -->
330
+
331
+ <!-- > Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! -->
332
+
333
+ <!-- x-hide-in-docs-start -->
334
+ ## ⭐️ Support the project
335
+
336
+ - Give this repo a ⭐️!
337
+ - Follow us on social media:
338
+ - Twitter: [@openfeature](https://twitter.com/openfeature)
339
+ - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/)
340
+ - Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1)
341
+ - For more, check out our [community page](https://openfeature.dev/community/)
342
+
343
+ ## 🤝 Contributing
114
344
 
115
- ## Contributing
345
+ Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide.
116
346
 
117
- See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to the OpenFeature project.
347
+ ### Thanks to everyone who has already contributed
118
348
 
119
- Our community meetings are held regularly and open to everyone. Check the [OpenFeature community calendar](https://calendar.google.com/calendar/u/0?cid=MHVhN2kxaGl2NWRoMThiMjd0b2FoNjM2NDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) for specific dates and for the Zoom meeting links.
349
+ <a href="https://github.com/open-feature/ruby-sdk/graphs/contributors">
350
+ <img src="https://contrib.rocks/image?repo=open-feature/ruby-sdk" alt="Pictures of the folks who have contributed to the project" />
351
+ </a>
120
352
 
121
- ## License
122
353
 
123
- [Apache License 2.0](LICENSE)
354
+ Made with [contrib.rocks](https://contrib.rocks).
355
+ <!-- x-hide-in-docs-end -->
@@ -32,7 +32,7 @@ module OpenFeature
32
32
  include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1
33
33
  extend Forwardable
34
34
 
35
- def_delegators :configuration, :provider, :set_provider, :hooks, :evaluation_context
35
+ def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :evaluation_context
36
36
 
37
37
  def configuration
38
38
  @configuration ||= Configuration.new
@@ -5,7 +5,15 @@ module OpenFeature
5
5
  # TODO: Write documentation
6
6
  #
7
7
  class Client
8
- RESULT_TYPE = %i[boolean string number integer float object].freeze
8
+ TYPE_CLASS_MAP = {
9
+ boolean: [TrueClass, FalseClass],
10
+ string: [String],
11
+ number: [Numeric],
12
+ integer: [Integer],
13
+ float: [Float],
14
+ object: [Array, Hash]
15
+ }.freeze
16
+ RESULT_TYPE = TYPE_CLASS_MAP.keys.freeze
9
17
  SUFFIXES = %i[value details].freeze
10
18
 
11
19
  attr_reader :metadata, :evaluation_context
@@ -26,14 +34,39 @@ module OpenFeature
26
34
  # result = @provider.fetch_boolean_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
27
35
  # end
28
36
  def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context: nil)
29
- built_context = EvaluationContextBuilder.new.call(api_context: OpenFeature::SDK.evaluation_context, client_context: self.evaluation_context, invocation_context: evaluation_context)
30
- resolution_details = @provider.fetch_#{result_type}_value(flag_key:, default_value:, evaluation_context: built_context)
31
- evaluation_details = EvaluationDetails.new(flag_key:, resolution_details:)
37
+ evaluation_details = fetch_details(type: :#{result_type}, flag_key:, default_value:, evaluation_context:)
32
38
  #{"evaluation_details.value" if suffix == :value}
33
39
  end
34
40
  RUBY
35
41
  end
36
42
  end
43
+
44
+ private
45
+
46
+ def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil)
47
+ validate_default_value_type(type, default_value)
48
+
49
+ built_context = EvaluationContextBuilder.new.call(api_context: OpenFeature::SDK.evaluation_context, client_context: self.evaluation_context, invocation_context: evaluation_context)
50
+
51
+ resolution_details = @provider.send(:"fetch_#{type}_value", flag_key:, default_value:, evaluation_context: built_context)
52
+
53
+ if TYPE_CLASS_MAP[type].none? { |klass| resolution_details.value.is_a?(klass) }
54
+ resolution_details.value = default_value
55
+ resolution_details.error_code = Provider::ErrorCode::TYPE_MISMATCH
56
+ resolution_details.reason = Provider::Reason::ERROR
57
+ end
58
+
59
+ EvaluationDetails.new(flag_key:, resolution_details:)
60
+ end
61
+
62
+ def validate_default_value_type(type, default_value)
63
+ expected_classes = TYPE_CLASS_MAP[type]
64
+ unless expected_classes.any? { |klass| default_value.is_a?(klass) }
65
+ expected_types = expected_classes.map(&:name).join(" or ")
66
+ actual_type = default_value.class.name
67
+ raise ArgumentError, "Default value for #{type} must be #{expected_types}, got #{actual_type}"
68
+ end
69
+ end
37
70
  end
38
71
  end
39
72
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
3
4
  require_relative "api"
5
+ require_relative "provider_initialization_error"
4
6
 
5
7
  module OpenFeature
6
8
  module SDK
@@ -16,6 +18,7 @@ module OpenFeature
16
18
  def initialize
17
19
  @hooks = []
18
20
  @providers = {}
21
+ @provider_mutex = Mutex.new
19
22
  end
20
23
 
21
24
  def provider(domain: nil)
@@ -27,11 +30,59 @@ module OpenFeature
27
30
  # 2. On the new provider, call `init`.
28
31
  # 3. Finally, set the internal provider to the new provider
29
32
  def set_provider(provider, domain: nil)
30
- @providers[domain].shutdown if @providers[domain].respond_to?(:shutdown)
33
+ @provider_mutex.synchronize do
34
+ @providers[domain].shutdown if @providers[domain].respond_to?(:shutdown)
35
+ provider.init if provider.respond_to?(:init)
36
+ new_providers = @providers.dup
37
+ new_providers[domain] = provider
38
+ @providers = new_providers
39
+ end
40
+ end
41
+
42
+ # Sets a provider and waits for the initialization to complete or fail.
43
+ # This method ensures the provider is ready (or in error state) before returning.
44
+ #
45
+ # @param provider [Object] the provider to set
46
+ # @param domain [String, nil] the domain for the provider (optional)
47
+ # @param timeout [Integer] maximum time to wait for initialization in seconds (default: 30)
48
+ # @raise [ProviderInitializationError] if the provider fails to initialize or times out
49
+ def set_provider_and_wait(provider, domain: nil, timeout: 30)
50
+ @provider_mutex.synchronize do
51
+ old_provider = @providers[domain]
52
+
53
+ # Shutdown old provider (ignore errors)
54
+ begin
55
+ old_provider.shutdown if old_provider.respond_to?(:shutdown)
56
+ rescue
57
+ # Ignore shutdown errors and continue with provider initialization
58
+ end
31
59
 
32
- provider.init if provider.respond_to?(:init)
60
+ begin
61
+ # Initialize new provider with timeout
62
+ if provider.respond_to?(:init)
63
+ Timeout.timeout(timeout) do
64
+ provider.init
65
+ end
66
+ end
33
67
 
34
- @providers[domain] = provider
68
+ # Set the new provider
69
+ new_providers = @providers.dup
70
+ new_providers[domain] = provider
71
+ @providers = new_providers
72
+ rescue Timeout::Error => e
73
+ raise ProviderInitializationError.new(
74
+ "Provider initialization timed out after #{timeout} seconds",
75
+ provider:,
76
+ original_error: e
77
+ )
78
+ rescue => e
79
+ raise ProviderInitializationError.new(
80
+ "Provider initialization failed: #{e.message}",
81
+ provider:,
82
+ original_error: e
83
+ )
84
+ end
85
+ end
35
86
  end
36
87
  end
37
88
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module SDK
5
+ module Hooks
6
+ class Hints < DelegateClass(Hash)
7
+ ALLOWED_TYPES = [String, Symbol, Numeric, TrueClass, FalseClass, Time, Hash, Array].freeze
8
+
9
+ def initialize(hash = {})
10
+ hash.each do |key, value|
11
+ assert_allowed_key(key)
12
+ assert_allowed_value(value)
13
+ end
14
+ @hash = hash.dup
15
+ super(@hash)
16
+ freeze
17
+ end
18
+
19
+ private
20
+
21
+ def assert_allowed_key(key)
22
+ raise ArgumentError, "Only String or Symbol are allowed as keys." unless key.is_a?(String) || key.is_a?(Symbol)
23
+ end
24
+
25
+ def assert_allowed_value(value)
26
+ allowed_type = ALLOWED_TYPES.any? { |t| value.is_a?(t) }
27
+ raise ArgumentError, "Only #{ALLOWED_TYPES.join(", ")} are allowed as values." unless allowed_type
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -2,13 +2,14 @@ module OpenFeature
2
2
  module SDK
3
3
  module Provider
4
4
  module ErrorCode
5
- PROVIDER_NOT_READY = "Provider Not Ready"
6
- FLAG_NOT_FOUND = "Flag Not Found"
7
- PARSE_ERROR = "Parse Error"
8
- TYPE_MISMATCH = "Type Mismatch"
9
- TARGETING_KEY_MISSING = "Targeting Key Missing"
10
- INVALID_CONTEXT = "Invalid Context"
11
- GENERAL = "General"
5
+ PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
6
+ FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
7
+ PARSE_ERROR = "PARSE_ERROR"
8
+ TYPE_MISMATCH = "TYPE_MISMATCH"
9
+ TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
10
+ INVALID_CONTEXT = "INVALID_CONTEXT"
11
+ PROVIDER_FATAL = "PROVIDER_FATAL"
12
+ GENERAL = "GENERAL"
12
13
  end
13
14
  end
14
15
  end
@@ -26,45 +26,41 @@ module OpenFeature
26
26
  end
27
27
 
28
28
  def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
29
- fetch_value(allowed_classes: [TrueClass, FalseClass], flag_key:, default_value:, evaluation_context:)
29
+ fetch_value(flag_key:, default_value:, evaluation_context:)
30
30
  end
31
31
 
32
32
  def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
33
- fetch_value(allowed_classes: [String], flag_key:, default_value:, evaluation_context:)
33
+ fetch_value(flag_key:, default_value:, evaluation_context:)
34
34
  end
35
35
 
36
36
  def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
37
- fetch_value(allowed_classes: [Numeric], flag_key:, default_value:, evaluation_context:)
37
+ fetch_value(flag_key:, default_value:, evaluation_context:)
38
38
  end
39
39
 
40
40
  def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
41
- fetch_value(allowed_classes: [Integer], flag_key:, default_value:, evaluation_context:)
41
+ fetch_value(flag_key:, default_value:, evaluation_context:)
42
42
  end
43
43
 
44
44
  def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
45
- fetch_value(allowed_classes: [Float], flag_key:, default_value:, evaluation_context:)
45
+ fetch_value(flag_key:, default_value:, evaluation_context:)
46
46
  end
47
47
 
48
48
  def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
49
- fetch_value(allowed_classes: [Array, Hash], flag_key:, default_value:, evaluation_context:)
49
+ fetch_value(flag_key:, default_value:, evaluation_context:)
50
50
  end
51
51
 
52
52
  private
53
53
 
54
54
  attr_reader :flags
55
55
 
56
- def fetch_value(allowed_classes:, flag_key:, default_value:, evaluation_context:)
56
+ def fetch_value(flag_key:, default_value:, evaluation_context:)
57
57
  value = flags[flag_key]
58
58
 
59
59
  if value.nil?
60
60
  return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
61
61
  end
62
62
 
63
- if allowed_classes.any? { |klass| value.is_a?(klass) }
64
- ResolutionDetails.new(value:, reason: Reason::STATIC)
65
- else
66
- ResolutionDetails.new(value: default_value, error_code: ErrorCode::TYPE_MISMATCH, reason: Reason::ERROR)
67
- end
63
+ ResolutionDetails.new(value:, reason: Reason::STATIC)
68
64
  end
69
65
  end
70
66
  end
@@ -2,15 +2,15 @@ module OpenFeature
2
2
  module SDK
3
3
  module Provider
4
4
  module Reason
5
- STATIC = "Static"
6
- DEFAULT = "Default"
7
- TARGETING_MATCH = "Targeting Match"
8
- SPLIT = "Split"
9
- CACHED = "Cached"
10
- DISABLED = "Disabled"
11
- UNKNOWN = "Unknown"
12
- STALE = "Stale"
13
- ERROR = "Error"
5
+ STATIC = "STATIC"
6
+ DEFAULT = "DEFAULT"
7
+ TARGETING_MATCH = "TARGETING_MATCH"
8
+ SPLIT = "SPLIT"
9
+ CACHED = "CACHED"
10
+ DISABLED = "DISABLED"
11
+ UNKNOWN = "UNKNOWN"
12
+ STALE = "STALE"
13
+ ERROR = "ERROR"
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider/error_code"
4
+
5
+ module OpenFeature
6
+ module SDK
7
+ # Exception raised when a provider fails to initialize during setProviderAndWait
8
+ #
9
+ # This exception provides access to both the original error that caused the
10
+ # initialization failure and the provider instance that failed to initialize.
11
+ class ProviderInitializationError < StandardError
12
+ # @return [Object] the provider that failed to initialize
13
+ attr_reader :provider
14
+
15
+ # @return [Exception] the original error that caused the initialization failure
16
+ attr_reader :original_error
17
+
18
+ # @return [String] the OpenFeature error code
19
+ attr_reader :error_code
20
+
21
+ # @param message [String] the error message
22
+ # @param provider [Object] the provider that failed to initialize
23
+ # @param original_error [Exception] the original error that caused the failure
24
+ # @param error_code [String] the OpenFeature error code (defaults to PROVIDER_FATAL)
25
+ def initialize(message, provider: nil, original_error: nil, error_code: Provider::ErrorCode::PROVIDER_FATAL)
26
+ super(message)
27
+ @provider = provider
28
+ @original_error = original_error
29
+ @error_code = error_code
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.1"
6
6
  end
7
7
  end
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "bootstrap-sha": "9bcc2bcbbcbce2d750ef1e0f67081fff4bb9ff79",
3
3
  "release-type": "ruby",
4
+ "signoff": "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>",
4
5
  "packages": {
5
6
  ".": {
6
7
  "monorepo-tags": false,
@@ -9,7 +10,10 @@
9
10
  "bump-minor-pre-major": true,
10
11
  "bump-patch-for-minor-pre-major": true,
11
12
  "package-name": "openfeature-sdk",
12
- "version-file": "lib/open_feature/sdk/version.rb"
13
+ "version-file": "lib/open_feature/sdk/version.rb",
14
+ "extra-files": [
15
+ "README.md"
16
+ ]
13
17
  }
14
18
  }
15
19
  }
data/renovate.json CHANGED
@@ -1,17 +1,9 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
3
  "extends": [
4
- "config:base"
4
+ "config:recommended",
5
+ ":automergeStableNonMajor",
6
+ "npm:unpublishSafe"
5
7
  ],
6
- "packageRules": [
7
- {
8
- "matchUpdateTypes": ["minor", "patch"],
9
- "matchCurrentVersion": "!/^0/",
10
- "automerge": true
11
- },
12
- {
13
- "matchManagers": ["github-actions"],
14
- "automerge": true
15
- }
16
- ]
8
+ "semanticCommits": "enabled"
17
9
  }
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openfeature-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-04-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: debug
@@ -139,6 +138,7 @@ files:
139
138
  - CHANGELOG.md
140
139
  - CODEOWNERS
141
140
  - CODE_OF_CONDUCT.md
141
+ - CONTRIBUTING.md
142
142
  - Gemfile
143
143
  - Gemfile.lock
144
144
  - LICENSE
@@ -152,6 +152,7 @@ files:
152
152
  - lib/open_feature/sdk/evaluation_context.rb
153
153
  - lib/open_feature/sdk/evaluation_context_builder.rb
154
154
  - lib/open_feature/sdk/evaluation_details.rb
155
+ - lib/open_feature/sdk/hooks/hints.rb
155
156
  - lib/open_feature/sdk/provider.rb
156
157
  - lib/open_feature/sdk/provider/error_code.rb
157
158
  - lib/open_feature/sdk/provider/in_memory_provider.rb
@@ -159,6 +160,7 @@ files:
159
160
  - lib/open_feature/sdk/provider/provider_metadata.rb
160
161
  - lib/open_feature/sdk/provider/reason.rb
161
162
  - lib/open_feature/sdk/provider/resolution_details.rb
163
+ - lib/open_feature/sdk/provider_initialization_error.rb
162
164
  - lib/open_feature/sdk/version.rb
163
165
  - release-please-config.json
164
166
  - renovate.json
@@ -171,7 +173,6 @@ metadata:
171
173
  changelog_uri: https://github.com/open-feature/openfeature-ruby/blob/main/CHANGELOG.md
172
174
  bug_tracker_uri: https://github.com/open-feature/openfeature-ruby/issues
173
175
  documentation_uri: https://github.com/open-feature/openfeature-ruby/README.md
174
- post_install_message:
175
176
  rdoc_options: []
176
177
  require_paths:
177
178
  - lib
@@ -186,8 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
187
  - !ruby/object:Gem::Version
187
188
  version: '0'
188
189
  requirements: []
189
- rubygems_version: 3.5.3
190
- signing_key:
190
+ rubygems_version: 3.6.9
191
191
  specification_version: 4
192
192
  summary: OpenFeature SDK for Ruby
193
193
  test_files: []