confidence-openfeature-provider 0.1.0
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
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 263d2c0dec4c14ba879db7f5d83c35119fdb53dc6798f6e1ce670d86b39fd704
|
|
4
|
+
data.tar.gz: 2b4c07c13b16992a554fbf15e4ae71bad4a6b5f4ffe3d3378fd99311f45de60b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5b0552399f0eb550c0eb3bf7838e9cdec8b5c9b843c34e06a722609f038c91af13e082e36fd7b11a1d8575cef2d0ca73618ef81c0abbd63493758c42b1fa8fbc
|
|
7
|
+
data.tar.gz: cd99ca08f2f0b49178e5e7e74ab008375b36dd4bcb40458aab53bb3af3d546464b0df5b5f151e5ecb0ebbebf5daeb5e2ea16a5cb9d8d89504cbd8f761fc628a7
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
require "json"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "net/http"
|
|
8
|
+
require "net/https"
|
|
9
|
+
|
|
10
|
+
module Confidence
|
|
11
|
+
module OpenFeature
|
|
12
|
+
class Region
|
|
13
|
+
attr_reader :uri
|
|
14
|
+
|
|
15
|
+
def initialize(uri)
|
|
16
|
+
@uri = uri
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
EU = new("https://resolver.eu.confidence.dev/v1")
|
|
20
|
+
US = new("https://resolver.us.confidence.dev/v1")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class APIClient
|
|
24
|
+
def initialize(client_secret:, region: Region::EU)
|
|
25
|
+
uri = URI.parse(region.uri)
|
|
26
|
+
@client_secret = client_secret
|
|
27
|
+
@agent = Net::HTTP.new(uri.host, uri.port)
|
|
28
|
+
@agent.use_ssl = uri.scheme == "https"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve_one(flag:, context: {}, apply: true)
|
|
32
|
+
result = resolve(flags: [flag], context: context, apply: apply)
|
|
33
|
+
if result.empty?
|
|
34
|
+
raise FlagNotFoundError.new("No active flag '#{flag}' found was found")
|
|
35
|
+
end
|
|
36
|
+
result = result[0]
|
|
37
|
+
if result.flag != flag
|
|
38
|
+
raise FlagNotFoundError.new("Unexpected flag '#{flag}' from remote")
|
|
39
|
+
end
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve(flags: [], context: {}, apply: true)
|
|
44
|
+
result = post_json("/v1/flags:resolve", {
|
|
45
|
+
clientSecret: @client_secret,
|
|
46
|
+
evaluationContext: context || {},
|
|
47
|
+
apply: apply,
|
|
48
|
+
flags: flags,
|
|
49
|
+
sdk: {id: "SDK_ID_PYTHON_PROVIDER", version: VERSION}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
resolved_flags = result["resolvedFlags"] || []
|
|
53
|
+
resolved_flags.map do |flag|
|
|
54
|
+
ResolvedFlag.new(
|
|
55
|
+
flag: flag["flag"],
|
|
56
|
+
variant: nil_if_empty(flag["variant"]),
|
|
57
|
+
value: nil_if_empty(flag["value"])
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def post_json(path, body)
|
|
65
|
+
headers = {"Content-Type" => "application/json"}
|
|
66
|
+
request = Net::HTTP::Post.new(path, headers)
|
|
67
|
+
request.body = JSON.dump(body)
|
|
68
|
+
response = @agent.request(request)
|
|
69
|
+
|
|
70
|
+
code = response.code.to_i
|
|
71
|
+
if code != 200
|
|
72
|
+
raise APIError.new("flags:resolve HTTP #{response.code} #{response.message}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
JSON.parse(response.body)
|
|
77
|
+
rescue JSON::ParserError => ex
|
|
78
|
+
raise APIError.new("flags:resolve malformed JSON: #{ex}")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def nil_if_empty(value)
|
|
83
|
+
(value.nil? || value == "") ? nil : value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
ResolvedFlag = Struct.new(:flag, :variant, :value, keyword_init: true) do
|
|
88
|
+
def empty?
|
|
89
|
+
variant.nil? || value.nil?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if __FILE__ == $0
|
|
96
|
+
require "optparse"
|
|
97
|
+
context = {}
|
|
98
|
+
secret = nil
|
|
99
|
+
OptionParser.new do |opts|
|
|
100
|
+
opts.on("--secret=VALUE") do |v|
|
|
101
|
+
secret = v
|
|
102
|
+
end
|
|
103
|
+
opts.on("--context=VALUE") do |v|
|
|
104
|
+
context.merge!(JSON.parse(v))
|
|
105
|
+
end
|
|
106
|
+
opts.on("--targeting_key=VALUE") do |v|
|
|
107
|
+
context["targeting_key"] = v
|
|
108
|
+
end
|
|
109
|
+
end.parse!
|
|
110
|
+
|
|
111
|
+
client = Confidence::OpenFeature::APIClient.new(client_secret: secret)
|
|
112
|
+
client.resolve(flags: ARGV, context: context).each do |flag|
|
|
113
|
+
puts flag.to_h
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Confidence
|
|
4
|
+
module OpenFeature
|
|
5
|
+
class BaseError < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class APIError < BaseError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class FlagNotFoundError < BaseError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class TypeMismatchError < BaseError
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "api_client"
|
|
4
|
+
require "open_feature/sdk"
|
|
5
|
+
|
|
6
|
+
module Confidence
|
|
7
|
+
module OpenFeature
|
|
8
|
+
# See NoOpProvider in the OpenFeature Ruby SDK for the interface
|
|
9
|
+
class Provider
|
|
10
|
+
attr_reader :metadata
|
|
11
|
+
|
|
12
|
+
# Error_code and error_message seemingly not used by OpenFeature SDK.
|
|
13
|
+
# Including here for compatibility.
|
|
14
|
+
ResolutionDetails = Struct.new(
|
|
15
|
+
:value, :reason, :variant, :error_code, :error_message,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def initialize(api_client:, apply_on_resolve: true)
|
|
20
|
+
@api_client = api_client
|
|
21
|
+
@apply_on_resolve = apply_on_resolve
|
|
22
|
+
@metadata = ::OpenFeature::SDK::Provider::ProviderMetadata.new(name: "Confidence").freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
26
|
+
evaluate(
|
|
27
|
+
flag_key: flag_key,
|
|
28
|
+
default_value: default_value,
|
|
29
|
+
evaluation_context: evaluation_context,
|
|
30
|
+
validator: lambda { |v| (v === true || v === false) }
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
35
|
+
evaluate(
|
|
36
|
+
flag_key: flag_key,
|
|
37
|
+
default_value: default_value,
|
|
38
|
+
evaluation_context: evaluation_context,
|
|
39
|
+
validator: lambda { |v| v.is_a?(String) }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
44
|
+
evaluate(
|
|
45
|
+
flag_key: flag_key,
|
|
46
|
+
default_value: default_value,
|
|
47
|
+
evaluation_context: evaluation_context,
|
|
48
|
+
validator: lambda { |v| v.is_a?(Numeric) }
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
53
|
+
evaluate(
|
|
54
|
+
flag_key: flag_key,
|
|
55
|
+
default_value: default_value,
|
|
56
|
+
evaluation_context: evaluation_context
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def evaluate(flag_key:, default_value:, evaluation_context: nil, validator: nil)
|
|
63
|
+
parts = flag_key.split(".")
|
|
64
|
+
flag_id = parts.shift
|
|
65
|
+
value_path = parts
|
|
66
|
+
context = context_hash(evaluation_context)
|
|
67
|
+
|
|
68
|
+
result = @api_client.resolve_one(
|
|
69
|
+
flag: "flags/#{flag_id}",
|
|
70
|
+
context: context,
|
|
71
|
+
apply: @apply_on_resolve
|
|
72
|
+
)
|
|
73
|
+
if result.empty?
|
|
74
|
+
return ResolutionDetails.new(
|
|
75
|
+
value: default_value,
|
|
76
|
+
reason: "DEFAULT"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
value = value_at_path(flag_key, result.value, value_path)
|
|
81
|
+
if !value.nil? && validator && !validator.call(value)
|
|
82
|
+
raise TypeMismatchError.new("value did not match expected type")
|
|
83
|
+
end
|
|
84
|
+
value = default_value if value.nil?
|
|
85
|
+
|
|
86
|
+
ResolutionDetails.new(
|
|
87
|
+
value: value,
|
|
88
|
+
variant: Confidence::OpenFeature.parse_variant(result.variant),
|
|
89
|
+
reason: "TARGETING_MATCH"
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def value_at_path(flag, value, path)
|
|
94
|
+
return value if path.empty?
|
|
95
|
+
the_value = value
|
|
96
|
+
path.each do |key|
|
|
97
|
+
if the_value.is_a?(Hash) && the_value.has_key?(key)
|
|
98
|
+
the_value = the_value[key]
|
|
99
|
+
else
|
|
100
|
+
raise TypeMismatchError.new("#{flag}: invalid path: #{path.join(".")}")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
the_value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def context_hash(evaluation_context)
|
|
107
|
+
return {} if evaluation_context.nil?
|
|
108
|
+
|
|
109
|
+
# In SDK 0.4+, EvaluationContext stores all fields in a hash
|
|
110
|
+
# targeting_key is a special field that can be accessed via .targeting_key
|
|
111
|
+
evaluation_context.fields.dup
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.parse_variant(value)
|
|
116
|
+
components = value.split("/", 4)
|
|
117
|
+
if components.length != 4 || components[0] != "flags" || components[2] != "variants"
|
|
118
|
+
raise ArgumentError.new("Invalid variant name: #{value}")
|
|
119
|
+
end
|
|
120
|
+
components[3]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: confidence-openfeature-provider
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Confidence Team
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: openfeature-sdk
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.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: 0.4.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: openssl
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 3.12.0
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 3.12.0
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: standard
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: standard-performance
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: simplecov
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 0.22.0
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 0.22.0
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: simplecov-cobertura
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 2.1.0
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: 2.1.0
|
|
125
|
+
description:
|
|
126
|
+
email:
|
|
127
|
+
- TBD@TBD.com
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- lib/confidence/openfeature.rb
|
|
133
|
+
- lib/confidence/openfeature/api_client.rb
|
|
134
|
+
- lib/confidence/openfeature/errors.rb
|
|
135
|
+
- lib/confidence/openfeature/provider.rb
|
|
136
|
+
- lib/confidence/openfeature/version.rb
|
|
137
|
+
homepage: https://github.com/spotify/confidence-openfeature-provider-ruby
|
|
138
|
+
licenses:
|
|
139
|
+
- Apache-2.0'
|
|
140
|
+
metadata:
|
|
141
|
+
homepage_uri: https://github.com/spotify/confidence-openfeature-provider-ruby
|
|
142
|
+
source_code_uri: https://github.com/spotify/confidence-openfeature-provider-ruby
|
|
143
|
+
changelog_uri: https://github.com/spotify/confidence-openfeature-provider-ruby/blob/main/CHANGELOG.md
|
|
144
|
+
bug_tracker_uri: https://github.com/spotify/confidence-openfeature-provider-ruby/issues
|
|
145
|
+
documentation_uri: https://github.com/spotify/confidence-openfeature-provider-ruby/README.md
|
|
146
|
+
post_install_message:
|
|
147
|
+
rdoc_options: []
|
|
148
|
+
require_paths:
|
|
149
|
+
- lib
|
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - ">="
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: '2.7'
|
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - ">="
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0'
|
|
160
|
+
requirements: []
|
|
161
|
+
rubygems_version: 3.5.22
|
|
162
|
+
signing_key:
|
|
163
|
+
specification_version: 4
|
|
164
|
+
summary: Confidence provider for the OpenFeature SDK
|
|
165
|
+
test_files: []
|