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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6a5b035934ec3f271e01238a872154920c919c5561b41a98e9b5b853e0bbf90
4
- data.tar.gz: 4cb2d36b85215238a791f283426b6e667adbfbb6e9d00d19d42ecaf209eda25a
3
+ metadata.gz: 02ba6911dc6076bbc0703d4c08a242ba24bb75ce82ce2fbce36822b51981c50e
4
+ data.tar.gz: fd4810c100d71063d4f61d8c4d752f07b616ac6f6d7ce4b31446d4360b766682
5
5
  SHA512:
6
- metadata.gz: 65b002cca3c2b89c9588d36b62c13aa9c7ac07396efd207d67b7dea2b588847ad5946febcf4ac0b82180a2e0725030c6bf4ede11e29f3c49e180d2ce1b7956eb
7
- data.tar.gz: 1987dd10cc71446fb3bdebc79637eaa9fee0cb4af66febe7afc7b5ad6f8bf619920c14f3c607f8535f54a7ff4e8ca356dd9d4b277e2af524e6de1bf0ee3c3a52
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 Only)
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
- # namespace is the first positional argument and is optional here and will have a value of "default" if not specified.
78
- # opts is the second positional argument and is also optional, the structure is:
79
- # {
80
- # "url": "http://localhost:8080",
81
- # "update_interval": 120,
82
- # "authentication": {
83
- # "client_token": "secret"
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::EvaluationClient` constructor accepts two optional arguments:
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
- - `opts`: A hash that supports several options for the client. The structure is:
101
- - `url`: The URL of the upstream Flipt instance. If not provided, the client will default to `http://localhost:8080`.
102
- - `request_timeout`: The timeout (in seconds) for total request time to the upstream Flipt instance. If not provided, the client will default to no timeout. Note: this only affects polling mode. Streaming mode will have no timeout set.
103
- - `update_interval`: The interval (in seconds) in which to fetch new flag state. If not provided, the client will default to 120 seconds.
104
- - `authentication`: The authentication strategy to use when communicating with the upstream Flipt instance. If not provided, the client will default to no authentication. See the [Authentication](#authentication) section for more information.
105
- - `reference`: The [reference](https://docs.flipt.io/guides/user/using-references) to use when fetching flag state. If not provided, reference will not be used.
106
- - `fetch_mode`: The fetch mode to use when fetching flag state. If not provided, the client will default to polling.
107
- - `error_strategy`: The error strategy to use when fetching flag state. If not provide, the client will be default to fail. See the [Error Strategies](#error-strategies) section for more information.
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 `FliptEvaluationClient` supports the following authentication strategies:
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:
@@ -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.3.0')
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
 
@@ -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 *initialize_engine(const char *namespace_, const char *opts);
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 for the given namespace.
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 free the memory occupied by the engine.
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 the string and free the memory.
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 destroy_string(char *ptr);
109
+ void destroy_string_ffi(char *ptr);
55
110
 
56
- // Add missing external declarations for Rust functions
57
- extern void* initialize_engine_ffi(const char* namespace, const char* options);
58
- extern const char* evaluate_boolean_ffi(void* engine, const char* request);
59
- extern const char* evaluate_variant_ffi(void* engine, const char* request);
60
- extern const char* evaluate_batch_ffi(void* engine, const char* request);
61
- extern const char* list_flags_ffi(void* engine);
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flipt
4
- VERSION = '0.17.0'
4
+ VERSION = '1.1.0'
5
5
  end
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
- # EvaluationClient is a Ruby Client Side Evaluation Library for Flipt
12
- class EvaluationClient
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 *namespace, const char *opts);
36
- attach_function :initialize_engine, %i[string string], :pointer
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 (https://flipt.io/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
- def initialize(namespace = 'default', opts = {})
63
- @namespace = namespace
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(namespace, opts.to_json)
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 evaluation_request [Hash] evaluation request
80
- # @option evaluation_request [String] :entity_id entity id
81
- # @option evaluation_request [String] :flag_key flag key
82
- def evaluate_variant(evaluation_request = {})
83
- validate_evaluation_request(evaluation_request)
84
- resp, ptr = self.class.evaluate_variant(@engine, evaluation_request.to_json)
85
- ptr = FFI::AutoPointer.new(ptr, EvaluationClient.method(:destroy_string))
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 Error, data['error_message'] if data['status'] != 'success'
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 evaluation_request [Hash] evaluation request
95
- # @option evaluation_request [String] :entity_id entity id
96
- # @option evaluation_request [String] :flag_key flag key
97
- def evaluate_boolean(evaluation_request = {})
98
- validate_evaluation_request(evaluation_request)
99
- resp, ptr = self.class.evaluate_boolean(@engine, evaluation_request.to_json)
100
- ptr = FFI::AutoPointer.new(ptr, EvaluationClient.method(:destroy_string))
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 Error, data['error_message'] if data['status'] != 'success'
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 batch_evaluation_request [Array<Hash>] batch evaluation request
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(batch_evaluation_request = [])
113
- batch_evaluation_request.each do |request|
114
- validate_evaluation_request(request)
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
- resp, ptr = self.class.evaluate_batch(@engine, batch_evaluation_request.to_json)
118
- ptr = FFI::AutoPointer.new(ptr, EvaluationClient.method(:destroy_string))
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 Error, data['error_message'] if data['status'] != 'success'
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, EvaluationClient.method(:destroy_string))
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(evaluation_request)
138
- if evaluation_request[:entity_id].nil? || evaluation_request[:entity_id].empty?
139
- raise ArgumentError, 'entity_id is required'
140
- elsif evaluation_request[:flag_key].nil? || evaluation_request[:flag_key].empty?
141
- raise ArgumentError, 'flag_key is required'
142
- end
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 ArgumentError, 'invalid authentication strategy'
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 ArgumentError, 'invalid fetch mode'
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 ArgumentError, 'invalid error strategy'
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: 0.17.0
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-03-19 00:00:00.000000000 Z
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.3.0
45
+ version: 2.7.0
46
46
  required_rubygems_version: !ruby/object:Gem::Requirement
47
47
  requirements:
48
48
  - - ">="