launchdarkly-openfeature-server-sdk 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +10 -0
- data/CODEOWNERS +2 -0
- data/CONTRIBUTING.md +58 -0
- data/Gemfile +6 -1
- data/PROVENANCE.md +43 -0
- data/SECURITY.md +5 -0
- data/docs/Makefile +26 -0
- data/docs/index.md +7 -0
- data/launchdarkly-openfeature-server-sdk.gemspec +6 -0
- data/lib/ldclient-openfeature/impl/context_converter.rb +147 -0
- data/lib/ldclient-openfeature/impl/details_converter.rb +86 -0
- data/lib/ldclient-openfeature/impl.rb +13 -0
- data/lib/ldclient-openfeature/provider.rb +127 -0
- data/lib/ldclient-openfeature/version.rb +1 -1
- data/lib/ldclient-openfeature.rb +3 -0
- data/release-please-config.json +12 -0
- metadata +47 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1246acd88a6e54ba166c00106b8262d5fdb0bd7a8e0ae68b5bf066eae7d883c7
|
4
|
+
data.tar.gz: 62e9efe0c482e5bb4650d5fe9079fe702cb156ecbd5d1fa8b7932a483efcb0a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c403999bb9c8a6bb5a8428ad40ab226174fa5fbee201331e2ca412d84bf1626e73af7fa53f4b4edf6a8e31ee78789e5403ca2749a441520e04c0b752e051936
|
7
|
+
data.tar.gz: ee6272c867a1a1c6f2f1e490b7873e63da1211197035bdd44f4e33013ebea898e26e6489ebfb89ad5e947f90ba222e8f0a6b0842cafc34e64e494953ba132fcc
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.1.0 (2024-08-05)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* Add initial implementation of provider ([#1](https://github.com/launchdarkly/openfeature-ruby-server/issues/1)) ([7550e14](https://github.com/launchdarkly/openfeature-ruby-server/commit/7550e14d94f6a70b0087ba9801dfbfe93c1b374d))
|
9
|
+
|
10
|
+
## Changelog
|
data/CODEOWNERS
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Contributing to the LaunchDarkly OpenFeature provider for the Server-side SDK for Ruby
|
2
|
+
|
3
|
+
LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this provider.
|
4
|
+
|
5
|
+
## Submitting bug reports and feature requests
|
6
|
+
|
7
|
+
The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/openfeature-ruby-server-sdk/issues) in the provider repository. Bug reports and feature requests specific to this provider should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days.
|
8
|
+
|
9
|
+
## Submitting pull requests
|
10
|
+
|
11
|
+
We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days.
|
12
|
+
|
13
|
+
## Build instructions
|
14
|
+
|
15
|
+
### Prerequisites
|
16
|
+
|
17
|
+
This SDK is built with [Bundler](https://bundler.io/). To install Bundler, run `gem install bundler`. You might need `sudo` to execute the command successfully.
|
18
|
+
|
19
|
+
To install the runtime dependencies:
|
20
|
+
|
21
|
+
```
|
22
|
+
bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
### Testing
|
26
|
+
|
27
|
+
To run all unit tests:
|
28
|
+
|
29
|
+
```
|
30
|
+
bundle exec rspec spec
|
31
|
+
```
|
32
|
+
|
33
|
+
### Building documentation
|
34
|
+
|
35
|
+
Documentation is built automatically with YARD for each release. To build the documentation locally:
|
36
|
+
|
37
|
+
```
|
38
|
+
cd docs
|
39
|
+
make
|
40
|
+
```
|
41
|
+
|
42
|
+
The output will appear in `docs/build/html`.
|
43
|
+
|
44
|
+
## Code organization
|
45
|
+
|
46
|
+
The SDK's namespacing convention is as follows:
|
47
|
+
|
48
|
+
* `LaunchDarkly::OpenFeature`: This namespace contains the most commonly used classes and methods in the SDK, such as `Provider`.
|
49
|
+
|
50
|
+
A special case is the namespace `LaunchDarkly::OpenFeature::Impl`, and any namespaces within it. Everything under `Impl` is considered a private implementation detail: all files there are excluded from the generated documentation, and are considered subject to change at any time and not supported for direct use by application developers. We do this because Ruby's scope/visibility system is somewhat limited compared to other languages: a method can be `private` or `protected` within a class, but there is no way to make it visible to other classes in the SDK yet invisible to code outside of the SDK, and there is similarly no way to hide a class.
|
51
|
+
|
52
|
+
So, if there is a class whose existence is entirely an implementation detail, it should be in `Impl`. Similarly, classes that are _not_ in `Impl` must not expose any public members that are not meant to be part of the supported public API. This is important because of our guarantee of backward compatibility for all public APIs within a major version: we want to be able to change our implementation details to suit the needs of the code, without worrying about breaking a customer's code. Due to how the language works, we can't actually prevent an application developer from referencing those classes in their code, but this convention makes it clear that such use is discouraged and unsupported.
|
53
|
+
|
54
|
+
## Documenting types and methods
|
55
|
+
|
56
|
+
All classes and public methods outside of `LaunchDarkly::OpenFeature::Impl` should have documentation comments. These are used to build the API documentation that is published at https://launchdarkly.github.io/openfeature-ruby-server-sdk/ and https://www.rubydoc.info/gems/launchdarkly-openfeature-server-sdk. The documentation generator is YARD; see https://yardoc.org/ for the comment format it uses.
|
57
|
+
|
58
|
+
Please try to make the style and terminology in documentation comments consistent with other documentation comments in the SDK. Also, if a class or method is being added that has an equivalent in other SDKs, and if we have described it in a consistent away in those other SDKs, please reuse the text whenever possible (with adjustments for anything language-specific) rather than writing new text.
|
data/Gemfile
CHANGED
@@ -2,9 +2,14 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
# Specify your gem's dependencies in openfeature-
|
5
|
+
# Specify your gem's dependencies in launchdarkly-openfeature-server-sdk.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
8
|
gem "rake", "~> 13.0"
|
9
9
|
|
10
10
|
gem "rspec", "~> 3.0"
|
11
|
+
|
12
|
+
gem "rubocop", "~> 1.21"
|
13
|
+
gem "rubocop-performance", "~> 1.15"
|
14
|
+
gem "rubocop-rake", "~> 0.6"
|
15
|
+
gem "rubocop-rspec", "~> 2.27"
|
data/PROVENANCE.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
## Verifying SDK build provenance with the SLSA framework
|
2
|
+
|
3
|
+
LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages.
|
4
|
+
|
5
|
+
As part of [SLSA requirements for level 3 compliance](https://slsa.dev/spec/v1.0/requirements), LaunchDarkly publishes provenance about our SDK package builds using [GitHub's generic SLSA3 provenance generator](https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md#generation-of-slsa3-provenance-for-arbitrary-projects) for distribution alongside our packages. These attestations are available for download from the GitHub release page for the release version under Assets > `multiple-provenance.intoto.jsonl`.
|
6
|
+
|
7
|
+
To verify SLSA provenance attestations, we recommend using [slsa-verifier](https://github.com/slsa-framework/slsa-verifier). Example usage for verifying SDK packages is included below:
|
8
|
+
|
9
|
+
<!-- x-release-please-start-version -->
|
10
|
+
```
|
11
|
+
# Set the version of the SDK to verify
|
12
|
+
SDK_VERSION=0.1.0
|
13
|
+
```
|
14
|
+
<!-- x-release-please-end -->
|
15
|
+
|
16
|
+
```
|
17
|
+
# Download gem
|
18
|
+
$ gem fetch launchdarkly-openfeature-server-sdk -v $SDK_VERSION
|
19
|
+
|
20
|
+
# Download provenance from Github release
|
21
|
+
$ curl --location -O \
|
22
|
+
https://github.com/launchdarkly/openfeature-ruby-server/releases/download/${SDK_VERSION}/launchdarkly-openfeature-server-sdk-${SDK_VERSION}.gem.intoto.jsonl
|
23
|
+
|
24
|
+
# Run slsa-verifier to verify provenance against package artifacts
|
25
|
+
$ slsa-verifier verify-artifact \
|
26
|
+
--provenance-path launchdarkly-openfeature-server-sdk-${SDK_VERSION}.gem.intoto.jsonl \
|
27
|
+
--source-uri github.com/launchdarkly/openfeature-ruby-server \
|
28
|
+
launchdarkly-openfeature-server-sdk-${SDK_VERSION}.gem
|
29
|
+
```
|
30
|
+
|
31
|
+
Below is a sample of expected output.
|
32
|
+
|
33
|
+
```
|
34
|
+
Verified signature against tlog entry index 78214752 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77ab941c118ef7e0b2d656b962a0d670c6ac91cfa37d07b7b121ae560b00a978ecf
|
35
|
+
Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.7.0" at commit f43b3ad834103fdc282652efbfe4963e8dfa737b
|
36
|
+
Verifying artifact launchdarkly-server-sdk-8.3.0.gem: PASSED
|
37
|
+
|
38
|
+
PASSED: Verified SLSA provenance
|
39
|
+
```
|
40
|
+
|
41
|
+
Alternatively, to verify the provenance manually, the SLSA framework specifies [recommendations for verifying build artifacts](https://slsa.dev/spec/v1.0/verifying-artifacts) in their documentation.
|
42
|
+
|
43
|
+
**Note:** These instructions do not apply when building our SDKs from source.
|
data/SECURITY.md
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
# Reporting and Fixing Security Issues
|
2
|
+
|
3
|
+
Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty.
|
4
|
+
|
5
|
+
Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors.
|
data/docs/Makefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
ifeq ($(LD_RELEASE_VERSION),)
|
2
|
+
TITLE=LaunchDarkly Ruby OTEL Library
|
3
|
+
else
|
4
|
+
TITLE=LaunchDarkly Ruby OTEL Library ($(LD_RELEASE_VERSION))
|
5
|
+
endif
|
6
|
+
|
7
|
+
.PHONY: dependencies html
|
8
|
+
|
9
|
+
html: dependencies
|
10
|
+
rm -rf ./build
|
11
|
+
cd .. && yard doc \
|
12
|
+
-o docs/build/html \
|
13
|
+
--title "$(TITLE)" \
|
14
|
+
--no-private \
|
15
|
+
--markup markdown \
|
16
|
+
--embed-mixins \
|
17
|
+
-r docs/index.md \
|
18
|
+
lib/*.rb \
|
19
|
+
lib/**/*.rb \
|
20
|
+
lib/**/**/*.rb \
|
21
|
+
lib/**/**/**/*.rb
|
22
|
+
rm -f build/html/frames.html
|
23
|
+
|
24
|
+
dependencies:
|
25
|
+
gem install --conservative yard
|
26
|
+
gem install --conservative redcarpet # provides Markdown formatting
|
data/docs/index.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# LaunchDarkly OpenFeature Server-Side Provider
|
2
|
+
|
3
|
+
This generated API documentation lists all types and methods in the SDK.
|
4
|
+
|
5
|
+
The API documentation for the most recent release is hosted on [GitHub Pages](https://launchdarkly.github.io/openfeature-ruby-server). API documentation for current and past releases is hosted on [RubyDoc.info](https://www.rubydoc.info/gems/launchdarkly-openfeature-server-sdk).
|
6
|
+
|
7
|
+
Source code and readme: [GitHub](https://github.com/launchdarkly/openfeature-ruby-server)
|
@@ -27,4 +27,10 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.bindir = "exe"
|
28
28
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
29
|
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_runtime_dependency "launchdarkly-server-sdk", "~> 8.4.0"
|
32
|
+
spec.add_runtime_dependency "openfeature-sdk", "~> 0.4.0"
|
33
|
+
|
34
|
+
# For more information and examples about making a new gem, check out our
|
35
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
30
36
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ldclient-rb'
|
4
|
+
require 'open_feature/sdk'
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
module OpenFeature
|
8
|
+
module Impl
|
9
|
+
class EvaluationContextConverter
|
10
|
+
#
|
11
|
+
# @param logger [Logger]
|
12
|
+
#
|
13
|
+
def initialize(logger)
|
14
|
+
@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Create an LDContext from an EvaluationContext.
|
19
|
+
#
|
20
|
+
# A context will always be created, but the created context may be invalid. Log messages will be written to
|
21
|
+
# indicate the source of the problem.
|
22
|
+
#
|
23
|
+
# @param context [OpenFeature::SDK::EvaluationContext]
|
24
|
+
#
|
25
|
+
# @return [LaunchDarkly::LDContext]
|
26
|
+
#
|
27
|
+
def to_ld_context(context)
|
28
|
+
kind = context.field('kind')
|
29
|
+
|
30
|
+
return build_multi_context(context) if kind == "multi"
|
31
|
+
|
32
|
+
unless kind.nil? || kind.is_a?(String)
|
33
|
+
@logger.warn("'kind' was set to a non-string value; defaulting to user")
|
34
|
+
kind = 'user'
|
35
|
+
end
|
36
|
+
|
37
|
+
targeting_key = context.targeting_key
|
38
|
+
key = context.field('key')
|
39
|
+
targeting_key = get_targeting_key(targeting_key, key)
|
40
|
+
|
41
|
+
kind ||= 'user'
|
42
|
+
build_single_context(context.fields, kind, targeting_key)
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# @param targeting_key [String, nil]
|
47
|
+
# @param key [any]
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
#
|
51
|
+
private def get_targeting_key(targeting_key, key)
|
52
|
+
# The targeting key may be set but empty. So we want to treat an empty string as a not defined one. Later it
|
53
|
+
# could become null, so we will need to check that.
|
54
|
+
if !targeting_key.nil? && targeting_key != "" && key.is_a?(String)
|
55
|
+
# There is both a targeting key and a key. It will work, but probably is not intentional.
|
56
|
+
@logger.warn("EvaluationContext contained both a 'key' and 'targeting_key'.")
|
57
|
+
end
|
58
|
+
|
59
|
+
@logger.warn("A non-string 'key' attribute was provided.") unless key.nil? || key.is_a?(String)
|
60
|
+
|
61
|
+
targeting_key ||= key unless key.nil? || !key.is_a?(String)
|
62
|
+
|
63
|
+
if targeting_key.nil? || targeting_key == "" || !targeting_key.is_a?(String)
|
64
|
+
@logger.error("The EvaluationContext must contain either a 'targeting_key' or a 'key' and the type must be a string.")
|
65
|
+
end
|
66
|
+
|
67
|
+
targeting_key || ""
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# @param context [OpenFeature::SDK::EvaluationContext]
|
72
|
+
#
|
73
|
+
# @return [LaunchDarkly::LDContext]
|
74
|
+
#
|
75
|
+
private def build_multi_context(context)
|
76
|
+
contexts = []
|
77
|
+
|
78
|
+
context.fields.each do |kind, attributes|
|
79
|
+
next if kind == 'kind'
|
80
|
+
|
81
|
+
unless attributes.is_a?(Hash)
|
82
|
+
@logger.warn("Top level attributes in a multi-kind context should be dictionaries")
|
83
|
+
next
|
84
|
+
end
|
85
|
+
|
86
|
+
key = attributes.fetch(:key, nil)
|
87
|
+
targeting_key = attributes.fetch(:targeting_key, nil)
|
88
|
+
|
89
|
+
next unless targeting_key.nil? || targeting_key.is_a?(String)
|
90
|
+
|
91
|
+
targeting_key = get_targeting_key(targeting_key, key)
|
92
|
+
single_context = build_single_context(attributes, kind, targeting_key)
|
93
|
+
|
94
|
+
contexts << single_context
|
95
|
+
end
|
96
|
+
|
97
|
+
LaunchDarkly::LDContext.create_multi(contexts)
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# @param attributes [Hash]
|
102
|
+
# @param kind [String]
|
103
|
+
# @param key [String]
|
104
|
+
#
|
105
|
+
# @return [LaunchDarkly::LDContext]
|
106
|
+
#
|
107
|
+
private def build_single_context(attributes, kind, key)
|
108
|
+
context = { kind: kind, key: key }
|
109
|
+
|
110
|
+
attributes.each do |k, v|
|
111
|
+
next if %w[key targeting_key kind].include? k
|
112
|
+
|
113
|
+
if k == 'name' && v.is_a?(String)
|
114
|
+
context[:name] = v
|
115
|
+
elsif k == 'name'
|
116
|
+
@logger.error("The attribute 'name' must be a string")
|
117
|
+
next
|
118
|
+
elsif k == 'anonymous' && [true, false].include?(v)
|
119
|
+
context[:anonymous] = v
|
120
|
+
elsif k == 'anonymous'
|
121
|
+
@logger.error("The attribute 'anonymous' must be a boolean")
|
122
|
+
next
|
123
|
+
elsif k == 'privateAttributes' && v.is_a?(Array)
|
124
|
+
private_attributes = []
|
125
|
+
v.each do |private_attribute|
|
126
|
+
unless private_attribute.is_a?(String)
|
127
|
+
@logger.error("'privateAttributes' must be an array of only string values")
|
128
|
+
next
|
129
|
+
end
|
130
|
+
|
131
|
+
private_attributes << private_attribute
|
132
|
+
end
|
133
|
+
|
134
|
+
context[:_meta] = { privateAttributes: private_attributes } unless private_attributes.empty?
|
135
|
+
elsif k == 'privateAttributes'
|
136
|
+
@logger.error("The attribute 'privateAttributes' must be an array")
|
137
|
+
else
|
138
|
+
context[k.to_sym] = v
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
LaunchDarkly::LDContext.create(context)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ldclient-rb'
|
4
|
+
require 'open_feature/sdk'
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
module OpenFeature
|
8
|
+
module Impl
|
9
|
+
class ResolutionDetailsConverter
|
10
|
+
#
|
11
|
+
# @param detail [LaunchDarkly::EvaluationDetail]
|
12
|
+
#
|
13
|
+
# @return [OpenFeature::SDK::ResolutionDetails]
|
14
|
+
#
|
15
|
+
def to_resolution_details(detail)
|
16
|
+
value = detail.value
|
17
|
+
is_default = detail.variation_index.nil?
|
18
|
+
variation_index = detail.variation_index
|
19
|
+
|
20
|
+
reason = detail.reason
|
21
|
+
reason_kind = reason.kind
|
22
|
+
|
23
|
+
openfeature_reason = kind_to_reason(reason_kind)
|
24
|
+
|
25
|
+
openfeature_error_code = nil
|
26
|
+
if reason_kind == LaunchDarkly::EvaluationReason::ERROR
|
27
|
+
openfeature_error_code = error_kind_to_code(reason.error_kind)
|
28
|
+
end
|
29
|
+
|
30
|
+
openfeature_variant = nil
|
31
|
+
openfeature_variant = variation_index.to_s unless is_default
|
32
|
+
|
33
|
+
::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
34
|
+
value: value,
|
35
|
+
error_code: openfeature_error_code,
|
36
|
+
error_message: nil,
|
37
|
+
reason: openfeature_reason,
|
38
|
+
variant: openfeature_variant
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# @param kind [Symbol]
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
#
|
47
|
+
private def kind_to_reason(kind)
|
48
|
+
case kind
|
49
|
+
when LaunchDarkly::EvaluationReason::OFF
|
50
|
+
::OpenFeature::SDK::Provider::Reason::DISABLED
|
51
|
+
when LaunchDarkly::EvaluationReason::TARGET_MATCH
|
52
|
+
::OpenFeature::SDK::Provider::Reason::TARGETING_MATCH
|
53
|
+
when LaunchDarkly::EvaluationReason::ERROR
|
54
|
+
::OpenFeature::SDK::Provider::Reason::ERROR
|
55
|
+
else
|
56
|
+
# NOTE: FALLTHROUGH, RULE_MATCH, PREREQUISITE_FAILED intentionally
|
57
|
+
kind.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# @param error_kind [Symbol]
|
63
|
+
#
|
64
|
+
# @return [String]
|
65
|
+
#
|
66
|
+
private def error_kind_to_code(error_kind)
|
67
|
+
return ::OpenFeature::SDK::Provider::ErrorCode::GENERAL if error_kind.nil?
|
68
|
+
|
69
|
+
case error_kind
|
70
|
+
when LaunchDarkly::EvaluationReason::ERROR_CLIENT_NOT_READY
|
71
|
+
::OpenFeature::SDK::Provider::ErrorCode::PROVIDER_NOT_READY
|
72
|
+
when LaunchDarkly::EvaluationReason::ERROR_FLAG_NOT_FOUND
|
73
|
+
::OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND
|
74
|
+
when LaunchDarkly::EvaluationReason::ERROR_MALFORMED_FLAG
|
75
|
+
::OpenFeature::SDK::Provider::ErrorCode::PARSE_ERROR
|
76
|
+
when LaunchDarkly::EvaluationReason::ERROR_USER_NOT_SPECIFIED
|
77
|
+
::OpenFeature::SDK::Provider::ErrorCode::TARGETING_KEY_MISSING
|
78
|
+
else
|
79
|
+
# NOTE: EXCEPTION_ERROR intentionally omitted
|
80
|
+
::OpenFeature::SDK::Provider::ErrorCode::GENERAL
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
module OpenFeature
|
3
|
+
#
|
4
|
+
# Internal implementation classes. Everything in this module should be considered unsupported
|
5
|
+
# and subject to change.
|
6
|
+
#
|
7
|
+
# @private
|
8
|
+
#
|
9
|
+
module Impl
|
10
|
+
# code is in ldclient-openfeature/impl/
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ldclient-rb'
|
4
|
+
require 'open_feature/sdk'
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
module OpenFeature
|
8
|
+
class Provider
|
9
|
+
#
|
10
|
+
# Retrieve metadata information describing this provider.
|
11
|
+
#
|
12
|
+
# @return [::OpenFeature::SDK::Provider::ProviderMetadata]
|
13
|
+
#
|
14
|
+
attr_reader :metadata
|
15
|
+
|
16
|
+
#
|
17
|
+
# Access the underlying LaunchDarky client instance backing this provider.
|
18
|
+
#
|
19
|
+
# This is useful for accessing additional functionality not exposed by the provider.
|
20
|
+
#
|
21
|
+
# @return [LaunchDarkly::LDClient]
|
22
|
+
#
|
23
|
+
attr_reader :client
|
24
|
+
|
25
|
+
NUMERIC_TYPES = %i[integer float number].freeze
|
26
|
+
private_constant :NUMERIC_TYPES
|
27
|
+
|
28
|
+
#
|
29
|
+
# @param sdk_key [String]
|
30
|
+
# @param config [LaunchDarkly::Config]
|
31
|
+
# @param wait_for_seconds [Float]
|
32
|
+
#
|
33
|
+
def initialize(sdk_key, config = LaunchDarkly::Config.default, wait_for_seconds = 5)
|
34
|
+
@client = LaunchDarkly::LDClient.new(sdk_key, config, wait_for_seconds)
|
35
|
+
|
36
|
+
@context_converter = Impl::EvaluationContextConverter.new(config.logger)
|
37
|
+
@details_converter = Impl::ResolutionDetailsConverter.new
|
38
|
+
|
39
|
+
@metadata = ::OpenFeature::SDK::Provider::ProviderMetadata.new(name: "launchdarkly-openfeature-server").freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
43
|
+
resolve_value(:boolean, flag_key, default_value, evaluation_context)
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
47
|
+
resolve_value(:string, flag_key, default_value, evaluation_context)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
51
|
+
resolve_value(:number, flag_key, default_value, evaluation_context)
|
52
|
+
end
|
53
|
+
|
54
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
55
|
+
resolve_value(:integer, flag_key, default_value, evaluation_context)
|
56
|
+
end
|
57
|
+
|
58
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
59
|
+
resolve_value(:float, flag_key, default_value, evaluation_context)
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
63
|
+
resolve_value(:object, flag_key, default_value, evaluation_context)
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# @param flag_type [Symbol]
|
68
|
+
# @param flag_key [String]
|
69
|
+
# @param default_value [any]
|
70
|
+
# @param evaluation_context [::OpenFeature::SDK::EvaluationContext, nil]
|
71
|
+
#
|
72
|
+
# @return [::OpenFeature::SDK::Provider::ResolutionDetails]
|
73
|
+
#
|
74
|
+
private def resolve_value(flag_type, flag_key, default_value, evaluation_context)
|
75
|
+
if evaluation_context.nil?
|
76
|
+
return ::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
77
|
+
value: default_value,
|
78
|
+
reason: ::OpenFeature::SDK::Provider::Reason::ERROR,
|
79
|
+
error_code: ::OpenFeature::SDK::Provider::ErrorCode::TARGETING_KEY_MISSING
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
ld_context = @context_converter.to_ld_context(evaluation_context)
|
84
|
+
evaluation_detail = @client.variation_detail(flag_key, ld_context, default_value)
|
85
|
+
|
86
|
+
if flag_type == :boolean && ![true, false].include?(evaluation_detail.value)
|
87
|
+
return mismatched_type_details(default_value)
|
88
|
+
elsif flag_type == :string && !evaluation_detail.value.is_a?(String)
|
89
|
+
return mismatched_type_details(default_value)
|
90
|
+
elsif NUMERIC_TYPES.include?(flag_type) && !evaluation_detail.value.is_a?(Numeric)
|
91
|
+
return mismatched_type_details(default_value)
|
92
|
+
elsif flag_type == :object && !evaluation_detail.value.is_a?(Hash) && !evaluation_detail.value.is_a?(Array)
|
93
|
+
return mismatched_type_details(default_value)
|
94
|
+
end
|
95
|
+
|
96
|
+
if flag_type == :integer
|
97
|
+
evaluation_detail = LaunchDarkly::EvaluationDetail.new(
|
98
|
+
evaluation_detail.value.to_i,
|
99
|
+
evaluation_detail.variation_index,
|
100
|
+
evaluation_detail.reason
|
101
|
+
)
|
102
|
+
elsif flag_type == :float
|
103
|
+
evaluation_detail = LaunchDarkly::EvaluationDetail.new(
|
104
|
+
evaluation_detail.value.to_f,
|
105
|
+
evaluation_detail.variation_index,
|
106
|
+
evaluation_detail.reason
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
@details_converter.to_resolution_details(evaluation_detail)
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# @param default_value [any]
|
115
|
+
#
|
116
|
+
# @return [::OpenFeature::SDK::Provider::ResolutionDetails]
|
117
|
+
#
|
118
|
+
private def mismatched_type_details(default_value)
|
119
|
+
::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
120
|
+
value: default_value,
|
121
|
+
reason: ::OpenFeature::SDK::Provider::Reason::ERROR,
|
122
|
+
error_code: ::OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/ldclient-openfeature.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "ldclient-openfeature/impl/context_converter"
|
4
|
+
require_relative "ldclient-openfeature/impl/details_converter"
|
5
|
+
require_relative "ldclient-openfeature/provider"
|
3
6
|
require_relative "ldclient-openfeature/version"
|
4
7
|
|
5
8
|
require "logger"
|
@@ -0,0 +1,12 @@
|
|
1
|
+
{
|
2
|
+
"packages": {
|
3
|
+
".": {
|
4
|
+
"release-type": "ruby",
|
5
|
+
"bump-minor-pre-major": true,
|
6
|
+
"versioning": "default",
|
7
|
+
"include-component-in-tag": false,
|
8
|
+
"include-v-in-tag": false,
|
9
|
+
"extra-files": ["PROVENANCE.md", "lib/ldclient-openfeature/version.rb"]
|
10
|
+
}
|
11
|
+
}
|
12
|
+
}
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: launchdarkly-openfeature-server-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
12
|
-
dependencies:
|
11
|
+
date: 2024-08-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: launchdarkly-server-sdk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 8.4.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 8.4.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: openfeature-sdk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.4.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.4.0
|
13
41
|
description: A LaunchDarkly provider for use with the OpenFeature SDK
|
14
42
|
email:
|
15
43
|
- team@launchdarkly.com
|
@@ -20,20 +48,32 @@ files:
|
|
20
48
|
- ".release-please-manifest.json"
|
21
49
|
- ".rspec"
|
22
50
|
- ".rubocop.yml"
|
51
|
+
- CHANGELOG.md
|
52
|
+
- CODEOWNERS
|
53
|
+
- CONTRIBUTING.md
|
23
54
|
- Gemfile
|
24
55
|
- LICENSE.txt
|
56
|
+
- PROVENANCE.md
|
25
57
|
- README.md
|
26
58
|
- Rakefile
|
59
|
+
- SECURITY.md
|
60
|
+
- docs/Makefile
|
61
|
+
- docs/index.md
|
27
62
|
- launchdarkly-openfeature-server-sdk.gemspec
|
28
63
|
- lib/ldclient-openfeature.rb
|
64
|
+
- lib/ldclient-openfeature/impl.rb
|
65
|
+
- lib/ldclient-openfeature/impl/context_converter.rb
|
66
|
+
- lib/ldclient-openfeature/impl/details_converter.rb
|
67
|
+
- lib/ldclient-openfeature/provider.rb
|
29
68
|
- lib/ldclient-openfeature/version.rb
|
69
|
+
- release-please-config.json
|
30
70
|
homepage: https://github.com/launchdarkly/openfeature-ruby-server
|
31
71
|
licenses: []
|
32
72
|
metadata:
|
33
73
|
homepage_uri: https://github.com/launchdarkly/openfeature-ruby-server
|
34
74
|
source_code_uri: https://github.com/launchdarkly/openfeature-ruby-server
|
35
75
|
changelog_uri: https://github.com/launchdarkly/openfeature-ruby-server/blob/main/CHANGELOG.md
|
36
|
-
post_install_message:
|
76
|
+
post_install_message:
|
37
77
|
rdoc_options: []
|
38
78
|
require_paths:
|
39
79
|
- lib
|
@@ -48,8 +88,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
88
|
- !ruby/object:Gem::Version
|
49
89
|
version: '0'
|
50
90
|
requirements: []
|
51
|
-
rubygems_version: 3.3.
|
52
|
-
signing_key:
|
91
|
+
rubygems_version: 3.3.27
|
92
|
+
signing_key:
|
53
93
|
specification_version: 4
|
54
94
|
summary: LaunchDarkly OpenFeature Server SDK
|
55
95
|
test_files: []
|