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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Confidence
4
+ module OpenFeature
5
+ VERSION = "0.1.0" # x-release-please-version
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openfeature/api_client"
4
+ require_relative "openfeature/errors"
5
+ require_relative "openfeature/provider"
6
+ require_relative "openfeature/version"
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: []