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,393 @@
1
+ # Flagsmith OpenFeature Provider - Design Document
2
+
3
+ **Created:** 2025-11-17
4
+ **Status:** Design Phase
5
+ **Target:** OpenFeature Ruby SDK integration with Flagsmith
6
+
7
+ ---
8
+
9
+ ## 1. Research Summary
10
+
11
+ ### 1.1 Flagsmith Ruby SDK Details
12
+
13
+ **Gem Name:** `flagsmith`
14
+ **Latest Version:** v4.3.0 (as of December 2024)
15
+ **Ruby Version:** Requires Ruby 2.4+
16
+ **GitHub:** https://github.com/Flagsmith/flagsmith-ruby-client
17
+ **Documentation:** https://docs.flagsmith.com/clients/server-side
18
+
19
+ #### Installation
20
+ ```ruby
21
+ gem install flagsmith
22
+ ```
23
+
24
+ #### Basic Initialization
25
+ ```ruby
26
+ require "flagsmith"
27
+ $flagsmith = Flagsmith::Client.new(
28
+ environment_key: 'FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY'
29
+ )
30
+ ```
31
+
32
+ #### Configuration Options
33
+
34
+ | Option | Type | Default | Description |
35
+ |--------|------|---------|-------------|
36
+ | `environment_key` | String | **Required** | Server-side authentication token |
37
+ | `api_url` | String | "https://edge.api.flagsmith.com/api/v1/" | Custom self-hosted endpoint |
38
+ | `enable_local_evaluation` | Boolean | false | Local vs. remote flag evaluation mode |
39
+ | `request_timeout_seconds` | Integer | 10 | Network request timeout |
40
+ | `environment_refresh_interval_seconds` | Integer | 60 | Polling interval in local mode |
41
+ | `enable_analytics` | Boolean | false | Send usage analytics to Flagsmith |
42
+ | `default_flag_handler` | Lambda | nil | Fallback for missing/failed flags |
43
+
44
+ #### Flag Evaluation Methods
45
+
46
+ **Environment-level (no user context):**
47
+ ```ruby
48
+ flags = $flagsmith.get_environment_flags()
49
+ show_button = flags.is_feature_enabled('secret_button')
50
+ button_data = flags.get_feature_value('secret_button')
51
+ ```
52
+
53
+ **Identity-specific (with user context):**
54
+ ```ruby
55
+ identifier = 'user@example.com'
56
+ traits = {'car_type': 'sedan', 'age': 30}
57
+ flags = $flagsmith.get_identity_flags(identifier, **traits)
58
+ show_button = flags.is_feature_enabled('secret_button')
59
+ value = flags.get_feature_value('secret_button')
60
+ ```
61
+
62
+ #### Evaluation Modes
63
+ - **Remote Evaluation** (default): Blocking HTTP requests per flag fetch
64
+ - **Local Evaluation**: Asynchronous polling (~60 sec intervals)
65
+ - **Offline Mode**: Requires custom `offline_handler`
66
+
67
+ #### Default Flag Handler Pattern
68
+ ```ruby
69
+ $flagsmith = Flagsmith::Client.new(
70
+ environment_key: '<KEY>',
71
+ default_flag_handler: lambda { |feature_name|
72
+ Flagsmith::Flags::DefaultFlag.new(
73
+ enabled: false,
74
+ value: {'colour': '#ababab'}.to_json
75
+ )
76
+ }
77
+ )
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 2. OpenFeature Provider Patterns (from repo analysis)
83
+
84
+ ### 2.1 Required Provider Interface
85
+
86
+ All providers must implement:
87
+ ```ruby
88
+ class Provider
89
+ attr_reader :metadata # Returns ProviderMetadata with name
90
+
91
+ # Lifecycle (optional)
92
+ def init
93
+ def shutdown
94
+
95
+ # Required evaluation methods
96
+ def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
97
+ def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
98
+ def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
99
+ def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
100
+ def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
101
+ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
102
+ end
103
+ ```
104
+
105
+ ### 2.2 Return Type: ResolutionDetails
106
+
107
+ All fetch_* methods must return:
108
+ ```ruby
109
+ OpenFeature::SDK::Provider::ResolutionDetails.new(
110
+ value: <evaluated_value>, # The flag value
111
+ reason: <Reason constant>, # TARGETING_MATCH, DEFAULT, DISABLED, ERROR, etc.
112
+ variant: "variant_key", # Optional variant identifier
113
+ flag_metadata: { ... }, # Optional metadata
114
+ error_code: <ErrorCode constant>, # If error occurred
115
+ error_message: "Error details" # If error occurred
116
+ )
117
+ ```
118
+
119
+ #### OpenFeature Reason Constants
120
+ - `TARGETING_MATCH` - Flag evaluated with targeting rules
121
+ - `DEFAULT` - Default value used
122
+ - `DISABLED` - Feature is disabled
123
+ - `ERROR` - Error during evaluation
124
+ - `STATIC` - Static value
125
+
126
+ #### OpenFeature ErrorCode Constants
127
+ - `PROVIDER_NOT_READY`
128
+ - `FLAG_NOT_FOUND`
129
+ - `TYPE_MISMATCH`
130
+ - `PARSE_ERROR`
131
+ - `TARGETING_KEY_MISSING`
132
+ - `INVALID_CONTEXT`
133
+ - `GENERAL`
134
+
135
+ ### 2.3 Configuration Patterns Used in Repo
136
+
137
+ **Pattern 1: Options Object** (Used by GO Feature Flag provider)
138
+ ```ruby
139
+ class Options
140
+ def initialize(endpoint:, headers: {}, ...)
141
+ validate_endpoint(endpoint)
142
+ @endpoint = endpoint
143
+ @headers = headers
144
+ end
145
+ end
146
+ ```
147
+
148
+ **Pattern 2: Block-Based Configuration** (Used by flagd provider)
149
+ ```ruby
150
+ OpenFeature::Flagd::Provider.configure do |config|
151
+ config.host = "localhost"
152
+ config.port = 8013
153
+ end
154
+ ```
155
+
156
+ ### 2.4 Error Handling Pattern
157
+
158
+ Create custom exception hierarchy:
159
+ ```ruby
160
+ class FlagsmithError < StandardError
161
+ attr_reader :error_code, :error_message
162
+
163
+ def initialize(error_code, error_message)
164
+ @error_code = error_code # Maps to SDK::Provider::ErrorCode
165
+ @error_message = error_message
166
+ super(error_message)
167
+ end
168
+ end
169
+
170
+ class FlagNotFoundError < FlagsmithError
171
+ class TypeMismatchError < FlagsmithError
172
+ class ConfigurationError < FlagsmithError
173
+ ```
174
+
175
+ ### 2.5 Type Validation Pattern
176
+
177
+ ```ruby
178
+ def evaluate(flag_key:, default_value:, allowed_classes:, evaluation_context: nil)
179
+ # ... evaluation logic ...
180
+
181
+ unless allowed_classes.include?(value.class)
182
+ return SDK::Provider::ResolutionDetails.new(
183
+ value: default_value,
184
+ error_code: SDK::Provider::ErrorCode::TYPE_MISMATCH,
185
+ error_message: "flag type #{value.class} does not match allowed types #{allowed_classes}",
186
+ reason: SDK::Provider::Reason::ERROR
187
+ )
188
+ end
189
+ end
190
+ ```
191
+
192
+ ---
193
+
194
+ ## 3. Proposed Flagsmith Provider Architecture
195
+
196
+ ### 3.1 Directory Structure
197
+
198
+ ```
199
+ providers/openfeature-flagsmith-provider/
200
+ ├── lib/
201
+ │ └── openfeature/
202
+ │ └── flagsmith/
203
+ │ ├── provider.rb # Main provider class
204
+ │ ├── configuration.rb # Configuration/options class
205
+ │ ├── error/
206
+ │ │ └── errors.rb # Custom exception hierarchy
207
+ │ └── version.rb # Version constant
208
+ ├── spec/
209
+ │ ├── spec_helper.rb
210
+ │ ├── provider_spec.rb
211
+ │ ├── configuration_spec.rb
212
+ │ └── fixtures/ # Mock responses
213
+ ├── openfeature-flagsmith-provider.gemspec
214
+ ├── README.md
215
+ ├── CHANGELOG.md
216
+ ├── Gemfile
217
+ └── Rakefile
218
+ ```
219
+
220
+ ### 3.2 Key Design Decisions
221
+
222
+ #### Configuration Strategy
223
+ **Chosen: Options Object Pattern**
224
+
225
+ Reasoning:
226
+ - Flagsmith has many configuration options (api_url, timeouts, evaluation mode, etc.)
227
+ - Options object provides clear validation
228
+ - Aligns with GO Feature Flag provider pattern (most similar use case)
229
+
230
+ ```ruby
231
+ options = OpenFeature::Flagsmith::Configuration.new(
232
+ environment_key: "your_key",
233
+ api_url: "https://edge.api.flagsmith.com/api/v1/",
234
+ enable_local_evaluation: false,
235
+ request_timeout_seconds: 10,
236
+ enable_analytics: false
237
+ )
238
+
239
+ provider = OpenFeature::Flagsmith::Provider.new(configuration: options)
240
+ ```
241
+
242
+ #### Evaluation Context Mapping
243
+
244
+ OpenFeature EvaluationContext → Flagsmith Identity + Traits:
245
+ - `evaluation_context.targeting_key` → Flagsmith identity identifier
246
+ - All other `evaluation_context.fields` → Flagsmith traits
247
+
248
+ ```ruby
249
+ def map_context_to_identity(evaluation_context)
250
+ return [nil, {}] if evaluation_context.nil?
251
+
252
+ identifier = evaluation_context.targeting_key
253
+ traits = evaluation_context.fields.reject { |k, _v| k == :targeting_key }
254
+
255
+ [identifier, traits]
256
+ end
257
+ ```
258
+
259
+ #### Flag Type Mapping
260
+
261
+ | Flagsmith Type | OpenFeature Type | Notes |
262
+ |----------------|------------------|-------|
263
+ | Boolean enabled | `fetch_boolean_value` | Use `is_feature_enabled` |
264
+ | String value | `fetch_string_value` | Use `get_feature_value` |
265
+ | Numeric value | `fetch_number_value` | Parse and validate |
266
+ | JSON value | `fetch_object_value` | Parse JSON string |
267
+
268
+ #### Error Handling Strategy
269
+
270
+ 1. **Flagsmith errors** → Map to OpenFeature ErrorCodes
271
+ 2. **Network errors** → `PROVIDER_NOT_READY` or `GENERAL`
272
+ 3. **Type mismatches** → `TYPE_MISMATCH`
273
+ 4. **Missing flags** → Return default with `FLAG_NOT_FOUND`
274
+
275
+ #### Reason Mapping
276
+
277
+ | Flagsmith State | OpenFeature Reason |
278
+ |-----------------|-------------------|
279
+ | Flag evaluated with identity | `TARGETING_MATCH` |
280
+ | Flag evaluated at environment level | `STATIC` |
281
+ | Flag not found | `DEFAULT` |
282
+ | Flag disabled | `DISABLED` |
283
+ | Error occurred | `ERROR` |
284
+
285
+ ---
286
+
287
+ ## 4. Implementation Plan
288
+
289
+ ### Phase 1: Core Structure
290
+ 1. Create directory structure
291
+ 2. Setup gemspec with dependencies
292
+ 3. Create Configuration class with validation
293
+ 4. Create Provider class skeleton with metadata
294
+
295
+ ### Phase 2: Flag Evaluation
296
+ 5. Implement `fetch_boolean_value` (simplest case)
297
+ 6. Implement context → identity/traits mapping
298
+ 7. Add error handling for boolean evaluation
299
+ 8. Implement remaining fetch_* methods
300
+
301
+ ### Phase 3: Advanced Features
302
+ 9. Handle default_flag_handler integration
303
+ 10. Support local evaluation mode
304
+ 11. Add proper lifecycle management (init/shutdown)
305
+
306
+ ### Phase 4: Testing & Documentation
307
+ 12. Create RSpec test suite with mocked Flagsmith responses
308
+ 13. Write comprehensive README
309
+ 14. Add usage examples
310
+ 15. Configure release automation
311
+
312
+ ---
313
+
314
+ ## 5. Open Questions & Decisions Needed
315
+
316
+ ### 5.1 Design Decisions - RESOLVED ✅
317
+
318
+ 1. **Evaluation Mode Preference**
319
+ - ✅ **Default to remote evaluation** (simpler, no polling overhead)
320
+ - Configurable via `enable_local_evaluation` option
321
+
322
+ 2. **Analytics**
323
+ - ✅ **Opt-in** (`enable_analytics: false` by default)
324
+
325
+ 3. **Default Flag Handler**
326
+ - ✅ **Use OpenFeature's default_value** (matches other providers)
327
+ - Do NOT implement Flagsmith's `default_flag_handler`
328
+ - Return `default_value` with appropriate error_code/reason on failures
329
+
330
+ 4. **Targeting Key Requirement**
331
+ - ✅ **Fall back to environment-level flags** if no targeting_key
332
+ - Use `get_environment_flags()` when targeting_key is nil/empty
333
+ - Use `get_identity_flags()` when targeting_key is present
334
+
335
+ 5. **Version Compatibility**
336
+ - ✅ **Target upcoming version** (will be released soon)
337
+ - Update dependency when new version is available
338
+
339
+ ### 5.2 Technical Considerations
340
+
341
+ **Type Detection Challenge:**
342
+ Flagsmith's `get_feature_value` returns values as strings/JSON. We need to:
343
+ - Parse JSON for objects
344
+ - Detect numeric types
345
+ - Handle type mismatches gracefully
346
+
347
+ **Variant Support:**
348
+ Flagsmith doesn't have explicit "variants" like some systems. Options:
349
+ - Use feature key as variant
350
+ - Leave variant nil
351
+ - Use enabled/disabled as variant
352
+
353
+ **Metadata:**
354
+ Flagsmith flags don't inherently have metadata beyond enabled/value. We could:
355
+ - Include trait data as flag_metadata
356
+ - Leave empty
357
+ - Add custom metadata extraction
358
+
359
+ ---
360
+
361
+ ## 6. Dependencies
362
+
363
+ ### Runtime
364
+ - `openfeature-sdk` (~> 0.3.1)
365
+ - `flagsmith` (~> 4.3.0)
366
+
367
+ ### Development
368
+ - `rake` (~> 13.0)
369
+ - `rspec` (~> 3.12.0)
370
+ - `webmock` (~> 3.0) - for mocking Flagsmith HTTP calls
371
+ - `standard` - Ruby linter
372
+ - `rubocop` - Code style
373
+ - `simplecov` - Test coverage
374
+
375
+ ---
376
+
377
+ ## 7. Next Steps
378
+
379
+ 1. **User Decisions** - Get answers to open questions above
380
+ 2. **Proof of Concept** - Build minimal provider with boolean support
381
+ 3. **Validate Approach** - Test with real Flagsmith instance
382
+ 4. **Expand** - Add remaining types and features
383
+ 5. **Polish** - Tests, docs, release config
384
+
385
+ ---
386
+
387
+ ## 8. References
388
+
389
+ - OpenFeature Specification: https://openfeature.dev/specification/
390
+ - Flagsmith Docs: https://docs.flagsmith.com/clients/server-side
391
+ - Flagsmith Ruby Client: https://github.com/Flagsmith/flagsmith-ruby-client
392
+ - GO Feature Flag Provider (reference impl): `providers/openfeature-go-feature-flag-provider/`
393
+ - flagd Provider (reference impl): `providers/openfeature-flagd-provider/`
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "flagsmith", "~> 4.3"
6
+ gem "rake", "~> 13.0"
7
+ gem "rspec", "~> 3.12.0"
8
+ gem "webmock", "~> 3.0"
9
+ gem "standard", "~> 1.0"
10
+ gem "rubocop", "~> 1.0"
11
+ gem "simplecov", "~> 0.22"
data/Gemfile.lock ADDED
@@ -0,0 +1,126 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ openfeature-flagsmith-provider (0.1.1)
5
+ flagsmith (~> 4.3)
6
+ openfeature-sdk (~> 0.3.1)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.8.7)
12
+ public_suffix (>= 2.0.2, < 7.0)
13
+ ast (2.4.3)
14
+ bigdecimal (3.3.1)
15
+ crack (1.0.1)
16
+ bigdecimal
17
+ rexml
18
+ diff-lcs (1.6.2)
19
+ docile (1.4.1)
20
+ faraday (2.14.0)
21
+ faraday-net_http (>= 2.0, < 3.5)
22
+ json
23
+ logger
24
+ faraday-net_http (3.4.2)
25
+ net-http (~> 0.5)
26
+ faraday-retry (2.3.2)
27
+ faraday (~> 2.0)
28
+ flagsmith (4.3.0)
29
+ faraday (>= 2.0.1)
30
+ faraday-retry
31
+ semantic
32
+ hashdiff (1.2.1)
33
+ json (2.16.0)
34
+ language_server-protocol (3.17.0.5)
35
+ lint_roller (1.1.0)
36
+ logger (1.7.0)
37
+ net-http (0.8.0)
38
+ uri (>= 0.11.1)
39
+ openfeature-sdk (0.3.1)
40
+ parallel (1.27.0)
41
+ parser (3.3.10.0)
42
+ ast (~> 2.4.1)
43
+ racc
44
+ prism (1.6.0)
45
+ public_suffix (6.0.2)
46
+ racc (1.8.1)
47
+ rainbow (3.1.1)
48
+ rake (13.3.1)
49
+ regexp_parser (2.11.3)
50
+ rexml (3.4.4)
51
+ rspec (3.12.0)
52
+ rspec-core (~> 3.12.0)
53
+ rspec-expectations (~> 3.12.0)
54
+ rspec-mocks (~> 3.12.0)
55
+ rspec-core (3.12.3)
56
+ rspec-support (~> 3.12.0)
57
+ rspec-expectations (3.12.4)
58
+ diff-lcs (>= 1.2.0, < 2.0)
59
+ rspec-support (~> 3.12.0)
60
+ rspec-mocks (3.12.7)
61
+ diff-lcs (>= 1.2.0, < 2.0)
62
+ rspec-support (~> 3.12.0)
63
+ rspec-support (3.12.2)
64
+ rubocop (1.81.7)
65
+ json (~> 2.3)
66
+ language_server-protocol (~> 3.17.0.2)
67
+ lint_roller (~> 1.1.0)
68
+ parallel (~> 1.10)
69
+ parser (>= 3.3.0.2)
70
+ rainbow (>= 2.2.2, < 4.0)
71
+ regexp_parser (>= 2.9.3, < 3.0)
72
+ rubocop-ast (>= 1.47.1, < 2.0)
73
+ ruby-progressbar (~> 1.7)
74
+ unicode-display_width (>= 2.4.0, < 4.0)
75
+ rubocop-ast (1.48.0)
76
+ parser (>= 3.3.7.2)
77
+ prism (~> 1.4)
78
+ rubocop-performance (1.25.0)
79
+ lint_roller (~> 1.1)
80
+ rubocop (>= 1.75.0, < 2.0)
81
+ rubocop-ast (>= 1.38.0, < 2.0)
82
+ ruby-progressbar (1.13.0)
83
+ semantic (1.6.1)
84
+ simplecov (0.22.0)
85
+ docile (~> 1.1)
86
+ simplecov-html (~> 0.11)
87
+ simplecov_json_formatter (~> 0.1)
88
+ simplecov-html (0.13.2)
89
+ simplecov_json_formatter (0.1.4)
90
+ standard (1.35.0.1)
91
+ language_server-protocol (~> 3.17.0.2)
92
+ lint_roller (~> 1.0)
93
+ rubocop (~> 1.62)
94
+ standard-custom (~> 1.0.0)
95
+ standard-performance (~> 1.3)
96
+ standard-custom (1.0.2)
97
+ lint_roller (~> 1.0)
98
+ rubocop (~> 1.50)
99
+ standard-performance (1.8.0)
100
+ lint_roller (~> 1.1)
101
+ rubocop-performance (~> 1.25.0)
102
+ unicode-display_width (3.2.0)
103
+ unicode-emoji (~> 4.1)
104
+ unicode-emoji (4.1.0)
105
+ uri (1.1.1)
106
+ webmock (3.26.1)
107
+ addressable (>= 2.8.0)
108
+ crack (>= 0.3.2)
109
+ hashdiff (>= 0.4.0, < 2.0.0)
110
+
111
+ PLATFORMS
112
+ arm64-darwin-24
113
+ ruby
114
+
115
+ DEPENDENCIES
116
+ flagsmith (~> 4.3)
117
+ openfeature-flagsmith-provider!
118
+ rake (~> 13.0)
119
+ rspec (~> 3.12.0)
120
+ rubocop (~> 1.0)
121
+ simplecov (~> 0.22)
122
+ standard (~> 1.0)
123
+ webmock (~> 3.0)
124
+
125
+ BUNDLED WITH
126
+ 2.6.9