Conversation
bd46bc5 to
89a428b
Compare
Signed-off-by: Jose Colella <jose.colella@gusto.com>
- Add spec to reflect compliance with https://docs.openfeature.dev/docs/specification/sections/flag-evaluation\#11-api-initialization-and-configuration references #7 Signed-off-by: Jose Colella <jose.colella@gusto.com>
- Including resolvers that handle logic for individual types references #7 Signed-off-by: Jose Colella <jose.colella@gusto.com>
Signed-off-by: Jose Colella <jose.colella@gusto.com>
- Add NoOpProvider references #7 Signed-off-by: Jose Colella <jose.colella@gusto.com>
references #7 Signed-off-by: Jose Colella <jose.colella@gusto.com>
Signed-off-by: Jose Colella <jose.colella@gusto.com>
Signed-off-by: Jose Colella <jose.colella@gusto.com>
Signed-off-by: Jose Colella <jose.colella@gusto.com>
89a428b to
96cc4c1
Compare
| class NumberResolver | ||
| extend T::Sig | ||
|
|
||
| Number = T.type_alias { T.any(Integer, Float) } |
There was a problem hiding this comment.
So we alias both floats and ints to this type - is it then possible for application authors to interpret this Number either way?
Both precise ints and fractional numbers / proportions (eg .5) are important for feature flag use cases. As long as an author can interpret the return value to either of these I think this alias is a good idea.
| const :reason, T.nilable(String) | ||
| const :variant, T.nilable(String) | ||
| const :error_code, T.nilable(FeatureFlagErrorCode) | ||
| const :error_message, T.nilable(String) |
There was a problem hiding this comment.
Is this different than EvaluationDetails? If so, it may be missing a value field.
There was a problem hiding this comment.
I wonder whether this was meant to be EvaluationContext . I say this, because it looks like being used as a parameter for the "fetcher" functions.
| class EvaluationDetails < ResolutionDetails | ||
| const :flag_key, String | ||
| end |
There was a problem hiding this comment.
See my comment on FeatureFlagEvaluationDetails.
| # Within the Metadata structure you have access to the following attribute reader: | ||
| # | ||
| # * <tt>name</tt> - Allows you to specify name of the Metadata structure | ||
| # | ||
| # * <tt>version</tt> - Allows you to specify version of the Metadata structure |
There was a problem hiding this comment.
Within the Metadata structure you have access to the following attribute reader...
Nitpick: avoid language like "you" and "I". Instead use passive voice:
Within the Metadata structure, the following attribute readers are available...
|
|
||
| class ResolutionDetails < T::Struct | ||
| const :value, T.any(String, T::Boolean, Integer, Float, T::Hash[String, T.untyped], T::Array[T.untyped]) | ||
| const :reason, T.nilable(T.any(ResolutionReason, String)) |
There was a problem hiding this comment.
This seems to mean that a reason can be a string, OR a pre-defined enum, which is good. Arbitrary reasons should be allowed.
There was a problem hiding this comment.
yep the T.any is equivalent to the Typescript union
| enums do | ||
| DEFAULT = new("DEFAULT") | ||
| TARGETING_MATCH = new("TARGETING_MATCH") | ||
| SPLIT = new("SPLIT") | ||
| DISABLED = new("DISABLED") | ||
| UNKNOWN = new("UNKNOWN") | ||
| ERROR = new("ERROR") | ||
| end |
There was a problem hiding this comment.
It may be worth adding in-line doc here if that's possible, you can lift it right from the spec.
| enums do | ||
| PROVIDER_NOT_READY = new("PROVIDER_NOT_READY") | ||
| FLAG_NOT_FOUND = new("FLAG_NOT_FOUND") | ||
| PARSE_ERROR = new("PARSE_ERROR") | ||
| TYPE_MISMATCH = new("TYPE_MISMATCH") | ||
| TARGETING_KEY_MISSING = new("TARGETING_KEY_MISSING") | ||
| INVALID_CONTEXT = new("INVALID_CONTEXT") | ||
| GENERAL = new("GENERAL") | ||
| end |
There was a problem hiding this comment.
It may be worth adding in-line doc here if that's possible, you can lift it right from the spec.
| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } | ||
| spec.require_paths = ["lib"] | ||
|
|
||
| spec.add_dependency "sorbet-runtime", "~> 0.5.10539" |
There was a problem hiding this comment.
We want to minimize runtime deps, but based on the fact that sorbet seems to be extremely useful and extremely popular, I think this is an acceptable runtime dependency. You may want to specify the version requirement less specifically.
There was a problem hiding this comment.
Way up in this PR I actually doubted whether including sorbet is that useful. Ruby comes with RBS in 3.1 onwards. Also, looking at most feature flag SDKs out there they do not seem to bring sorbet either.
There was a problem hiding this comment.
The reason I chose sorbet is for type safety at runtime.
There was a problem hiding this comment.
In #32 I left out sorbet and just implemented it with it
Gemfile.lock
Outdated
| remote: . | ||
| specs: | ||
| openfeature-sdk (0.0.1) | ||
| sorbet-runtime |
There was a problem hiding this comment.
Making this optional.
technicalpickles
left a comment
There was a problem hiding this comment.
Did a partial commit-by-commit review. Will take another pass in the next few days!
| require_relative "../spec_helper" | ||
|
|
||
| require_relative "../../lib/openfeature/sdk/provider/no_op_provider" | ||
| require_relative "../../lib/openfeature/sdk/configuration" | ||
| require_relative "../../lib/openfeature/sdk" | ||
| require_relative "../../lib/openfeature/sdk/metadata" |
There was a problem hiding this comment.
You can save yourself a bunch of requires in specs by moving the require of the main library to spec_helper.rb.
You can also ave yourself having to do ../.. by adding the lib to the RSpec load path. You should be able to add -I lib to .rspec, ie: https://stackoverflow.com/a/43307662/135364
| before do | ||
| subject | ||
| end |
There was a problem hiding this comment.
I'd suggest removing this line, and change the subject sections to be before.
I'm a little hesitant about using subject to do a thing, rather than it being the thing that is being tested. Practically speaking, OpenFeature::SDK would be the real subject, since you are making expectations about it. You could use described_class in that case.
| end | ||
| end | ||
|
|
||
| context "Requirement 1.1.4" do |
There was a problem hiding this comment.
Maybe use plain english to describe this instead of the requirement? THis is included in failures.
It'd still be good to reference, so maybe link to it in a comment?
| def_delegator :@boolean_resolver, :fetch_value, :fetch_boolean_value | ||
| def_delegator :@boolean_resolver, :fetch_detailed_value, :fetch_boolean_details | ||
|
|
||
| def_delegator :@number_resolver, :fetch_value, :fetch_number_value | ||
| def_delegator :@number_resolver, :fetch_detailed_value, :fetch_number_details | ||
|
|
||
| def_delegator :@string_resolver, :fetch_value, :fetch_string_value | ||
| def_delegator :@string_resolver, :fetch_detailed_value, :fetch_string_details | ||
|
|
||
| def_delegator :@object_resolver, :fetch_value, :fetch_object_value | ||
| def_delegator :@object_resolver, :fetch_detailed_value, :fetch_object_details |
There was a problem hiding this comment.
You might be able to leverage method_missing for some of these 🤔 I'm not sure if that would end up being more/less clear and/or code though.
There was a problem hiding this comment.
I think delegators are clearer (and probably also more performant) than method_missing.
Also, it might make debugging easier. def_delegator actually creates a method, I don't think that method_missing does that.
In any case I was wondering whether we need this overhead of having different resolvers. I was expecting one resolver with several methods (basically what the spec calls a provider).
Though maybe later in the PR i will find out why we would want the possibility to "Mix And Match".
There was a problem hiding this comment.
it might make debugging easier
Implementingrespond_to?andrespond_to_missing?should mitigate some of this.
I don't think that method_missing does that.
It doesn't own it's own, but can be implemented in a way that defines methods as they go.
| module Resolver | ||
| # TODO: Write documentation | ||
| # | ||
| class BooleanResolver |
There was a problem hiding this comment.
If there's a lot of duplication between the resolvers, could we use a superclass, module, etc to reduce it? Might also be able to dynamically make a class using Class.new or something.
I expect sorbet may make it tough though.
I'm also wondering if we need the resolvers, or if it could be simpler having something in client?
mschoenlaub
left a comment
There was a problem hiding this comment.
Cool stuff :-)
However, I have to main points for feedback. One relates to using RBS instead of sorbet. Most feature flag SDKs out there don't come with sorbet and RBS is available for Ruby 3.1. It also would be way less clutter in the code.
The other main point relateds to the resolvers. I failed to recognize why we need another layer between providers and the SDK. But I might be missing something there!
|
|
||
| Layout/LineLength: | ||
| Max: 120 | ||
| Enabled: false |
There was a problem hiding this comment.
Do we really want to disable it entirely?
| rubocop-ast (1.23.0) | ||
| parser (>= 3.1.1.0) | ||
| ruby-progressbar (1.11.0) | ||
| sorbet (0.5.10539) |
There was a problem hiding this comment.
Do we really need sorbet? Ruby 3.1 brings us RBS and sorbet is a an addiitional dependency. I just ask, because OpenFeature SDKs were initially meant to come with a lightweight (e.g. quasi empty) set of dependencies.
Additionally, common practice for gems seems to be to not include a Gemfile.lock in the repo.
Now, I realize that I might be quite oldschool here, and later on in the review I might find out that you actually have renovate or something else updating it, so feel free to ignore my rant :D
There was a problem hiding this comment.
Not including a Gemfile would also help resolve the release issue we ran into @josecolella.
| extend T::Sig | ||
| extend Forwardable | ||
|
|
||
| def_delegator :@configuration, :provider |
There was a problem hiding this comment.
I love Ruby for this kind of stuff :)
| end | ||
| def build_client(name: nil, version: nil, context: nil) | ||
| client_options = Metadata.new(name: name, version: version) | ||
| provider = Provider::NoOpProvider.new if provider.nil? |
There was a problem hiding this comment.
Do other SDKs do that? It seems fine to me, but it also means that there is now a coupling to a specific provider.
There was a problem hiding this comment.
@mschoenlaub With this I'm following the specification point of
The evaluation API allows for the evaluation of feature flag values, independent of any flag control plane or vendor. In the absence of a provider the evaluation API uses the "No-op provider", which simply returns the supplied default flag value.
|
|
||
| require "sorbet-runtime" | ||
|
|
||
| class FeatureFlagErrorCode < T::Enum |
There was a problem hiding this comment.
Do we have other ErrorCodes? If not, we could remove the FeatureFlag prefix. That would improve readability.
| const :reason, T.nilable(String) | ||
| const :variant, T.nilable(String) | ||
| const :error_code, T.nilable(FeatureFlagErrorCode) | ||
| const :error_message, T.nilable(String) |
There was a problem hiding this comment.
I wonder whether this was meant to be EvaluationContext . I say this, because it looks like being used as a parameter for the "fetcher" functions.
|
|
||
| class FlagEvaluationOptions < T::Struct | ||
| const :hooks, T.nilable(T::Array[Hook]) | ||
| const :hook_hints, T.nilable(T::Hash[String, T.untyped]) |
There was a problem hiding this comment.
Hints do not support arbitrary types. Only boolean | string | number | datetime | structure according to spec.
| private | ||
|
|
||
| def correct_type?(value) | ||
| result = JSON.parse(value) |
There was a problem hiding this comment.
I noticed that the spec says Structured data, presented however is idiomatic in the implementation language, such as JSON or YAML.. However, I would argue that the idiomatic representation in Ruby would be a Hash.
After all, neitehr JSON nor YAML should be forced upon the user of the SDK IMHO.
| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } | ||
| spec.require_paths = ["lib"] | ||
|
|
||
| spec.add_dependency "sorbet-runtime", "~> 0.5.10539" |
There was a problem hiding this comment.
Way up in this PR I actually doubted whether including sorbet is that useful. Ruby comes with RBS in 3.1 onwards. Also, looking at most feature flag SDKs out there they do not seem to bring sorbet either.
| root = true | ||
|
|
||
| [*] | ||
| end_of_line = lf | ||
| indent_size = 4 | ||
| indent_style = space | ||
| insert_final_newline = true | ||
| tab_width = 8 | ||
| trim_trailing_whitespace = true | ||
|
|
||
| [*.bat] | ||
| end_of_line = crlf | ||
|
|
||
| [*.gemspec] | ||
| indent_size = 2 | ||
|
|
||
| [*.rb] | ||
| indent_size = 2 | ||
|
|
||
| [*.yml] | ||
| indent_size = 2 | ||
|
|
||
| [{*[Mm]akefile*,*.mak,*.mk,depend}] | ||
| indent_style = tab | ||
|
|
||
| [enc/*] | ||
| indent_size = 2 | ||
|
|
||
| [reg*.[ch]] | ||
| indent_size = 2 |
There was a problem hiding this comment.
Let's pull this and the Rubocop rules out as a separate PR.
This PR
This PR creates the initial implementation of the OpenFeature specification
Related Issues
References #7
Notes
Follow-up Tasks
How to test
bundle exec rspec