Sorbet-aware OpenFeature Ruby Implementation
NOTE This implementation has been deprecated in favor of the official Ruby SDK. We recommend switching to that. In the future, we hope to add RBS and Tapioca DSL generation support to the official library.
OpenFeature is an open standard for vendor-agnostic feature flagging. Sorbet is a type-checker for Ruby, built by Stripe. Sorbet provides powerful runtime utilities to achieve things traditionally not possible with Ruby, such as interfaces, immutable structures and enums. This makes it a very good option when defining specifications.
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.
Current OpenFeature specification target version: 0.5.2
Current supported Ruby versions: 3.1.X
, 3.2.X
Support for Evaluation Context and Hooks is not complete.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add openfeature-sdk-sorbet
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install openfeature-sdk-sorbet
Usage
require "open_feature_sorbet"
# Configure global API properties
OpenFeature.set_provider(OpenFeatureSorbet::NoOpProvider.new)
OpenFeature.set_evaluation_context(OpenFeatureSorbet::EvaluationContext.new(fields: { "globally" => "available" }))
OpenFeature.add_hooks([OpenFeatureSorbet::Hook.new]) # experimental, not fully supported
client = OpenFeature.create_client(evaluation_context: OpenFeatureSorbet::EvaluationContext.new(fields: { "client" => "available" }))
# Fetch boolean value
# Also methods available for String, Number, Integer, Float and Structure (Hash)
bool_value = client.fetch_boolean_value(flag_key: "my_toggle", default_value: false) # => (true or false)
# Sorbet sprinkles in type safety
bool_value = client.fetch_boolean_value(flag_key: "my_toggle", default_value: "bad!") # => raises TypeError from Sorbet, invalid default value
# Additional evaluation context can be provided during invocation
number_value = client.fetch_number_value(flag_key: "my_toggle", default_value: 1, context: OpenFeatureSorbet::EvaluationContext.new(fields: { "only_this_call_site" => 10 })) # => merges client and global context
# Fetch structure evaluation details
structure_evaluation_details = client.fetch_structure_details(flag_key: "my_structure", default_value: { "a" => "fallback" }) # => EvaluationDetails(value: Hash, flag_key: "my_structure", ...)
Note on Structure
The OpenFeature specification defines Structure as a potential return type. 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.
Evaluation Context
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.
Provider Abstract Class
By default, this implementation sets the provider to the OpenFeatureSorbet::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).
This gem also provides OpenFeatureSorbet::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:
provider = OpenFeatureSorbet::MultipleSourceProvider.new(
providers: [
CustomProvider.new,
OpenFeatureSorbet::NoOpProvider.new
]
)
OpenFeature.set_provider(provider)
Implementing Custom Providers
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:
class JsonFileFlagProvider < OpenFeatureSorbet::Provider
extend T::Sig
sig { void }
def initialize
super(OpenFeatureSorbet::ProviderStatus::NotReady)
end
def init(context)
@file = File.open(context.file || "flags.json")
@status = OpenFeatureSorbet::ProviderStatus::Ready
end
sig { overridable.void }
def shutdown
@file.close
end
sig { override.returns(OpenFeatureSorbet::ProviderMetadata) }
def metadata
OpenFeatureSorbet::ProviderMetadata.new(name: "Json File Flag Provider")
end
sig { override.returns(T::Array[Hook]) }
def hooks
[]
end
sig do
override
.params(
flag_key: String,
default_value: T::Boolean,
context: T.nilable(EvaluationContext)
)
.returns(OpenFeatureSorbet::ResolutionDetails[T::Boolean])
end
def resolve_boolean_value(flag_key:, default_value:, context: nil)
file_input = JSON.parse(File.read("flags.rb"))
value = file_input.fetch("flag_key", default_value)
OpenFeatureSorbet::ResolutionDetails.new(
value: value,
# ... other optional fields
)
end
# ... other resolver methods
end
By inheriting from the OpenFeatureSorbet::Provider
class, Sorbet will indicate what methods it's expecting and what their type signatures should be.
A note on initialize
versus init
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
.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake
to run Rubocop and the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/maxveldink/openfeature-sdk-sorbet. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in this project's codebase, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
Sponsorships
I love creating in the open. If you find this or any other maxveld.ink content useful, please consider sponsoring me on GitHub.