openfeature-flagsmith-provider 0.1.1

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.
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open_feature/sdk"
4
+ require "flagsmith"
5
+ require "json"
6
+ require_relative "options"
7
+ require_relative "error/errors"
8
+
9
+ module OpenFeature
10
+ module Flagsmith
11
+ # OpenFeature provider for Flagsmith
12
+ class Provider
13
+ PROVIDER_NAME = "Flagsmith Provider"
14
+ attr_reader :metadata, :options
15
+
16
+ def initialize(options:)
17
+ @metadata = SDK::Provider::ProviderMetadata.new(name: PROVIDER_NAME)
18
+ @options = options
19
+ @flagsmith_client = nil
20
+ end
21
+
22
+ def init
23
+ # Initialize Flagsmith client
24
+ @flagsmith_client = create_flagsmith_client
25
+ end
26
+
27
+ def shutdown
28
+ # Cleanup Flagsmith client resources
29
+ # Note: Flagsmith Ruby SDK doesn't require explicit cleanup as of version 4.3
30
+ # If future versions add cleanup methods, they should be called here
31
+ @flagsmith_client = nil
32
+ end
33
+
34
+ def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
35
+ evaluate_boolean(flag_key, default_value, evaluation_context)
36
+ end
37
+
38
+ def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
39
+ evaluate_value(flag_key, default_value, evaluation_context, [String])
40
+ end
41
+
42
+ def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
43
+ evaluate_value(flag_key, default_value, evaluation_context, [Integer, Float, Numeric])
44
+ end
45
+
46
+ def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
47
+ evaluate_value(flag_key, default_value, evaluation_context, [Integer])
48
+ end
49
+
50
+ def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
51
+ evaluate_value(flag_key, default_value, evaluation_context, [Float])
52
+ end
53
+
54
+ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
55
+ evaluate_value(flag_key, default_value, evaluation_context, [Hash, Array])
56
+ end
57
+
58
+ private
59
+
60
+ def create_flagsmith_client
61
+ ::Flagsmith::Client.new(
62
+ environment_key: @options.environment_key,
63
+ api_url: @options.api_url,
64
+ enable_local_evaluation: @options.local_evaluation?,
65
+ request_timeout_seconds: @options.request_timeout_seconds,
66
+ enable_analytics: @options.analytics_enabled?,
67
+ environment_refresh_interval_seconds: @options.environment_refresh_interval_seconds
68
+ )
69
+ rescue => e
70
+ raise ProviderNotReadyError, "Failed to create Flagsmith client: #{e.class}: #{e.message}"
71
+ end
72
+
73
+ def evaluate_boolean(flag_key, default_value, evaluation_context)
74
+ return provider_not_ready_result(default_value) if @flagsmith_client.nil?
75
+ return invalid_flag_key_result(default_value) if flag_key.nil? || flag_key.to_s.empty?
76
+
77
+ flags = get_flags(evaluation_context)
78
+ value = flags.is_feature_enabled(flag_key)
79
+
80
+ success_result(value, evaluation_context)
81
+ rescue FlagsmithError => e
82
+ error_result(default_value, e.error_code, e.error_message)
83
+ rescue => e
84
+ error_result(default_value, SDK::Provider::ErrorCode::GENERAL, "Unexpected error: #{e.class}: #{e.message}")
85
+ end
86
+
87
+ def evaluate_value(flag_key, default_value, evaluation_context, allowed_type_classes)
88
+ return provider_not_ready_result(default_value) if @flagsmith_client.nil?
89
+ return invalid_flag_key_result(default_value) if flag_key.nil? || flag_key.to_s.empty?
90
+
91
+ flags = get_flags(evaluation_context)
92
+ found_flag = flags.all_flags.find { |f| f.feature_name == flag_key }
93
+
94
+ return flag_not_found_result(default_value, flag_key) if found_flag.nil?
95
+ return flag_disabled_result(default_value, flag_key) unless found_flag.enabled
96
+
97
+ raw_value = found_flag.value
98
+ value = if [Hash, Array].any? { |klass| allowed_type_classes.include?(klass) }
99
+ parse_json_value(raw_value)
100
+ elsif [Integer, Float, Numeric].any? { |klass| allowed_type_classes.include?(klass) }
101
+ parse_numeric_value(raw_value, allowed_type_classes)
102
+ else
103
+ raw_value
104
+ end
105
+
106
+ return type_mismatch_result(default_value, value, allowed_type_classes) unless type_matches?(value, allowed_type_classes)
107
+
108
+ success_result(value, evaluation_context)
109
+ rescue FlagsmithError => e
110
+ error_result(default_value, e.error_code, e.error_message)
111
+ rescue => e
112
+ error_result(default_value, SDK::Provider::ErrorCode::GENERAL, "Unexpected error: #{e.class}: #{e.message}")
113
+ end
114
+
115
+ def provider_not_ready_result(default_value)
116
+ SDK::Provider::ResolutionDetails.new(
117
+ value: default_value,
118
+ reason: SDK::Provider::Reason::ERROR,
119
+ error_code: SDK::Provider::ErrorCode::PROVIDER_NOT_READY,
120
+ error_message: "Provider not initialized. Call init() first."
121
+ )
122
+ end
123
+
124
+ def invalid_flag_key_result(default_value)
125
+ SDK::Provider::ResolutionDetails.new(
126
+ value: default_value,
127
+ reason: SDK::Provider::Reason::DEFAULT,
128
+ error_code: SDK::Provider::ErrorCode::FLAG_NOT_FOUND,
129
+ error_message: "Flag key cannot be empty or nil"
130
+ )
131
+ end
132
+
133
+ def flag_not_found_result(default_value, flag_key)
134
+ SDK::Provider::ResolutionDetails.new(
135
+ value: default_value,
136
+ reason: SDK::Provider::Reason::DEFAULT,
137
+ error_code: SDK::Provider::ErrorCode::FLAG_NOT_FOUND,
138
+ error_message: "Flag '#{flag_key}' not found"
139
+ )
140
+ end
141
+
142
+ def flag_disabled_result(default_value, flag_key)
143
+ SDK::Provider::ResolutionDetails.new(
144
+ value: default_value,
145
+ reason: SDK::Provider::Reason::DISABLED,
146
+ error_message: "Flag '#{flag_key}' is disabled"
147
+ )
148
+ end
149
+
150
+ def type_mismatch_result(default_value, value, allowed_type_classes)
151
+ SDK::Provider::ResolutionDetails.new(
152
+ value: default_value,
153
+ reason: SDK::Provider::Reason::ERROR,
154
+ error_code: SDK::Provider::ErrorCode::TYPE_MISMATCH,
155
+ error_message: "Expected type #{allowed_type_classes}, got #{value.class}"
156
+ )
157
+ end
158
+
159
+ def error_result(default_value, error_code, error_message)
160
+ SDK::Provider::ResolutionDetails.new(
161
+ value: default_value,
162
+ reason: SDK::Provider::Reason::ERROR,
163
+ error_code: error_code,
164
+ error_message: error_message
165
+ )
166
+ end
167
+
168
+ def success_result(value, evaluation_context)
169
+ SDK::Provider::ResolutionDetails.new(
170
+ value: value,
171
+ reason: determine_reason(evaluation_context),
172
+ variant: nil,
173
+ flag_metadata: {}
174
+ )
175
+ end
176
+
177
+ def get_flags(evaluation_context)
178
+ raise ProviderNotReadyError, "Flagsmith client not initialized" if @flagsmith_client.nil?
179
+
180
+ if evaluation_context.nil?
181
+ return @flagsmith_client.get_environment_flags
182
+ end
183
+
184
+ targeting_key = evaluation_context.targeting_key
185
+ if targeting_key.nil? || targeting_key.to_s.strip.empty?
186
+ @flagsmith_client.get_environment_flags
187
+ else
188
+ traits = evaluation_context.fields.transform_keys(&:to_sym).reject { |k, _v| k == :targeting_key }
189
+ @flagsmith_client.get_identity_flags(targeting_key.to_s, **traits)
190
+ end
191
+ rescue => e
192
+ raise FlagsmithClientError, "#{e.class}: #{e.message}"
193
+ end
194
+
195
+ def parse_json_value(value)
196
+ return value if value.is_a?(Hash) || value.is_a?(Array)
197
+ return nil if value.nil?
198
+
199
+ JSON.parse(value.to_s)
200
+ rescue JSON::ParserError => e
201
+ raise ParseError, e.message
202
+ end
203
+
204
+ def parse_numeric_value(value, allowed_type_classes)
205
+ # If already the right type, return it
206
+ return value if allowed_type_classes.any? { |klass| value.is_a?(klass) }
207
+ return nil if value.nil?
208
+
209
+ # Try to parse string to numeric
210
+ if value.is_a?(String)
211
+ if allowed_type_classes.include?(Numeric)
212
+ # For Numeric, try Integer first, then Float
213
+ begin
214
+ return Integer(value)
215
+ rescue ArgumentError, TypeError
216
+ return Float(value)
217
+ end
218
+ elsif allowed_type_classes.include?(Integer)
219
+ return Integer(value)
220
+ elsif allowed_type_classes.include?(Float)
221
+ return Float(value)
222
+ end
223
+ end
224
+
225
+ # Safe numeric type conversions (following Flipt provider pattern)
226
+ if value.is_a?(Numeric)
227
+ # Integer → Float: always safe (no precision loss)
228
+ if value.is_a?(Integer) && allowed_type_classes == [Float]
229
+ return value.to_f
230
+ end
231
+
232
+ # Float → Integer: only if it's a whole number (prevents data loss)
233
+ # Example: 3.0 → 3 (OK), but 3.99 → fails type check (ERROR)
234
+ if value.is_a?(Float) && allowed_type_classes == [Integer]
235
+ return value.to_i if value.to_i == value
236
+ end
237
+
238
+ # For generic fetch_number_value (accepts any numeric type), return as-is
239
+ # This handles [Integer, Float, Numeric] case
240
+ end
241
+
242
+ value
243
+ rescue ArgumentError, TypeError => e
244
+ raise ParseError, "Cannot convert '#{value}' to numeric type: #{e.message}"
245
+ end
246
+
247
+ def type_matches?(value, allowed_type_classes)
248
+ allowed_type_classes.any? { |klass| value.is_a?(klass) }
249
+ end
250
+
251
+ def determine_reason(evaluation_context)
252
+ # Use TARGETING_MATCH if we have targeting_key (identity-specific)
253
+ # Use STATIC for environment-level flags
254
+ return SDK::Provider::Reason::STATIC if evaluation_context.nil?
255
+
256
+ targeting_key = evaluation_context.targeting_key
257
+ if targeting_key.nil? || targeting_key.to_s.strip.empty?
258
+ SDK::Provider::Reason::STATIC
259
+ else
260
+ SDK::Provider::Reason::TARGETING_MATCH
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFeature
4
+ module Flagsmith
5
+ VERSION = "0.1.1"
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ require_relative "lib/openfeature/flagsmith/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "openfeature-flagsmith-provider"
5
+ spec.version = OpenFeature::Flagsmith::VERSION
6
+ spec.authors = ["OpenFeature Contributors"]
7
+ spec.email = ["cncf-openfeature-contributors@lists.cncf.io"]
8
+
9
+ spec.summary = "OpenFeature provider for Flagsmith"
10
+ spec.description = "Flagsmith provider for the OpenFeature Ruby SDK"
11
+ spec.homepage = "https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-flagsmith-provider"
12
+ spec.license = "Apache-2.0"
13
+ spec.required_ruby_version = ">= 3.1"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
18
+ spec.metadata["bug_tracker_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/issues"
19
+ spec.metadata["documentation_uri"] = "#{spec.homepage}/README.md"
20
+
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+
25
+ spec.require_paths = ["lib"]
26
+
27
+ # Runtime dependencies
28
+ spec.add_runtime_dependency "openfeature-sdk", "~> 0.3.1"
29
+ spec.add_runtime_dependency "flagsmith", "~> 4.3"
30
+
31
+ # Development dependencies
32
+ spec.add_development_dependency "rake", "~> 13.0"
33
+ spec.add_development_dependency "rspec", "~> 3.12.0"
34
+ spec.add_development_dependency "webmock", "~> 3.0"
35
+ spec.add_development_dependency "standard", "~> 1.0"
36
+ spec.add_development_dependency "rubocop", "~> 1.0"
37
+ spec.add_development_dependency "simplecov", "~> 0.22"
38
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openfeature-flagsmith-provider
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - OpenFeature Contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-12-15 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.3.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: flagsmith
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.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: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: standard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.22'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.22'
125
+ description: Flagsmith provider for the OpenFeature Ruby SDK
126
+ email:
127
+ - cncf-openfeature-contributors@lists.cncf.io
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".context.md"
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - ".rubocop.yml"
136
+ - ".ruby-version"
137
+ - CHANGELOG.md
138
+ - FLAGSMITH_PROVIDER_DESIGN.md
139
+ - Gemfile
140
+ - Gemfile.lock
141
+ - README.md
142
+ - Rakefile
143
+ - lib/openfeature/flagsmith/error/errors.rb
144
+ - lib/openfeature/flagsmith/options.rb
145
+ - lib/openfeature/flagsmith/provider.rb
146
+ - lib/openfeature/flagsmith/version.rb
147
+ - openfeature-flagsmith-provider.gemspec
148
+ homepage: https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-flagsmith-provider
149
+ licenses:
150
+ - Apache-2.0
151
+ metadata:
152
+ homepage_uri: https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-flagsmith-provider
153
+ source_code_uri: https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-flagsmith-provider
154
+ changelog_uri: https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-flagsmith-provider/blob/main/CHANGELOG.md
155
+ bug_tracker_uri: https://github.com/open-feature/ruby-sdk-contrib/issues
156
+ documentation_uri: https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-flagsmith-provider/README.md
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '3.1'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubygems_version: 3.5.22
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: OpenFeature provider for Flagsmith
176
+ test_files: []