flipt_client 0.17.0 → 1.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 +4 -4
- data/README.md +180 -28
- data/flipt-client-ruby.gemspec +1 -1
- data/lib/ext/darwin_aarch64/libfliptengine.dylib +0 -0
- data/lib/ext/darwin_x86_64/libfliptengine.dylib +0 -0
- data/lib/ext/flipt_engine.h +67 -14
- data/lib/ext/linux_aarch64/libfliptengine.so +0 -0
- data/lib/ext/linux_x86_64/libfliptengine.so +0 -0
- data/lib/ext/windows_x86_64/fliptengine.dll +0 -0
- data/lib/flipt_client/models.rb +163 -0
- data/lib/flipt_client/version.rb +1 -1
- data/lib/flipt_client.rb +129 -48
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02ba6911dc6076bbc0703d4c08a242ba24bb75ce82ce2fbce36822b51981c50e
|
4
|
+
data.tar.gz: fd4810c100d71063d4f61d8c4d752f07b616ac6f6d7ce4b31446d4360b766682
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb5e9dce20daa6f5bb0998a5b0a99635c23ac5d9d8ac76eb111bc7e87de4d9d730754bf9c3cc4be5f89f788c596c254153d0f31bf2137d28d086bc38516bfcd2
|
7
|
+
data.tar.gz: 569fbc3bf3dd29205881aaab465592891f37dbcd3112f12f54dc4e9ac6977c4e111fda0ac1e77474af0f879fc36bcc1ff4f98aaddd5a7ad07e1c4df2641f06b6
|
data/README.md
CHANGED
@@ -26,9 +26,9 @@ Upon instantiation, the `flipt-client-ruby` library will fetch the flag state fr
|
|
26
26
|
|
27
27
|
By default, the SDK will poll the Flipt server for new flag state at a regular interval. This interval can be configured using the `update_interval` option when constructing a client. The default interval is 120 seconds.
|
28
28
|
|
29
|
-
### Streaming (Flipt Cloud
|
29
|
+
### Streaming (Flipt Cloud/Flipt v2)
|
30
30
|
|
31
|
-
[Flipt Cloud](https://flipt.io/cloud) users can use the `streaming` fetch method to stream flag state changes from the Flipt server to the SDK.
|
31
|
+
[Flipt Cloud](https://flipt.io/cloud) and [Flipt v2](https://docs.flipt.io/v2) users can use the `streaming` fetch method to stream flag state changes from the Flipt server to the SDK.
|
32
32
|
|
33
33
|
When in streaming mode, the SDK will connect to the Flipt server and open a persistent connection that will remain open until the client is closed. The SDK will then receive flag state changes in real-time.
|
34
34
|
|
@@ -67,6 +67,21 @@ gem install ffi -- --enable-system-libffi # to install the gem manually
|
|
67
67
|
bundle config build.ffi --enable-system-libffi # for bundle install
|
68
68
|
```
|
69
69
|
|
70
|
+
## Migration Notes
|
71
|
+
|
72
|
+
### Pre-1.0.0 -> 1.0.0
|
73
|
+
|
74
|
+
This section is for users who are migrating from a previous (pre-1.0.0) version of the SDK.
|
75
|
+
|
76
|
+
- `Flipt::EvaluationClient` has been renamed to `Flipt::Client`. Update all usages and imports accordingly. A deprecation warning is emitted if you use the old class.
|
77
|
+
- All evaluation methods now use **keyword arguments** (e.g., `flag_key:`, `entity_id:`, `context:`) instead of a single hash argument. Update your method calls to use keyword arguments.
|
78
|
+
- All evaluation methods now return **response model objects** (`VariantEvaluationResponse`, `BooleanEvaluationResponse`, `BatchEvaluationResponse`, `ErrorEvaluationResponse`) instead of raw hashes. Update your code to use attribute readers (e.g., `resp.flag_key`, `resp.enabled`).
|
79
|
+
- Batch evaluation responses now contain an array of model objects, not hashes.
|
80
|
+
- Error handling is now standardized and idiomatic. All errors inherit from `Flipt::Error` (with subclasses like `ValidationError`, `EvaluationError`). Update your rescue blocks accordingly.
|
81
|
+
- The minimum supported Ruby version is now 2.7.0.
|
82
|
+
- The client constructor now accepts keyword arguments for configuration (e.g., `url:`, `namespace:`, `authentication:`). The `environment` option is now supported.
|
83
|
+
- The API and documentation are more idiomatic and Ruby-like throughout.
|
84
|
+
|
70
85
|
## Usage
|
71
86
|
|
72
87
|
In your Ruby code you can import this client and use it as so:
|
@@ -74,46 +89,143 @@ In your Ruby code you can import this client and use it as so:
|
|
74
89
|
```ruby
|
75
90
|
require 'flipt_client'
|
76
91
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
#
|
86
|
-
#
|
87
|
-
# You can replace the url with where your upstream Flipt instance points to, the update interval for how long you are willing
|
88
|
-
# to wait for updated flag state, and the auth token if your Flipt instance requires it.
|
89
|
-
client = Flipt::EvaluationClient.new()
|
90
|
-
resp = client.evaluate_variant({ flag_key: 'buzz', entity_id: 'someentity', context: { fizz: 'buzz' } })
|
91
|
-
|
92
|
-
puts resp
|
92
|
+
client = Flipt::Client.new(url: 'http://localhost:8080', authentication: Flipt::ClientTokenAuthentication.new('secret'))
|
93
|
+
|
94
|
+
resp = client.evaluate_variant(flag_key: 'buzz', entity_id: 'someentity', context: { fizz: 'buzz' })
|
95
|
+
puts resp.flag_key # => 'buzz'
|
96
|
+
puts resp.match # => true
|
97
|
+
puts resp.reason # => 'MATCH_EVALUATION_REASON'
|
98
|
+
|
99
|
+
resp = client.evaluate_boolean(flag_key: 'my-feature', entity_id: 'someentity')
|
100
|
+
puts resp.enabled # => true
|
93
101
|
```
|
94
102
|
|
95
103
|
### Constructor Arguments
|
96
104
|
|
97
|
-
The `Flipt::
|
105
|
+
The `Flipt::Client` constructor accepts the following keyword arguments:
|
98
106
|
|
107
|
+
- `environment`: The environment (Flipt v2) to fetch flag state from. If not provided, the client will default to the `default` environment.
|
99
108
|
- `namespace`: The namespace to fetch flag state from. If not provided, the client will default to the `default` namespace.
|
100
|
-
- `
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
109
|
+
- `url`: The URL of the upstream Flipt instance. Defaults to `http://localhost:8080`.
|
110
|
+
- `request_timeout`: Timeout (in seconds) for requests. Defaults to no timeout.
|
111
|
+
- `update_interval`: Interval (in seconds) to fetch new flag state. Defaults to 120 seconds.
|
112
|
+
- `authentication`: The authentication strategy to use. Defaults to no authentication. See [Authentication](#authentication).
|
113
|
+
- `reference`: The [reference](https://docs.flipt.io/guides/user/using-references) to use when fetching flag state.
|
114
|
+
- `fetch_mode`: The fetch mode to use. Defaults to polling.
|
115
|
+
- `error_strategy`: The error strategy to use. Defaults to fail. See [Error Strategies](#error-strategies).
|
116
|
+
- `snapshot`: The snapshot to use when initializing the client. Defaults to no snapshot. See [Snapshotting](#snapshotting).
|
117
|
+
- `tls_config`: The TLS configuration for connecting to servers with custom certificates. See [TLS Configuration](#tls-configuration).
|
108
118
|
|
109
119
|
### Authentication
|
110
120
|
|
111
|
-
The `
|
121
|
+
The `Flipt::Client` supports the following authentication strategies:
|
112
122
|
|
113
123
|
- No Authentication (default)
|
114
124
|
- [Client Token Authentication](https://docs.flipt.io/authentication/using-tokens)
|
115
125
|
- [JWT Authentication](https://docs.flipt.io/authentication/using-jwts)
|
116
126
|
|
127
|
+
### TLS Configuration
|
128
|
+
|
129
|
+
The `Flipt::Client` supports configuring TLS settings for secure connections to Flipt servers. This is useful when:
|
130
|
+
|
131
|
+
- Connecting to Flipt servers with self-signed certificates
|
132
|
+
- Using custom Certificate Authorities (CAs)
|
133
|
+
- Implementing mutual TLS authentication
|
134
|
+
- Testing with insecure connections (development only)
|
135
|
+
|
136
|
+
#### Basic TLS with Custom CA Certificate
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
# Using a CA certificate file
|
140
|
+
tls_config = Flipt::TlsConfig.with_ca_cert_file('/path/to/ca.pem')
|
141
|
+
|
142
|
+
client = Flipt::Client.new(
|
143
|
+
url: 'https://flipt.example.com',
|
144
|
+
tls_config: tls_config
|
145
|
+
)
|
146
|
+
```
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# Using CA certificate data directly
|
150
|
+
ca_cert_data = File.read('/path/to/ca.pem')
|
151
|
+
tls_config = Flipt::TlsConfig.with_ca_cert_data(ca_cert_data)
|
152
|
+
|
153
|
+
client = Flipt::Client.new(
|
154
|
+
url: 'https://flipt.example.com',
|
155
|
+
tls_config: tls_config
|
156
|
+
)
|
157
|
+
```
|
158
|
+
|
159
|
+
#### Mutual TLS Authentication
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
# Using certificate and key files
|
163
|
+
tls_config = Flipt::TlsConfig.with_mutual_tls('/path/to/client.pem', '/path/to/client.key')
|
164
|
+
|
165
|
+
client = Flipt::Client.new(
|
166
|
+
url: 'https://flipt.example.com',
|
167
|
+
tls_config: tls_config
|
168
|
+
)
|
169
|
+
```
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
# Using certificate and key data directly
|
173
|
+
client_cert_data = File.read('/path/to/client.pem')
|
174
|
+
client_key_data = File.read('/path/to/client.key')
|
175
|
+
|
176
|
+
tls_config = Flipt::TlsConfig.with_mutual_tls_data(client_cert_data, client_key_data)
|
177
|
+
|
178
|
+
client = Flipt::Client.new(
|
179
|
+
url: 'https://flipt.example.com',
|
180
|
+
tls_config: tls_config
|
181
|
+
)
|
182
|
+
```
|
183
|
+
|
184
|
+
#### Advanced TLS Configuration
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
# Full TLS configuration with all options
|
188
|
+
tls_config = Flipt::TlsConfig.new(
|
189
|
+
ca_cert_file: '/path/to/ca.pem',
|
190
|
+
client_cert_file: '/path/to/client.pem',
|
191
|
+
client_key_file: '/path/to/client.key',
|
192
|
+
insecure_skip_verify: false
|
193
|
+
)
|
194
|
+
|
195
|
+
client = Flipt::Client.new(
|
196
|
+
url: 'https://flipt.example.com',
|
197
|
+
tls_config: tls_config
|
198
|
+
)
|
199
|
+
```
|
200
|
+
|
201
|
+
#### Development Mode (Insecure)
|
202
|
+
|
203
|
+
**⚠️ WARNING: Only use this in development environments!**
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
# Skip certificate verification (NOT for production)
|
207
|
+
tls_config = Flipt::TlsConfig.insecure
|
208
|
+
|
209
|
+
client = Flipt::Client.new(
|
210
|
+
url: 'https://localhost:8443',
|
211
|
+
tls_config: tls_config
|
212
|
+
)
|
213
|
+
```
|
214
|
+
|
215
|
+
#### TLS Configuration Options
|
216
|
+
|
217
|
+
The `TlsConfig` class supports the following options:
|
218
|
+
|
219
|
+
- `ca_cert_file`: Path to custom CA certificate file (PEM format)
|
220
|
+
- `ca_cert_data`: Raw CA certificate content (PEM format) - takes precedence over `ca_cert_file`
|
221
|
+
- `insecure_skip_verify`: Skip certificate verification (development only)
|
222
|
+
- `client_cert_file`: Client certificate file for mutual TLS (PEM format)
|
223
|
+
- `client_key_file`: Client private key file for mutual TLS (PEM format)
|
224
|
+
- `client_cert_data`: Raw client certificate content (PEM format) - takes precedence over `client_cert_file`
|
225
|
+
- `client_key_data`: Raw client private key content (PEM format) - takes precedence over `client_key_file`
|
226
|
+
|
227
|
+
> **Note**: When both file paths and data are provided, the data fields take precedence. For example, if both `ca_cert_file` and `ca_cert_data` are set, `ca_cert_data` will be used.
|
228
|
+
|
117
229
|
### Error Strategies
|
118
230
|
|
119
231
|
The client supports the following error strategies:
|
@@ -121,6 +233,46 @@ The client supports the following error strategies:
|
|
121
233
|
- `fail`: The client will throw an error if the flag state cannot be fetched. This is the default behavior.
|
122
234
|
- `fallback`: The client will maintain the last known good state and use that state for evaluation in case of an error.
|
123
235
|
|
236
|
+
### Response Models
|
237
|
+
|
238
|
+
All evaluation methods return response model objects:
|
239
|
+
|
240
|
+
- `evaluate_variant` returns a `Flipt::VariantEvaluationResponse`
|
241
|
+
- `evaluate_boolean` returns a `Flipt::BooleanEvaluationResponse`
|
242
|
+
- `evaluate_batch` returns a `Flipt::BatchEvaluationResponse`
|
243
|
+
|
244
|
+
### Error Handling
|
245
|
+
|
246
|
+
All errors inherit from `Flipt::Error`. Common subclasses include:
|
247
|
+
|
248
|
+
- `Flipt::ValidationError`
|
249
|
+
- `Flipt::EvaluationError`
|
250
|
+
|
251
|
+
You can rescue these errors as needed:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
begin
|
255
|
+
client.evaluate_variant(flag_key: 'missing', entity_id: 'user')
|
256
|
+
rescue Flipt::EvaluationError => e
|
257
|
+
puts "Evaluation failed: #{e.message}"
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
### Snapshotting
|
262
|
+
|
263
|
+
The client supports snapshotting of flag state as well as seeding the client with a snapshot for evaluation. This is helpful if you want to use the client in an environment where the Flipt server is not guaranteed to be available or reachable on startup.
|
264
|
+
|
265
|
+
To get the snapshot for the client, you can use the `snapshot` method. This returns a base64 encoded JSON string that represents the flag state for the client.
|
266
|
+
|
267
|
+
You can set the snapshot for the client using the `snapshot` option when constructing a client.
|
268
|
+
|
269
|
+
**Note:** You most likely will want to also set the `error_strategy` to `fallback` when using snapshots. This will ensure that you wont get an error if the Flipt server is not available or reachable even on the initial fetch.
|
270
|
+
|
271
|
+
You also may want to store the snapshot in a local file so that you can use it to seed the client on startup.
|
272
|
+
|
273
|
+
> [!IMPORTANT]
|
274
|
+
> If the Flipt server becomes reachable after the setting the snapshot, the client will replace the snapshot with the new flag state from the Flipt server.
|
275
|
+
|
124
276
|
## Load Test
|
125
277
|
|
126
278
|
1. To run the load test, you'll need to have Flipt running locally. You can do this by running the following command from the root of the repository:
|
data/flipt-client-ruby.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.description = 'Flipt Client Evaluation SDK'
|
12
12
|
spec.homepage = 'https://www.flipt.io'
|
13
13
|
spec.license = 'MIT'
|
14
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 2.
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
|
15
15
|
|
16
16
|
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
17
17
|
|
Binary file
|
Binary file
|
data/lib/ext/flipt_engine.h
CHANGED
@@ -8,7 +8,35 @@
|
|
8
8
|
*
|
9
9
|
* This function will initialize an Engine and return a pointer back to the caller.
|
10
10
|
*/
|
11
|
-
void *
|
11
|
+
void *initialize_engine_ffi(const char *opts);
|
12
|
+
|
13
|
+
/**
|
14
|
+
* # Safety
|
15
|
+
*
|
16
|
+
* This function will initialize an Engine and return a pointer back to the caller.
|
17
|
+
*/
|
18
|
+
void *initialize_engine(const char *opts);
|
19
|
+
|
20
|
+
/**
|
21
|
+
* # Safety
|
22
|
+
*
|
23
|
+
* This function will take in a pointer to the engine and return the current snapshot as a JSON string.
|
24
|
+
*/
|
25
|
+
const char *get_snapshot_ffi(void *engine_ptr);
|
26
|
+
|
27
|
+
/**
|
28
|
+
* # Safety
|
29
|
+
*
|
30
|
+
* This function will take in a pointer to the engine and return the current snapshot as a JSON string.
|
31
|
+
*/
|
32
|
+
const char *get_snapshot(void *engine_ptr);
|
33
|
+
|
34
|
+
/**
|
35
|
+
* # Safety
|
36
|
+
*
|
37
|
+
* This function will take in a pointer to the engine and return a variant evaluation response.
|
38
|
+
*/
|
39
|
+
const char *evaluate_variant_ffi(void *engine_ptr, const char *evaluation_request);
|
12
40
|
|
13
41
|
/**
|
14
42
|
* # Safety
|
@@ -17,6 +45,13 @@ void *initialize_engine(const char *namespace_, const char *opts);
|
|
17
45
|
*/
|
18
46
|
const char *evaluate_variant(void *engine_ptr, const char *evaluation_request);
|
19
47
|
|
48
|
+
/**
|
49
|
+
* # Safety
|
50
|
+
*
|
51
|
+
* This function will take in a pointer to the engine and return a boolean evaluation response.
|
52
|
+
*/
|
53
|
+
const char *evaluate_boolean_ffi(void *engine_ptr, const char *evaluation_request);
|
54
|
+
|
20
55
|
/**
|
21
56
|
* # Safety
|
22
57
|
*
|
@@ -24,6 +59,13 @@ const char *evaluate_variant(void *engine_ptr, const char *evaluation_request);
|
|
24
59
|
*/
|
25
60
|
const char *evaluate_boolean(void *engine_ptr, const char *evaluation_request);
|
26
61
|
|
62
|
+
/**
|
63
|
+
* # Safety
|
64
|
+
*
|
65
|
+
* This function will take in a pointer to the engine and return a batch evaluation response.
|
66
|
+
*/
|
67
|
+
const char *evaluate_batch_ffi(void *engine_ptr, const char *batch_evaluation_request);
|
68
|
+
|
27
69
|
/**
|
28
70
|
* # Safety
|
29
71
|
*
|
@@ -34,30 +76,41 @@ const char *evaluate_batch(void *engine_ptr, const char *batch_evaluation_reques
|
|
34
76
|
/**
|
35
77
|
* # Safety
|
36
78
|
*
|
37
|
-
* This function will take in a pointer to the engine and return a list of flags
|
79
|
+
* This function will take in a pointer to the engine and return a list of flags.
|
80
|
+
*/
|
81
|
+
const char *list_flags_ffi(void *engine_ptr);
|
82
|
+
|
83
|
+
/**
|
84
|
+
* # Safety
|
85
|
+
*
|
86
|
+
* This function will take in a pointer to the engine and return a list of flags.
|
38
87
|
*/
|
39
88
|
const char *list_flags(void *engine_ptr);
|
40
89
|
|
41
90
|
/**
|
42
91
|
* # Safety
|
43
92
|
*
|
44
|
-
* This function will
|
93
|
+
* This function will take in a pointer to the engine and destroy it.
|
94
|
+
*/
|
95
|
+
void destroy_engine_ffi(void *engine_ptr);
|
96
|
+
|
97
|
+
/**
|
98
|
+
* # Safety
|
99
|
+
*
|
100
|
+
* This function will take in a pointer to the engine and destroy it.
|
45
101
|
*/
|
46
102
|
void destroy_engine(void *engine_ptr);
|
47
103
|
|
48
104
|
/**
|
49
105
|
* # Safety
|
50
106
|
*
|
51
|
-
* This function will take in a pointer to
|
52
|
-
* See Rust the safety section in CString::from_raw.
|
107
|
+
* This function will take in a pointer to a string and destroy it.
|
53
108
|
*/
|
54
|
-
void
|
109
|
+
void destroy_string_ffi(char *ptr);
|
55
110
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
extern void destroy_engine_ffi(void* engine);
|
63
|
-
extern void destroy_string_ffi(char* str);
|
111
|
+
/**
|
112
|
+
* # Safety
|
113
|
+
*
|
114
|
+
* This function will take in a pointer to a string and destroy it.
|
115
|
+
*/
|
116
|
+
void destroy_string(char *ptr);
|
Binary file
|
Binary file
|
Binary file
|
data/lib/flipt_client/models.rb
CHANGED
@@ -40,4 +40,167 @@ module Flipt
|
|
40
40
|
}
|
41
41
|
end
|
42
42
|
end
|
43
|
+
|
44
|
+
# TlsConfig provides configuration for TLS connections to Flipt servers
|
45
|
+
class TlsConfig
|
46
|
+
attr_reader :ca_cert_file, :ca_cert_data, :insecure_skip_verify,
|
47
|
+
:client_cert_file, :client_key_file, :client_cert_data, :client_key_data
|
48
|
+
|
49
|
+
# Initialize TLS configuration
|
50
|
+
#
|
51
|
+
# @param ca_cert_file [String, nil] Path to CA certificate file (PEM format)
|
52
|
+
# @param ca_cert_data [String, nil] Raw CA certificate content (PEM format)
|
53
|
+
# @param insecure_skip_verify [Boolean, nil] Skip certificate verification (development only)
|
54
|
+
# @param client_cert_file [String, nil] Path to client certificate file (PEM format)
|
55
|
+
# @param client_key_file [String, nil] Path to client key file (PEM format)
|
56
|
+
# @param client_cert_data [String, nil] Raw client certificate content (PEM format)
|
57
|
+
# @param client_key_data [String, nil] Raw client key content (PEM format)
|
58
|
+
def initialize(ca_cert_file: nil, ca_cert_data: nil, insecure_skip_verify: nil,
|
59
|
+
client_cert_file: nil, client_key_file: nil,
|
60
|
+
client_cert_data: nil, client_key_data: nil)
|
61
|
+
@ca_cert_file = ca_cert_file
|
62
|
+
@ca_cert_data = ca_cert_data
|
63
|
+
@insecure_skip_verify = insecure_skip_verify
|
64
|
+
@client_cert_file = client_cert_file
|
65
|
+
@client_key_file = client_key_file
|
66
|
+
@client_cert_data = client_cert_data
|
67
|
+
@client_key_data = client_key_data
|
68
|
+
|
69
|
+
validate_files!
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create TLS config for insecure connections (development only)
|
73
|
+
# WARNING: Only use this in development environments
|
74
|
+
#
|
75
|
+
# @return [TlsConfig] TLS config with certificate verification disabled
|
76
|
+
def self.insecure
|
77
|
+
new(insecure_skip_verify: true)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Create TLS config with CA certificate file
|
81
|
+
#
|
82
|
+
# @param ca_cert_file [String] Path to CA certificate file
|
83
|
+
# @return [TlsConfig] TLS config with custom CA certificate
|
84
|
+
def self.with_ca_cert_file(ca_cert_file)
|
85
|
+
new(ca_cert_file: ca_cert_file)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create TLS config with CA certificate data
|
89
|
+
#
|
90
|
+
# @param ca_cert_data [String] CA certificate content in PEM format
|
91
|
+
# @return [TlsConfig] TLS config with custom CA certificate
|
92
|
+
def self.with_ca_cert_data(ca_cert_data)
|
93
|
+
new(ca_cert_data: ca_cert_data)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Create TLS config for mutual TLS with certificate files
|
97
|
+
#
|
98
|
+
# @param client_cert_file [String] Path to client certificate file
|
99
|
+
# @param client_key_file [String] Path to client key file
|
100
|
+
# @return [TlsConfig] TLS config with mutual TLS
|
101
|
+
def self.with_mutual_tls(client_cert_file, client_key_file)
|
102
|
+
new(client_cert_file: client_cert_file, client_key_file: client_key_file)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Create TLS config for mutual TLS with certificate data
|
106
|
+
#
|
107
|
+
# @param client_cert_data [String] Client certificate content in PEM format
|
108
|
+
# @param client_key_data [String] Client key content in PEM format
|
109
|
+
# @return [TlsConfig] TLS config with mutual TLS
|
110
|
+
def self.with_mutual_tls_data(client_cert_data, client_key_data)
|
111
|
+
new(client_cert_data: client_cert_data, client_key_data: client_key_data)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Convert to hash for JSON serialization
|
115
|
+
# @return [Hash] TLS configuration as hash
|
116
|
+
def to_h
|
117
|
+
hash = {}
|
118
|
+
hash[:ca_cert_file] = @ca_cert_file if @ca_cert_file
|
119
|
+
hash[:ca_cert_data] = @ca_cert_data if @ca_cert_data
|
120
|
+
hash[:insecure_skip_verify] = @insecure_skip_verify unless @insecure_skip_verify.nil?
|
121
|
+
hash[:client_cert_file] = @client_cert_file if @client_cert_file
|
122
|
+
hash[:client_key_file] = @client_key_file if @client_key_file
|
123
|
+
hash[:client_cert_data] = @client_cert_data if @client_cert_data
|
124
|
+
hash[:client_key_data] = @client_key_data if @client_key_data
|
125
|
+
hash
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def validate_files!
|
131
|
+
validate_file_exists(@ca_cert_file, 'CA certificate file') if @ca_cert_file
|
132
|
+
validate_file_exists(@client_cert_file, 'Client certificate file') if @client_cert_file
|
133
|
+
validate_file_exists(@client_key_file, 'Client key file') if @client_key_file
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_file_exists(file_path, description)
|
137
|
+
return if file_path.nil? || file_path.strip.empty?
|
138
|
+
|
139
|
+
return if File.exist?(file_path)
|
140
|
+
|
141
|
+
raise ValidationError, "#{description} does not exist: #{file_path}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# VariantEvaluationResponse
|
146
|
+
# @attr_reader [String] flag_key
|
147
|
+
# @attr_reader [Boolean] match
|
148
|
+
# @attr_reader [String] reason
|
149
|
+
# @attr_reader [String] variant_key
|
150
|
+
# @attr_reader [String, nil] variant_attachment
|
151
|
+
# @attr_reader [Array<String>] segment_keys
|
152
|
+
class VariantEvaluationResponse
|
153
|
+
attr_reader :flag_key, :match, :reason, :variant_key, :variant_attachment, :segment_keys
|
154
|
+
|
155
|
+
def initialize(flag_key:, match:, reason:, variant_key:, variant_attachment: nil, segment_keys: [])
|
156
|
+
@flag_key = flag_key
|
157
|
+
@match = match
|
158
|
+
@reason = reason
|
159
|
+
@variant_key = variant_key
|
160
|
+
@variant_attachment = variant_attachment
|
161
|
+
@segment_keys = segment_keys
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# BooleanEvaluationResponse
|
166
|
+
# @attr_reader [String] flag_key
|
167
|
+
# @attr_reader [Boolean] enabled
|
168
|
+
# @attr_reader [String] reason
|
169
|
+
# @attr_reader [Array<String>] segment_keys
|
170
|
+
class BooleanEvaluationResponse
|
171
|
+
attr_reader :flag_key, :enabled, :reason, :segment_keys
|
172
|
+
|
173
|
+
def initialize(flag_key:, enabled:, reason:, segment_keys: [])
|
174
|
+
@flag_key = flag_key
|
175
|
+
@enabled = enabled
|
176
|
+
@reason = reason
|
177
|
+
@segment_keys = segment_keys
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# ErrorEvaluationResponse
|
182
|
+
# @attr_reader [String] flag_key
|
183
|
+
# @attr_reader [String] namespace_key
|
184
|
+
# @attr_reader [String] reason
|
185
|
+
# @attr_reader [String] error_message
|
186
|
+
class ErrorEvaluationResponse
|
187
|
+
attr_reader :flag_key, :namespace_key, :reason, :error_message
|
188
|
+
|
189
|
+
def initialize(flag_key:, namespace_key:, reason:, error_message:)
|
190
|
+
@flag_key = flag_key
|
191
|
+
@namespace_key = namespace_key
|
192
|
+
@reason = reason
|
193
|
+
@error_message = error_message
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# BatchEvaluationResponse
|
198
|
+
# @attr_reader [Array] responses
|
199
|
+
class BatchEvaluationResponse
|
200
|
+
attr_reader :responses
|
201
|
+
|
202
|
+
def initialize(responses: [])
|
203
|
+
@responses = responses
|
204
|
+
end
|
205
|
+
end
|
43
206
|
end
|
data/lib/flipt_client/version.rb
CHANGED
data/lib/flipt_client.rb
CHANGED
@@ -7,9 +7,11 @@ require 'json'
|
|
7
7
|
|
8
8
|
module Flipt
|
9
9
|
class Error < StandardError; end
|
10
|
+
class ValidationError < Error; end
|
11
|
+
class EvaluationError < Error; end
|
10
12
|
|
11
|
-
#
|
12
|
-
class
|
13
|
+
# Client is a Ruby Client Side Evaluation Library for Flipt
|
14
|
+
class Client
|
13
15
|
extend FFI::Library
|
14
16
|
|
15
17
|
FLIPTENGINE = 'fliptengine'
|
@@ -32,8 +34,8 @@ module Flipt
|
|
32
34
|
|
33
35
|
ffi_lib File.expand_path(libfile, __dir__)
|
34
36
|
|
35
|
-
# void *initialize_engine(const char *
|
36
|
-
attach_function :initialize_engine,
|
37
|
+
# void *initialize_engine(const char *opts);
|
38
|
+
attach_function :initialize_engine, [:string], :pointer
|
37
39
|
# void destroy_engine(void *engine_ptr);
|
38
40
|
attach_function :destroy_engine, [:pointer], :void
|
39
41
|
# const char *evaluate_variant(void *engine_ptr, const char *evaluation_request);
|
@@ -46,27 +48,33 @@ module Flipt
|
|
46
48
|
attach_function :list_flags, [:pointer], :strptr
|
47
49
|
# void destroy_string(const char *ptr);
|
48
50
|
attach_function :destroy_string, [:pointer], :void
|
51
|
+
# const char *get_snapshot(void *engine_ptr);
|
52
|
+
attach_function :get_snapshot, [:pointer], :strptr
|
49
53
|
|
50
54
|
# Create a new Flipt client
|
51
55
|
#
|
52
|
-
# @param namespace [String] namespace
|
53
56
|
# @param opts [Hash] options
|
57
|
+
# @option opts [String] :environment Flipt environment (default: 'default')
|
58
|
+
# @option opts [String] :namespace Flipt namespace (default: 'default')
|
54
59
|
# @option opts [String] :url Flipt server url
|
55
60
|
# @option opts [AuthenticationStrategy] :authentication strategy to authenticate with Flipt
|
56
61
|
# @option opts [Integer] :request_timeout timeout in seconds for the request
|
57
62
|
# @option opts [Integer] :update_interval interval in seconds to update the cache
|
58
63
|
# @option opts [String] :reference reference to use for namespace data
|
59
64
|
# @option opts [Symbol] :fetch_mode fetch mode to use for the client (:polling or :streaming).
|
60
|
-
# Note: Streaming is currently only supported when using the SDK with Flipt Cloud
|
65
|
+
# Note: Streaming is currently only supported when using the SDK with Flipt Cloud or Flipt v2.
|
61
66
|
# @option opts [Symbol] :error_strategy error strategy to use for the client (:fail or :fallback).
|
62
|
-
|
63
|
-
|
67
|
+
# @option opts [String] :snapshot snapshot to use when initializing the client
|
68
|
+
# @option opts [TlsConfig] :tls_config TLS configuration for connecting to servers with custom certificates
|
69
|
+
def initialize(**opts)
|
70
|
+
@namespace = opts.fetch(:namespace, 'default')
|
64
71
|
|
65
72
|
opts[:authentication] = validate_authentication(opts.fetch(:authentication, NoAuthentication.new))
|
66
73
|
opts[:fetch_mode] = validate_fetch_mode(opts.fetch(:fetch_mode, :polling))
|
67
74
|
opts[:error_strategy] = validate_error_strategy(opts.fetch(:error_strategy, :fail))
|
75
|
+
opts[:tls_config] = validate_tls_config(opts.fetch(:tls_config, nil))
|
68
76
|
|
69
|
-
@engine = self.class.initialize_engine(
|
77
|
+
@engine = self.class.initialize_engine(opts.to_json)
|
70
78
|
ObjectSpace.define_finalizer(self, self.class.finalize(@engine))
|
71
79
|
end
|
72
80
|
|
@@ -76,88 +84,161 @@ module Flipt
|
|
76
84
|
|
77
85
|
# Evaluate a variant flag for a given request
|
78
86
|
#
|
79
|
-
# @param
|
80
|
-
# @
|
81
|
-
# @
|
82
|
-
def evaluate_variant(
|
83
|
-
validate_evaluation_request(
|
84
|
-
|
85
|
-
ptr =
|
87
|
+
# @param flag_key [String]
|
88
|
+
# @param entity_id [String]
|
89
|
+
# @param context [Hash]
|
90
|
+
def evaluate_variant(flag_key:, entity_id:, context: {})
|
91
|
+
validate_evaluation_request(flag_key, entity_id, context)
|
92
|
+
req = { flag_key: flag_key, entity_id: entity_id, context: context }
|
93
|
+
resp, ptr = self.class.evaluate_variant(@engine, req.to_json)
|
94
|
+
ptr = FFI::AutoPointer.new(ptr, Client.method(:destroy_string))
|
86
95
|
data = JSON.parse(resp)
|
87
|
-
raise
|
88
|
-
|
89
|
-
data['result']
|
96
|
+
raise EvaluationError, data['error_message'] if data['status'] != 'success'
|
97
|
+
|
98
|
+
r = data['result']
|
99
|
+
VariantEvaluationResponse.new(
|
100
|
+
flag_key: r['flag_key'],
|
101
|
+
match: r['match'],
|
102
|
+
reason: r['reason'],
|
103
|
+
variant_key: r['variant_key'],
|
104
|
+
variant_attachment: r['variant_attachment'],
|
105
|
+
segment_keys: r['segment_keys'] || []
|
106
|
+
)
|
90
107
|
end
|
91
108
|
|
92
109
|
# Evaluate a boolean flag for a given request
|
93
110
|
#
|
94
|
-
# @param
|
95
|
-
# @
|
96
|
-
# @
|
97
|
-
def evaluate_boolean(
|
98
|
-
validate_evaluation_request(
|
99
|
-
|
100
|
-
ptr =
|
111
|
+
# @param flag_key [String]
|
112
|
+
# @param entity_id [String]
|
113
|
+
# @param context [Hash]
|
114
|
+
def evaluate_boolean(flag_key:, entity_id:, context: {})
|
115
|
+
validate_evaluation_request(flag_key, entity_id, context)
|
116
|
+
req = { flag_key: flag_key, entity_id: entity_id, context: context }
|
117
|
+
resp, ptr = self.class.evaluate_boolean(@engine, req.to_json)
|
118
|
+
ptr = FFI::AutoPointer.new(ptr, Client.method(:destroy_string))
|
101
119
|
data = JSON.parse(resp)
|
102
|
-
raise
|
103
|
-
|
104
|
-
data['result']
|
120
|
+
raise EvaluationError, data['error_message'] if data['status'] != 'success'
|
121
|
+
|
122
|
+
r = data['result']
|
123
|
+
BooleanEvaluationResponse.new(
|
124
|
+
flag_key: r['flag_key'],
|
125
|
+
enabled: r['enabled'],
|
126
|
+
reason: r['reason'],
|
127
|
+
segment_keys: r['segment_keys'] || []
|
128
|
+
)
|
105
129
|
end
|
106
130
|
|
107
131
|
# Evaluate a batch of flags for a given request
|
108
132
|
#
|
109
|
-
# @param
|
133
|
+
# @param requests [Array<Hash>] batch evaluation request
|
110
134
|
# - :entity_id [String] entity id
|
111
135
|
# - :flag_key [String] flag key
|
112
|
-
def evaluate_batch(
|
113
|
-
|
114
|
-
|
136
|
+
def evaluate_batch(requests:)
|
137
|
+
unless requests.is_a?(Array)
|
138
|
+
raise ValidationError, 'requests must be an array of evaluation requests'
|
115
139
|
end
|
116
140
|
|
117
|
-
|
118
|
-
|
141
|
+
requests.each do |request|
|
142
|
+
validate_evaluation_request(request[:flag_key], request[:entity_id], request[:context] || {})
|
143
|
+
end
|
144
|
+
resp, ptr = self.class.evaluate_batch(@engine, requests.to_json)
|
145
|
+
ptr = FFI::AutoPointer.new(ptr, Client.method(:destroy_string))
|
119
146
|
data = JSON.parse(resp)
|
120
|
-
raise
|
121
|
-
|
122
|
-
data['result']
|
147
|
+
raise EvaluationError, data['error_message'] if data['status'] != 'success'
|
148
|
+
|
149
|
+
responses = (data['result']['responses'] || []).map do |r|
|
150
|
+
case r['type']
|
151
|
+
when 'VARIANT_EVALUATION_RESPONSE_TYPE'
|
152
|
+
v = r['variant_evaluation_response']
|
153
|
+
VariantEvaluationResponse.new(
|
154
|
+
flag_key: v['flag_key'],
|
155
|
+
match: v['match'],
|
156
|
+
reason: v['reason'],
|
157
|
+
variant_key: v['variant_key'],
|
158
|
+
variant_attachment: v['variant_attachment'],
|
159
|
+
segment_keys: v['segment_keys'] || []
|
160
|
+
)
|
161
|
+
when 'BOOLEAN_EVALUATION_RESPONSE_TYPE'
|
162
|
+
b = r['boolean_evaluation_response']
|
163
|
+
BooleanEvaluationResponse.new(
|
164
|
+
flag_key: b['flag_key'],
|
165
|
+
enabled: b['enabled'],
|
166
|
+
reason: b['reason'],
|
167
|
+
segment_keys: b['segment_keys'] || []
|
168
|
+
)
|
169
|
+
when 'ERROR_EVALUATION_RESPONSE_TYPE'
|
170
|
+
e = r['error_evaluation_response']
|
171
|
+
ErrorEvaluationResponse.new(
|
172
|
+
flag_key: e['flag_key'],
|
173
|
+
namespace_key: e['namespace_key'],
|
174
|
+
reason: e['reason'],
|
175
|
+
error_message: e['error_message']
|
176
|
+
)
|
177
|
+
else
|
178
|
+
raise EvaluationError, "Unknown response type encountered: #{r['type']}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
BatchEvaluationResponse.new(responses: responses)
|
123
182
|
end
|
124
183
|
|
125
184
|
# List all flags in the namespace
|
126
185
|
def list_flags
|
127
186
|
resp, ptr = self.class.list_flags(@engine)
|
128
|
-
ptr = FFI::AutoPointer.new(ptr,
|
187
|
+
ptr = FFI::AutoPointer.new(ptr, Client.method(:destroy_string))
|
129
188
|
data = JSON.parse(resp)
|
130
189
|
raise Error, data['error_message'] if data['status'] != 'success'
|
131
190
|
|
132
191
|
data['result']
|
133
192
|
end
|
134
193
|
|
194
|
+
# Get the snapshot of the current flag state
|
195
|
+
def snapshot
|
196
|
+
resp, ptr = self.class.get_snapshot(@engine)
|
197
|
+
ptr = FFI::AutoPointer.new(ptr, Client.method(:destroy_string))
|
198
|
+
resp
|
199
|
+
end
|
200
|
+
|
135
201
|
private
|
136
202
|
|
137
|
-
def validate_evaluation_request(
|
138
|
-
if
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
203
|
+
def validate_evaluation_request(flag_key, entity_id, context)
|
204
|
+
raise ValidationError, 'flag_key is required' if flag_key.nil? || flag_key.empty?
|
205
|
+
raise ValidationError, 'entity_id is required' if entity_id.nil? || entity_id.empty?
|
206
|
+
return if context.is_a?(Hash)
|
207
|
+
|
208
|
+
raise ValidationError, 'context must be a Hash<String, String>'
|
143
209
|
end
|
144
210
|
|
145
211
|
def validate_authentication(authentication)
|
146
212
|
return authentication.strategy if authentication.is_a?(AuthenticationStrategy)
|
147
213
|
|
148
|
-
raise
|
214
|
+
raise ValidationError, 'invalid authentication strategy'
|
149
215
|
end
|
150
216
|
|
151
217
|
def validate_fetch_mode(fetch_mode)
|
152
218
|
return fetch_mode if %i[polling streaming].include?(fetch_mode)
|
153
219
|
|
154
|
-
raise
|
220
|
+
raise ValidationError, 'invalid fetch mode'
|
155
221
|
end
|
156
222
|
|
157
223
|
def validate_error_strategy(error_strategy)
|
158
224
|
return error_strategy if %i[fail fallback].include?(error_strategy)
|
159
225
|
|
160
|
-
raise
|
226
|
+
raise ValidationError, 'invalid error strategy'
|
227
|
+
end
|
228
|
+
|
229
|
+
def validate_tls_config(tls_config)
|
230
|
+
return nil if tls_config.nil?
|
231
|
+
return tls_config.to_h if tls_config.is_a?(TlsConfig)
|
232
|
+
|
233
|
+
raise ValidationError, 'invalid tls_config: must be TlsConfig instance'
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Deprecation shim for EvaluationClient
|
238
|
+
class EvaluationClient < Client
|
239
|
+
def initialize(*args, **kwargs)
|
240
|
+
warn '[DEPRECATION] `EvaluationClient` is deprecated. Please use `Client` instead.'
|
241
|
+
super
|
161
242
|
end
|
162
243
|
end
|
163
244
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flipt_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flipt Devs
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Flipt Client Evaluation SDK
|
14
14
|
email:
|
@@ -42,7 +42,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
42
|
requirements:
|
43
43
|
- - ">="
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version: 2.
|
45
|
+
version: 2.7.0
|
46
46
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
47
|
requirements:
|
48
48
|
- - ">="
|