smplkit 3.0.95 → 3.0.97
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/lib/smplkit/account/client.rb +128 -0
- data/lib/smplkit/account/models.rb +71 -0
- data/lib/smplkit/api_support.rb +91 -0
- data/lib/smplkit/audit/buffer.rb +3 -1
- data/lib/smplkit/audit/categories.rb +21 -10
- data/lib/smplkit/audit/client.rb +18 -9
- data/lib/smplkit/audit/event_types.rb +26 -10
- data/lib/smplkit/audit/events.rb +93 -17
- data/lib/smplkit/{management/audit.rb → audit/forwarders.rb} +93 -85
- data/lib/smplkit/audit/models.rb +86 -32
- data/lib/smplkit/audit/resource_types.rb +21 -9
- data/lib/smplkit/buffers.rb +250 -0
- data/lib/smplkit/client.rb +161 -70
- data/lib/smplkit/config/client.rb +874 -186
- data/lib/smplkit/config/helpers.rb +44 -6
- data/lib/smplkit/config/models.rb +114 -7
- data/lib/smplkit/config_resolution.rb +17 -9
- data/lib/smplkit/errors.rb +14 -3
- data/lib/smplkit/flags/client.rb +602 -116
- data/lib/smplkit/flags/models.rb +110 -8
- data/lib/smplkit/flags/types.rb +8 -9
- data/lib/smplkit/jobs/client.rb +306 -0
- data/lib/smplkit/jobs/models.rb +47 -18
- data/lib/smplkit/logging/client.rb +755 -191
- data/lib/smplkit/logging/helpers.rb +5 -1
- data/lib/smplkit/logging/levels.rb +3 -1
- data/lib/smplkit/logging/models.rb +163 -6
- data/lib/smplkit/logging/normalize.rb +3 -1
- data/lib/smplkit/logging/resolution.rb +4 -4
- data/lib/smplkit/logging/sources.rb +1 -1
- data/lib/smplkit/platform/client.rb +597 -0
- data/lib/smplkit/platform/models.rb +282 -0
- data/lib/smplkit/{management → platform}/types.rb +21 -4
- data/lib/smplkit/transport.rb +103 -0
- data/lib/smplkit/ws.rb +1 -1
- data/lib/smplkit.rb +18 -6
- metadata +11 -7
- data/lib/smplkit/management/buffer.rb +0 -198
- data/lib/smplkit/management/client.rb +0 -1074
- data/lib/smplkit/management/jobs.rb +0 -226
- data/lib/smplkit/management/models.rb +0 -178
data/lib/smplkit/flags/client.rb
CHANGED
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "concurrent"
|
|
4
3
|
require "json"
|
|
5
4
|
require "digest"
|
|
6
5
|
|
|
6
|
+
# The Smpl Flags client — one unified +FlagsClient+.
|
|
7
|
+
#
|
|
8
|
+
# Smpl Flags has two surfaces on a single client, mirroring how the config,
|
|
9
|
+
# audit, and jobs clients expose their full surface from one class:
|
|
10
|
+
#
|
|
11
|
+
# * *CRUD surface* — pure CRUD, no live connection: +new_boolean_flag+ /
|
|
12
|
+
# +new_string_flag+ / +new_number_flag+ / +new_json_flag+ constructors, +get+
|
|
13
|
+
# / +list+ / +delete+ CRUD, and the flag-declaration discovery buffer
|
|
14
|
+
# (+register+ / +flush+ / +flush_sync+ / +pending_count+). The client owns the
|
|
15
|
+
# discovery buffer directly.
|
|
16
|
+
# * *Live surface* — lazily connects to your running service on first use: the
|
|
17
|
+
# typed handle declarations (+boolean_flag+ / +string_flag+ / +number_flag+ /
|
|
18
|
+
# +json_flag+) whose +.get+ evaluates against the cached definitions, plus
|
|
19
|
+
# +refresh+ / +stats+ / +on_change+. The first live call transparently flushes
|
|
20
|
+
# discovery, fetches all flag definitions into the local cache, and opens the
|
|
21
|
+
# live-updates WebSocket — no explicit install step.
|
|
22
|
+
#
|
|
23
|
+
# The client supports two construction shapes:
|
|
24
|
+
#
|
|
25
|
+
# * *Wired* into +Smplkit::Client+ — borrows the parent's flags transport for
|
|
26
|
+
# both runtime fetch and CRUD, the parent's shared WebSocket for the live
|
|
27
|
+
# channel, and +client.platform.contexts+ for evaluation-context
|
|
28
|
+
# registration. This is the common path.
|
|
29
|
+
# * *Standalone* — +FlagsClient.new(api_key: ..., base_url: ..., ...)+ builds
|
|
30
|
+
# and owns its own flags transport and a contexts client (against its own app
|
|
31
|
+
# transport), and on first live use opens and owns its own WebSocket. +close+
|
|
32
|
+
# tears down only the owned transports and owned WebSocket.
|
|
7
33
|
module Smplkit
|
|
8
34
|
module Flags
|
|
9
35
|
# Describes a flag definition change. Frozen — fields are set at construction.
|
|
36
|
+
#
|
|
37
|
+
# @!attribute [r] id
|
|
38
|
+
# @return [String] id of the flag whose definition changed.
|
|
39
|
+
# @!attribute [r] source
|
|
40
|
+
# @return [String] origin of the change (e.g. +"websocket"+ for a live
|
|
41
|
+
# update or +"manual"+ for a refresh).
|
|
42
|
+
# @!attribute [r] deleted
|
|
43
|
+
# @return [Boolean] whether the change was a deletion of the flag.
|
|
10
44
|
class FlagChangeEvent
|
|
11
45
|
attr_reader :id, :source, :deleted
|
|
12
46
|
|
|
@@ -28,6 +62,8 @@ module Smplkit
|
|
|
28
62
|
end
|
|
29
63
|
|
|
30
64
|
# Thread-safe LRU resolution cache with hit/miss stats.
|
|
65
|
+
#
|
|
66
|
+
# @api private
|
|
31
67
|
class ResolutionCache
|
|
32
68
|
DEFAULT_MAX_SIZE = 10_000
|
|
33
69
|
|
|
@@ -41,6 +77,7 @@ module Smplkit
|
|
|
41
77
|
@cache_misses = 0
|
|
42
78
|
end
|
|
43
79
|
|
|
80
|
+
# Return [hit, value]. Moves the key to end on hit.
|
|
44
81
|
def get(cache_key)
|
|
45
82
|
@lock.synchronize do
|
|
46
83
|
if @cache.key?(cache_key)
|
|
@@ -69,106 +106,385 @@ module Smplkit
|
|
|
69
106
|
end
|
|
70
107
|
|
|
71
108
|
# Evaluation statistics for the flags runtime.
|
|
109
|
+
#
|
|
110
|
+
# @!attribute [r] cache_hits
|
|
111
|
+
# @return [Integer] number of flag evaluations served from the local cache.
|
|
112
|
+
# @!attribute [r] cache_misses
|
|
113
|
+
# @return [Integer] number of flag evaluations that missed the cache and
|
|
114
|
+
# were evaluated from the flag definitions.
|
|
72
115
|
FlagStats = Struct.new(:cache_hits, :cache_misses, keyword_init: true)
|
|
73
116
|
|
|
74
|
-
#
|
|
117
|
+
# Convert a list of Context objects to the nested evaluation dict.
|
|
75
118
|
#
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
# @api private
|
|
120
|
+
def self.contexts_to_eval_dict(contexts)
|
|
121
|
+
contexts.to_h { |ctx| [ctx.type, ctx.to_eval_hash] }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Compute a stable hash for a context evaluation dict.
|
|
125
|
+
#
|
|
126
|
+
# @api private
|
|
127
|
+
def self.hash_context(eval_dict)
|
|
128
|
+
Digest::MD5.hexdigest(JSON.generate(deep_sort(eval_dict)))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @api private
|
|
132
|
+
def self.deep_sort(value)
|
|
133
|
+
case value
|
|
134
|
+
when Hash
|
|
135
|
+
value.keys.sort_by(&:to_s).to_h { |k| [k, deep_sort(value[k])] }
|
|
136
|
+
when Array
|
|
137
|
+
value.map { |v| deep_sort(v) }
|
|
138
|
+
else
|
|
139
|
+
value
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Build standalone flags + app transports and resolve the app base URL.
|
|
144
|
+
#
|
|
145
|
+
# +base_url+/+api_key+ are used directly when supplied (the path a top-level
|
|
146
|
+
# client takes after it has already resolved them); otherwise the management
|
|
147
|
+
# config resolver fills in whatever is missing (+~/.smplkit+ / env vars /
|
|
148
|
+
# defaults). The app transport backs the standalone contexts client
|
|
149
|
+
# (evaluation-context registration); the app base URL is returned so a
|
|
150
|
+
# standalone client can open its own WebSocket against the event gateway.
|
|
151
|
+
#
|
|
152
|
+
# @api private
|
|
153
|
+
def self.flags_transport(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
|
|
154
|
+
cfg = ConfigResolution.resolve_client_config(
|
|
155
|
+
profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
|
|
156
|
+
)
|
|
157
|
+
resolved_key = api_key.nil? ? cfg.api_key : api_key
|
|
158
|
+
merged = {}
|
|
159
|
+
merged.merge!(cfg.extra_headers || {})
|
|
160
|
+
merged.merge!(extra_headers || {})
|
|
161
|
+
tcfg = ConfigResolution::ResolvedClientConfig.new(
|
|
162
|
+
api_key: resolved_key, base_domain: cfg.base_domain, scheme: cfg.scheme,
|
|
163
|
+
debug: cfg.debug, extra_headers: merged
|
|
164
|
+
)
|
|
165
|
+
app_url = ConfigResolution.service_url(cfg.scheme, "app", cfg.base_domain)
|
|
166
|
+
flags_http = Transport.build_api_client(SmplkitGeneratedClient::Flags, "flags", tcfg, base_url: base_url)
|
|
167
|
+
app_http = Transport.build_api_client(SmplkitGeneratedClient::App, "app", tcfg)
|
|
168
|
+
[flags_http, app_http, app_url, resolved_key]
|
|
169
|
+
end
|
|
84
170
|
|
|
85
|
-
|
|
171
|
+
# The Smpl Flags client (sync).
|
|
172
|
+
#
|
|
173
|
+
# One client exposes the full surface, reachable as +client.flags+
|
|
174
|
+
# (+Smplkit::Client+) or constructed directly:
|
|
175
|
+
#
|
|
176
|
+
# flags = Smplkit::FlagsClient.new(environment: "production")
|
|
177
|
+
# new_flag = flags.new_boolean_flag("beta", default: false)
|
|
178
|
+
# new_flag.save
|
|
179
|
+
# beta = flags.boolean_flag("beta", default: false)
|
|
180
|
+
# beta.get # => ...
|
|
181
|
+
#
|
|
182
|
+
# The CRUD surface (+new_*+ / +get+ / +list+ / +delete+ and discovery)
|
|
183
|
+
# is pure CRUD. The live surface (+boolean_flag+ / +string_flag+ /
|
|
184
|
+
# +number_flag+ / +json_flag+ / +refresh+ / +stats+ / +on_change+) connects
|
|
185
|
+
# lazily on first use — the first call flushes discovery, fetches all flag
|
|
186
|
+
# definitions into the local cache, and opens the live-updates WebSocket. No
|
|
187
|
+
# explicit install step is required.
|
|
188
|
+
class FlagsClient
|
|
189
|
+
def initialize(api_key = nil, environment: nil, base_url: nil, profile: nil,
|
|
190
|
+
base_domain: nil, scheme: nil, debug: nil, extra_headers: nil,
|
|
191
|
+
parent: nil, transport: nil, contexts: nil, metrics: nil)
|
|
86
192
|
@parent = parent
|
|
87
|
-
@manage = manage
|
|
88
193
|
@metrics = metrics
|
|
89
|
-
@
|
|
90
|
-
@
|
|
91
|
-
@
|
|
92
|
-
|
|
194
|
+
@environment = parent.nil? ? environment : parent._environment
|
|
195
|
+
@service = parent&._service
|
|
196
|
+
@standalone_api_key = nil
|
|
197
|
+
if transport.nil?
|
|
198
|
+
@flags_http, app_http, @app_base_url, @standalone_api_key = Flags.flags_transport(
|
|
199
|
+
api_key: api_key, base_url: base_url, profile: profile,
|
|
200
|
+
base_domain: base_domain, scheme: scheme, debug: debug, extra_headers: extra_headers
|
|
201
|
+
)
|
|
202
|
+
# Standalone: build our own contexts client (and own its app transport).
|
|
203
|
+
@contexts = Platform::ContextsClient.new(app_http, ContextRegistrationBuffer.new)
|
|
204
|
+
else
|
|
205
|
+
@flags_http = transport
|
|
206
|
+
@app_base_url = nil
|
|
207
|
+
# Wired: borrow client.platform.contexts as the evaluation-context
|
|
208
|
+
# registration seam.
|
|
209
|
+
@contexts = contexts
|
|
210
|
+
end
|
|
211
|
+
@api = SmplkitGeneratedClient::Flags::FlagsApi.new(@flags_http)
|
|
212
|
+
|
|
213
|
+
# Discovery buffer is owned by this client (no management delegation).
|
|
214
|
+
@buffer = FlagRegistrationBuffer.new
|
|
93
215
|
|
|
216
|
+
# Live-surface state.
|
|
94
217
|
@flag_store = {}
|
|
95
218
|
@connected = false
|
|
96
219
|
@ws_subscribed = false
|
|
97
|
-
@next_start_attempt_at = 0.0
|
|
98
|
-
@start_retry_delay = INITIAL_START_RETRY_DELAY
|
|
99
220
|
@cache = ResolutionCache.new
|
|
100
221
|
@handles = {}
|
|
101
222
|
@global_listeners = []
|
|
102
223
|
@key_listeners = Hash.new { |h, k| h[k] = [] }
|
|
103
224
|
@ws_manager = nil
|
|
225
|
+
@owns_ws = false
|
|
104
226
|
@lock = Mutex.new
|
|
105
227
|
end
|
|
106
228
|
|
|
107
|
-
|
|
108
|
-
|
|
229
|
+
# ----------------------------------------------------------------
|
|
230
|
+
# Management surface: CRUD (no live connection)
|
|
231
|
+
# ----------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
# Return a new unsaved boolean +BooleanFlag+. Call +save+ to persist.
|
|
234
|
+
#
|
|
235
|
+
# @param id [String] stable flag identifier, unique per account.
|
|
236
|
+
# @param default [Boolean] value served when no environment override or
|
|
237
|
+
# rule applies.
|
|
238
|
+
# @param name [String, nil] human-readable display name; defaults to a
|
|
239
|
+
# title-cased form of +id+.
|
|
240
|
+
# @param description [String, nil] optional free-text description of the flag.
|
|
241
|
+
# @return [BooleanFlag] an unsaved flag; call +save+ to persist it.
|
|
242
|
+
def new_boolean_flag(id, default:, name: nil, description: nil)
|
|
243
|
+
BooleanFlag.new(
|
|
244
|
+
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
245
|
+
type: "BOOLEAN", default: default,
|
|
246
|
+
values: [FlagValue.new(name: "True", value: true), FlagValue.new(name: "False", value: false)],
|
|
247
|
+
description: description
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Return a new unsaved string +StringFlag+. Call +save+ to persist.
|
|
252
|
+
#
|
|
253
|
+
# @param id [String] stable flag identifier, unique per account.
|
|
254
|
+
# @param default [String] value served when no environment override or
|
|
255
|
+
# rule applies.
|
|
256
|
+
# @param name [String, nil] human-readable display name; defaults to a
|
|
257
|
+
# title-cased form of +id+.
|
|
258
|
+
# @param description [String, nil] optional free-text description of the flag.
|
|
259
|
+
# @param values [Array<FlagValue>, nil] optional list of allowed values
|
|
260
|
+
# constraining what the flag may serve; when omitted the flag is
|
|
261
|
+
# unconstrained.
|
|
262
|
+
# @return [StringFlag] an unsaved flag; call +save+ to persist it.
|
|
263
|
+
def new_string_flag(id, default:, name: nil, description: nil, values: nil)
|
|
264
|
+
StringFlag.new(
|
|
265
|
+
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
266
|
+
type: "STRING", default: default, values: values, description: description
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Return a new unsaved numeric +NumberFlag+. Call +save+ to persist.
|
|
271
|
+
#
|
|
272
|
+
# @param id [String] stable flag identifier, unique per account.
|
|
273
|
+
# @param default [Numeric] value served when no environment override or
|
|
274
|
+
# rule applies.
|
|
275
|
+
# @param name [String, nil] human-readable display name; defaults to a
|
|
276
|
+
# title-cased form of +id+.
|
|
277
|
+
# @param description [String, nil] optional free-text description of the flag.
|
|
278
|
+
# @param values [Array<FlagValue>, nil] optional list of allowed values
|
|
279
|
+
# constraining what the flag may serve; when omitted the flag is
|
|
280
|
+
# unconstrained.
|
|
281
|
+
# @return [NumberFlag] an unsaved flag; call +save+ to persist it.
|
|
282
|
+
def new_number_flag(id, default:, name: nil, description: nil, values: nil)
|
|
283
|
+
NumberFlag.new(
|
|
284
|
+
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
285
|
+
type: "NUMERIC", default: default, values: values, description: description
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Return a new unsaved JSON +JsonFlag+. Call +save+ to persist.
|
|
290
|
+
#
|
|
291
|
+
# @param id [String] stable flag identifier, unique per account.
|
|
292
|
+
# @param default [Hash] value served when no environment override or
|
|
293
|
+
# rule applies.
|
|
294
|
+
# @param name [String, nil] human-readable display name; defaults to a
|
|
295
|
+
# title-cased form of +id+.
|
|
296
|
+
# @param description [String, nil] optional free-text description of the flag.
|
|
297
|
+
# @param values [Array<FlagValue>, nil] optional list of allowed values
|
|
298
|
+
# constraining what the flag may serve; when omitted the flag is
|
|
299
|
+
# unconstrained.
|
|
300
|
+
# @return [JsonFlag] an unsaved flag; call +save+ to persist it.
|
|
301
|
+
def new_json_flag(id, default:, name: nil, description: nil, values: nil)
|
|
302
|
+
JsonFlag.new(
|
|
303
|
+
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
304
|
+
type: "JSON", default: default, values: values, description: description
|
|
305
|
+
)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Fetch the editable +Flag+ resource by id.
|
|
309
|
+
#
|
|
310
|
+
# @param id [String] identifier of the flag to fetch.
|
|
311
|
+
# @return [Flag] the flag, ready to mutate and +save+.
|
|
312
|
+
# @raise [Smplkit::NotFoundError] no flag with that id exists for the account.
|
|
313
|
+
def get(id)
|
|
314
|
+
response = ApiSupport::ErrorMapping.call { @api.get_flag(id) }
|
|
315
|
+
model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
|
|
109
316
|
end
|
|
110
317
|
|
|
111
|
-
|
|
112
|
-
|
|
318
|
+
# List flags for the authenticated account.
|
|
319
|
+
#
|
|
320
|
+
# @param page_number [Integer, nil] 1-based page index to fetch; when
|
|
321
|
+
# omitted the server default applies.
|
|
322
|
+
# @param page_size [Integer, nil] number of flags per page; when omitted
|
|
323
|
+
# the server default applies.
|
|
324
|
+
# @return [Array<Flag>] the flags on the requested page.
|
|
325
|
+
def list(page_number: nil, page_size: nil)
|
|
326
|
+
opts = {}
|
|
327
|
+
opts[:page_number] = page_number unless page_number.nil?
|
|
328
|
+
opts[:page_size] = page_size unless page_size.nil?
|
|
329
|
+
response = ApiSupport::ErrorMapping.call { @api.list_flags(opts) }
|
|
330
|
+
(response.data || []).map { |r| model_from_resource(ApiSupport::ResourceShim.from_model(r)) }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Delete a flag by id.
|
|
334
|
+
#
|
|
335
|
+
# @param id [String] identifier of the flag to delete.
|
|
336
|
+
# @return [void]
|
|
337
|
+
# @raise [Smplkit::NotFoundError] no flag with that id exists for the account.
|
|
338
|
+
def delete(id)
|
|
339
|
+
ApiSupport::ErrorMapping.call { @api.delete_flag(id) }
|
|
340
|
+
nil
|
|
113
341
|
end
|
|
114
342
|
|
|
115
|
-
def
|
|
116
|
-
|
|
343
|
+
def _create_flag(flag)
|
|
344
|
+
response = ApiSupport::ErrorMapping.call { @api.create_flag(flag_body(flag)) }
|
|
345
|
+
model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
|
|
117
346
|
end
|
|
118
347
|
|
|
119
|
-
def
|
|
120
|
-
|
|
348
|
+
def _update_flag(flag)
|
|
349
|
+
response = ApiSupport::ErrorMapping.call { @api.update_flag(flag.id, flag_body(flag)) }
|
|
350
|
+
model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
|
|
121
351
|
end
|
|
122
352
|
|
|
123
|
-
#
|
|
353
|
+
# ----------------------------------------------------------------
|
|
354
|
+
# Management surface: discovery buffer (owned directly)
|
|
355
|
+
# ----------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
# Buffer flag declarations for bulk-discovery upload; optionally flush now.
|
|
124
358
|
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
359
|
+
# @param items [FlagDeclaration, Array<FlagDeclaration>] a single
|
|
360
|
+
# declaration or an array of them to queue.
|
|
361
|
+
# @param flush [Boolean] when true, send the buffered declarations
|
|
362
|
+
# immediately via +flush+ before returning. When false (the default),
|
|
363
|
+
# they stay buffered and are sent on the next flush — automatic once the
|
|
364
|
+
# buffer reaches its batch size, or on the first live call.
|
|
365
|
+
# @return [void]
|
|
366
|
+
def register(items, flush: false)
|
|
367
|
+
batch = items.is_a?(Array) ? items : [items]
|
|
368
|
+
batch.each { |d| @buffer.add(d) }
|
|
369
|
+
if flush
|
|
370
|
+
self.flush
|
|
371
|
+
return
|
|
372
|
+
end
|
|
373
|
+
return unless @buffer.pending_count >= FLAG_BATCH_FLUSH_SIZE
|
|
374
|
+
|
|
375
|
+
Thread.new { threshold_flush }
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# POST pending declarations to the flags bulk endpoint.
|
|
128
379
|
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
380
|
+
# Items remain in the buffer until the request succeeds, so a flush
|
|
381
|
+
# against an unhealthy +flags+ service is automatically retried by the
|
|
382
|
+
# next +flush+ call (periodic background flush, install retry, or final
|
|
383
|
+
# flush on close).
|
|
131
384
|
#
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# backed-off delay (capped at +MAX_START_RETRY_DELAY+ seconds).
|
|
137
|
-
# Evaluations during that window fall back to handle defaults.
|
|
138
|
-
def start
|
|
139
|
-
return if @connected
|
|
140
|
-
return if Process.clock_gettime(Process::CLOCK_MONOTONIC) < @next_start_attempt_at
|
|
385
|
+
# @return [void]
|
|
386
|
+
def flush
|
|
387
|
+
batch = @buffer.peek
|
|
388
|
+
return if batch.empty?
|
|
141
389
|
|
|
142
|
-
|
|
390
|
+
body = build_flag_bulk_request(batch)
|
|
391
|
+
ApiSupport::ErrorMapping.call { @api.bulk_register_flags(body) }
|
|
392
|
+
@buffer.commit(batch.map { |b| b["id"] })
|
|
393
|
+
end
|
|
143
394
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
end
|
|
395
|
+
# Synchronous flush — alias of +flush+ for the periodic-flush path.
|
|
396
|
+
#
|
|
397
|
+
# @return [void]
|
|
398
|
+
def flush_sync
|
|
399
|
+
flush
|
|
400
|
+
end
|
|
151
401
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
402
|
+
# Number of pending flag declarations awaiting flush.
|
|
403
|
+
#
|
|
404
|
+
# @return [Integer] number of pending flag declarations awaiting flush.
|
|
405
|
+
def pending_count
|
|
406
|
+
@buffer.pending_count
|
|
407
|
+
end
|
|
155
408
|
|
|
156
|
-
|
|
157
|
-
|
|
409
|
+
# ----------------------------------------------------------------
|
|
410
|
+
# Live surface: typed flag handles
|
|
411
|
+
# ----------------------------------------------------------------
|
|
158
412
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
413
|
+
# Declare a boolean flag handle for live evaluation. Connects lazily on first use.
|
|
414
|
+
#
|
|
415
|
+
# @param id [String] identifier of the flag to evaluate.
|
|
416
|
+
# @param default [Boolean] value returned by +handle.get+ when the flag is
|
|
417
|
+
# unknown or no environment override or rule applies.
|
|
418
|
+
# @return [BooleanFlag] a handle whose +get+ evaluates against the live cache.
|
|
419
|
+
def boolean_flag(id, default:)
|
|
420
|
+
ensure_connected
|
|
421
|
+
handle = BooleanFlag.new(self, id: id, name: id, type: "BOOLEAN", default: default)
|
|
422
|
+
@handles[id] = handle
|
|
423
|
+
observe_declaration(id, "BOOLEAN", default)
|
|
424
|
+
handle
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Declare a string flag handle for live evaluation. Connects lazily on first use.
|
|
428
|
+
#
|
|
429
|
+
# @param id [String] identifier of the flag to evaluate.
|
|
430
|
+
# @param default [String] value returned by +handle.get+ when the flag is
|
|
431
|
+
# unknown or no environment override or rule applies.
|
|
432
|
+
# @return [StringFlag] a handle whose +get+ evaluates against the live cache.
|
|
433
|
+
def string_flag(id, default:)
|
|
434
|
+
ensure_connected
|
|
435
|
+
handle = StringFlag.new(self, id: id, name: id, type: "STRING", default: default)
|
|
436
|
+
@handles[id] = handle
|
|
437
|
+
observe_declaration(id, "STRING", default)
|
|
438
|
+
handle
|
|
163
439
|
end
|
|
164
440
|
|
|
441
|
+
# Declare a numeric flag handle for live evaluation. Connects lazily on first use.
|
|
442
|
+
#
|
|
443
|
+
# @param id [String] identifier of the flag to evaluate.
|
|
444
|
+
# @param default [Numeric] value returned by +handle.get+ when the flag is
|
|
445
|
+
# unknown or no environment override or rule applies.
|
|
446
|
+
# @return [NumberFlag] a handle whose +get+ evaluates against the live cache.
|
|
447
|
+
def number_flag(id, default:)
|
|
448
|
+
ensure_connected
|
|
449
|
+
handle = NumberFlag.new(self, id: id, name: id, type: "NUMERIC", default: default)
|
|
450
|
+
@handles[id] = handle
|
|
451
|
+
observe_declaration(id, "NUMERIC", default)
|
|
452
|
+
handle
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Declare a JSON flag handle for live evaluation. Connects lazily on first use.
|
|
456
|
+
#
|
|
457
|
+
# @param id [String] identifier of the flag to evaluate.
|
|
458
|
+
# @param default [Hash] value returned by +handle.get+ when the flag is
|
|
459
|
+
# unknown or no environment override or rule applies.
|
|
460
|
+
# @return [JsonFlag] a handle whose +get+ evaluates against the live cache.
|
|
461
|
+
def json_flag(id, default:)
|
|
462
|
+
ensure_connected
|
|
463
|
+
handle = JsonFlag.new(self, id: id, name: id, type: "JSON", default: default)
|
|
464
|
+
@handles[id] = handle
|
|
465
|
+
observe_declaration(id, "JSON", default)
|
|
466
|
+
handle
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# ----------------------------------------------------------------
|
|
470
|
+
# Live surface: refresh / stats / change listeners
|
|
471
|
+
# ----------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
# Re-fetch all flag definitions and clear cache.
|
|
474
|
+
#
|
|
475
|
+
# Connects lazily on first use — no explicit install step.
|
|
476
|
+
#
|
|
477
|
+
# @return [void]
|
|
165
478
|
def refresh
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
fire_change_listeners_all("manual")
|
|
479
|
+
ensure_connected
|
|
480
|
+
do_refresh("manual")
|
|
169
481
|
end
|
|
170
482
|
|
|
483
|
+
# Return evaluation statistics. Connects lazily on first use.
|
|
484
|
+
#
|
|
485
|
+
# @return [FlagStats] the evaluation statistics.
|
|
171
486
|
def stats
|
|
487
|
+
ensure_connected
|
|
172
488
|
FlagStats.new(cache_hits: @cache.cache_hits, cache_misses: @cache.cache_misses)
|
|
173
489
|
end
|
|
174
490
|
|
|
@@ -176,7 +492,15 @@ module Smplkit
|
|
|
176
492
|
#
|
|
177
493
|
# client.flags.on_change { |event| ... } # global
|
|
178
494
|
# client.flags.on_change("checkout-v2") { |e| ... } # flag-scoped
|
|
495
|
+
#
|
|
496
|
+
# Connects lazily on first use — no explicit install step.
|
|
497
|
+
#
|
|
498
|
+
# @param flag_id [String, nil] optional flag id scoping the listener to
|
|
499
|
+
# that flag; when +nil+ a global listener is registered.
|
|
500
|
+
# @yield [FlagChangeEvent] the listener block, invoked on each change.
|
|
501
|
+
# @return [Proc] the registered listener block.
|
|
179
502
|
def on_change(flag_id = nil, &block)
|
|
503
|
+
ensure_connected
|
|
180
504
|
raise ArgumentError, "on_change requires a block" unless block
|
|
181
505
|
|
|
182
506
|
if flag_id.nil?
|
|
@@ -187,25 +511,59 @@ module Smplkit
|
|
|
187
511
|
block
|
|
188
512
|
end
|
|
189
513
|
|
|
190
|
-
|
|
191
|
-
|
|
514
|
+
# Release resources — only those this client owns.
|
|
515
|
+
#
|
|
516
|
+
# Tears down the owned WebSocket (standalone install). A wired client
|
|
517
|
+
# borrows the parent's transport, WebSocket, and contexts client and
|
|
518
|
+
# closes none of them.
|
|
519
|
+
#
|
|
520
|
+
# @return [void]
|
|
521
|
+
def close
|
|
522
|
+
if @owns_ws && @ws_manager
|
|
523
|
+
@ws_manager.stop
|
|
524
|
+
@ws_manager = nil
|
|
525
|
+
@owns_ws = false
|
|
526
|
+
end
|
|
527
|
+
# Owned flags/app transports (standalone construction) release their
|
|
528
|
+
# Faraday connections on GC; there is no explicit shutdown to call.
|
|
529
|
+
nil
|
|
192
530
|
end
|
|
531
|
+
alias _close close
|
|
193
532
|
|
|
194
|
-
|
|
195
|
-
|
|
533
|
+
# Construct, yield to the block, and close on exit.
|
|
534
|
+
#
|
|
535
|
+
# @yield [FlagsClient] the constructed client.
|
|
536
|
+
# @return [Object] the block's return value; closes the client on exit.
|
|
537
|
+
def self.open(**kwargs)
|
|
538
|
+
client = new(**kwargs)
|
|
539
|
+
begin
|
|
540
|
+
yield client
|
|
541
|
+
ensure
|
|
542
|
+
client.close
|
|
543
|
+
end
|
|
544
|
+
end
|
|
196
545
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
546
|
+
# Core evaluation used by flag handles (the +.get+ path).
|
|
547
|
+
#
|
|
548
|
+
# Connects lazily on first use so +flag.get+ works without an explicit
|
|
549
|
+
# install step.
|
|
550
|
+
def _evaluate_handle(flag_id, default, context)
|
|
551
|
+
ensure_connected
|
|
552
|
+
if context
|
|
553
|
+
# Explicit context: register here. (Implicit set_context registers at
|
|
554
|
+
# the entry point, so the request-context branch below doesn't need
|
|
555
|
+
# to.)
|
|
556
|
+
@contexts&.register(context)
|
|
557
|
+
eval_dict = Flags.contexts_to_eval_dict(context)
|
|
558
|
+
else
|
|
559
|
+
contexts = Smplkit.request_context
|
|
560
|
+
eval_dict = contexts.empty? ? {} : Flags.contexts_to_eval_dict(contexts)
|
|
561
|
+
end
|
|
205
562
|
|
|
563
|
+
# Auto-inject service context if set and not already provided.
|
|
206
564
|
eval_dict["service"] = { "key" => @service } if @service && !eval_dict.key?("service")
|
|
207
565
|
|
|
208
|
-
ctx_hash = hash_context(eval_dict)
|
|
566
|
+
ctx_hash = Flags.hash_context(eval_dict)
|
|
209
567
|
cache_key = "#{flag_id}:#{ctx_hash}"
|
|
210
568
|
|
|
211
569
|
hit, cached_value = @cache.get(cache_key)
|
|
@@ -229,62 +587,131 @@ module Smplkit
|
|
|
229
587
|
value
|
|
230
588
|
end
|
|
231
589
|
|
|
590
|
+
# Internal: trigger lazy connect. Used by +Client#wait_until_ready+.
|
|
591
|
+
def _ensure_connected
|
|
592
|
+
ensure_connected
|
|
593
|
+
end
|
|
594
|
+
|
|
232
595
|
private
|
|
233
596
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
597
|
+
# Queue a declared flag with the owned discovery buffer.
|
|
598
|
+
def observe_declaration(flag_id, flag_type, default)
|
|
599
|
+
register(FlagDeclaration.new(
|
|
600
|
+
id: flag_id, type: flag_type, default: default,
|
|
601
|
+
service: @service, environment: @environment
|
|
602
|
+
))
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def threshold_flush
|
|
606
|
+
flush
|
|
607
|
+
rescue StandardError => e
|
|
608
|
+
Smplkit.debug("registration", "flag registration flush failed: #{e.class}: #{e.message}")
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def model_from_resource(resource)
|
|
612
|
+
d = Helpers.flag_dict_from_json(resource)
|
|
613
|
+
klass =
|
|
614
|
+
case d["type"]
|
|
615
|
+
when "BOOLEAN" then BooleanFlag
|
|
616
|
+
when "STRING" then StringFlag
|
|
617
|
+
when "NUMERIC" then NumberFlag
|
|
618
|
+
else JsonFlag
|
|
619
|
+
end
|
|
620
|
+
klass.new(
|
|
621
|
+
self,
|
|
622
|
+
id: d["id"], name: d["name"], type: d["type"], default: d["default"],
|
|
623
|
+
description: d["description"], values: d["values"], environments: d["environments"],
|
|
624
|
+
created_at: (resource["attributes"] || {})["created_at"],
|
|
625
|
+
updated_at: (resource["attributes"] || {})["updated_at"]
|
|
626
|
+
)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# ----------------------------------------------------------------
|
|
630
|
+
# Live surface: lazy connect + transport / WebSocket helpers
|
|
631
|
+
# ----------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
def ensure_ws
|
|
634
|
+
return @parent._ensure_ws unless @parent.nil?
|
|
635
|
+
|
|
636
|
+
if @ws_manager.nil?
|
|
637
|
+
@ws_manager = SharedWebSocket.new(
|
|
638
|
+
app_base_url: @app_base_url, api_key: @standalone_api_key, metrics: @metrics
|
|
639
|
+
)
|
|
640
|
+
@ws_manager.start
|
|
641
|
+
@owns_ws = true
|
|
242
642
|
end
|
|
243
|
-
|
|
643
|
+
@ws_manager
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Open the live connection to the running Smpl Flags service.
|
|
647
|
+
#
|
|
648
|
+
# Flushes any buffered discovery declarations, fetches all flag
|
|
649
|
+
# definitions into the local cache, opens the shared WebSocket, and
|
|
650
|
+
# subscribes to +flag_changed+ / +flag_deleted+ / +flags_changed+ events.
|
|
651
|
+
#
|
|
652
|
+
# Idempotent and internal — every live method calls it on first use, so
|
|
653
|
+
# the live surface auto-connects with no explicit step.
|
|
654
|
+
def ensure_connected
|
|
655
|
+
@parent&._ensure_started
|
|
656
|
+
return if @connected
|
|
657
|
+
|
|
658
|
+
# Flush discovered flags BEFORE fetching definitions so the fetch
|
|
659
|
+
# reflects them. Items stay in the buffer until the POST succeeds.
|
|
660
|
+
begin
|
|
661
|
+
flush
|
|
662
|
+
rescue StandardError => e
|
|
663
|
+
Smplkit.debug("flags", "discovery flush before connect failed: #{e.class}: #{e.message}")
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
fetch_all_flags
|
|
667
|
+
@cache.clear
|
|
668
|
+
@connected = true
|
|
669
|
+
|
|
670
|
+
@ws_manager = ensure_ws
|
|
671
|
+
return if @ws_subscribed
|
|
672
|
+
|
|
673
|
+
@ws_manager.on("flag_changed") { |data| handle_flag_changed(data) }
|
|
674
|
+
@ws_manager.on("flag_deleted") { |data| handle_flag_deleted(data) }
|
|
675
|
+
@ws_manager.on("flags_changed") { |data| handle_flags_changed(data) }
|
|
676
|
+
@ws_subscribed = true
|
|
244
677
|
end
|
|
245
678
|
|
|
246
|
-
def
|
|
247
|
-
|
|
248
|
-
@
|
|
249
|
-
|
|
250
|
-
Smplkit.debug("registration",
|
|
251
|
-
"flags client start failed (will retry in #{delay}s): #{exc.class}: #{exc.message}")
|
|
679
|
+
def do_refresh(_source)
|
|
680
|
+
fetch_all_flags
|
|
681
|
+
@cache.clear
|
|
682
|
+
fire_change_listeners_all("manual")
|
|
252
683
|
end
|
|
253
684
|
|
|
685
|
+
# ----------------------------------------------------------------
|
|
686
|
+
# Internal: flag store
|
|
687
|
+
# ----------------------------------------------------------------
|
|
688
|
+
|
|
254
689
|
def fetch_all_flags
|
|
255
|
-
flags =
|
|
690
|
+
flags = fetch_flags_list
|
|
256
691
|
@flag_store = flags.to_h { |f| [f["id"], f] }
|
|
257
|
-
rescue Smplkit::Error
|
|
258
|
-
raise
|
|
259
|
-
rescue StandardError => e
|
|
260
|
-
raise Smplkit::ConnectionError, "Failed to fetch flags: #{e.message}"
|
|
261
692
|
end
|
|
262
693
|
|
|
263
|
-
def
|
|
264
|
-
|
|
694
|
+
def fetch_flags_list
|
|
695
|
+
rows = ApiSupport::PaginatedFetch.collect { |opts| @api.list_flags(opts) }
|
|
696
|
+
rows.map { |r| Helpers.flag_dict_from_json(ApiSupport::ResourceShim.from_model(r)) }
|
|
265
697
|
end
|
|
266
698
|
|
|
267
|
-
|
|
268
|
-
|
|
699
|
+
# Fetch a single flag by key and return a store-format dict.
|
|
700
|
+
def fetch_flag_single_data(key)
|
|
701
|
+
response = ApiSupport::ErrorMapping.call { @api.get_flag(key) }
|
|
702
|
+
Helpers.flag_dict_from_json(ApiSupport::ResourceShim.from_model(response.data))
|
|
269
703
|
end
|
|
270
704
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
value.keys.sort_by(&:to_s).to_h { |k| [k, deep_sort(value[k])] }
|
|
275
|
-
when Array
|
|
276
|
-
value.map { |v| deep_sort(v) }
|
|
277
|
-
else
|
|
278
|
-
value
|
|
279
|
-
end
|
|
280
|
-
end
|
|
705
|
+
# ----------------------------------------------------------------
|
|
706
|
+
# Internal: event handlers (called by SharedWebSocket)
|
|
707
|
+
# ----------------------------------------------------------------
|
|
281
708
|
|
|
282
709
|
def handle_flag_changed(data)
|
|
283
710
|
key = data["id"]
|
|
284
711
|
return unless key
|
|
285
712
|
|
|
286
713
|
pre = @flag_store[key]&.dup || {}
|
|
287
|
-
new_data =
|
|
714
|
+
new_data = fetch_flag_single_data(key)
|
|
288
715
|
@flag_store[key] = new_data
|
|
289
716
|
@cache.clear
|
|
290
717
|
fire_change_listeners(key, "websocket") if pre != new_data
|
|
@@ -314,6 +741,7 @@ module Smplkit
|
|
|
314
741
|
changed = all_keys.reject { |k| pre_store[k] == post_store[k] }
|
|
315
742
|
return if changed.empty?
|
|
316
743
|
|
|
744
|
+
# Global listener fires once.
|
|
317
745
|
first_event = FlagChangeEvent.new(id: changed.first, source: "websocket")
|
|
318
746
|
@global_listeners.each do |cb|
|
|
319
747
|
cb.call(first_event)
|
|
@@ -321,6 +749,7 @@ module Smplkit
|
|
|
321
749
|
Smplkit.debug("flags", "global listener raised: #{e.class}: #{e.message}")
|
|
322
750
|
end
|
|
323
751
|
|
|
752
|
+
# Per-key listeners fire for each changed key.
|
|
324
753
|
changed.each do |k|
|
|
325
754
|
deleted = pre_store.key?(k) && !post_store.key?(k)
|
|
326
755
|
event = FlagChangeEvent.new(id: k, source: "websocket", deleted: deleted)
|
|
@@ -349,9 +778,9 @@ module Smplkit
|
|
|
349
778
|
|
|
350
779
|
# Evaluate a flag definition against the given context.
|
|
351
780
|
#
|
|
352
|
-
#
|
|
353
|
-
# 1. Look up the environment. If missing, return flag-level default.
|
|
354
|
-
# 2. If disabled, return env default or flag default.
|
|
781
|
+
# Evaluation steps:
|
|
782
|
+
# 1. Look up the environment. If missing, return the flag-level default.
|
|
783
|
+
# 2. If disabled, return the env default or flag default.
|
|
355
784
|
# 3. Iterate rules; first match wins.
|
|
356
785
|
# 4. No match -> env default or flag default.
|
|
357
786
|
def evaluate_flag(flag_def, environment, eval_dict)
|
|
@@ -378,15 +807,70 @@ module Smplkit
|
|
|
378
807
|
|
|
379
808
|
fallback
|
|
380
809
|
end
|
|
810
|
+
|
|
811
|
+
def flag_body(flag)
|
|
812
|
+
SmplkitGeneratedClient::Flags::FlagResponse.new(
|
|
813
|
+
data: SmplkitGeneratedClient::Flags::FlagResource.new(
|
|
814
|
+
type: "flag",
|
|
815
|
+
id: flag.id,
|
|
816
|
+
attributes: SmplkitGeneratedClient::Flags::Flag.new(
|
|
817
|
+
name: flag.name,
|
|
818
|
+
type: flag.type,
|
|
819
|
+
default: flag.default,
|
|
820
|
+
description: flag.description,
|
|
821
|
+
values: flag_values_to_wire(flag.values),
|
|
822
|
+
environments: flag_envs_to_wire(flag.environments)
|
|
823
|
+
)
|
|
824
|
+
)
|
|
825
|
+
)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
def flag_values_to_wire(values)
|
|
829
|
+
# The server requires +values+ to be null (unconstrained) or a
|
|
830
|
+
# non-empty array (constrained) — an empty array is rejected, so an
|
|
831
|
+
# empty/cleared values list is sent as null.
|
|
832
|
+
return nil if values.nil? || values.empty?
|
|
833
|
+
|
|
834
|
+
values.map do |v|
|
|
835
|
+
SmplkitGeneratedClient::Flags::FlagValue.new(name: v.name, value: v.value)
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def flag_envs_to_wire(environments)
|
|
840
|
+
return nil if environments.empty?
|
|
841
|
+
|
|
842
|
+
environments.each_with_object({}) do |(env_key, env_obj), out|
|
|
843
|
+
rules = env_obj.rules.map do |r|
|
|
844
|
+
SmplkitGeneratedClient::Flags::FlagRule.new(
|
|
845
|
+
logic: r.logic, value: r.value, description: r.description
|
|
846
|
+
)
|
|
847
|
+
end
|
|
848
|
+
out[env_key] = SmplkitGeneratedClient::Flags::FlagEnvironment.new(
|
|
849
|
+
enabled: env_obj.enabled, default: env_obj.default, rules: rules
|
|
850
|
+
)
|
|
851
|
+
end
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def build_flag_bulk_request(batch)
|
|
855
|
+
flag_items = batch.map do |entry|
|
|
856
|
+
SmplkitGeneratedClient::Flags::FlagBulkItem.new(
|
|
857
|
+
id: entry["id"], type: entry["type"], default: entry["default"],
|
|
858
|
+
service: entry["service"], environment: entry["environment"]
|
|
859
|
+
)
|
|
860
|
+
end
|
|
861
|
+
SmplkitGeneratedClient::Flags::FlagBulkRequest.new(flags: flag_items)
|
|
862
|
+
end
|
|
381
863
|
end
|
|
382
864
|
|
|
383
865
|
# Vendored minimal JSON Logic evaluator covering the operators the smplkit
|
|
384
866
|
# platform ships in flag rules.
|
|
385
867
|
#
|
|
386
|
-
# Stays in-tree so the Ruby SDK doesn't depend on the +json_logic+ gem
|
|
387
|
-
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
868
|
+
# Stays in-tree so the Ruby SDK doesn't depend on the +json_logic+ gem being
|
|
869
|
+
# correct — the Java SDK followed the same pattern. Operators supported:
|
|
870
|
+
# +==+, +!=+, +<+, +<=+, +>+, +>=+, +in+, +var+, +and+, +or+, +!+, +if+,
|
|
871
|
+
# +missing+, +none+.
|
|
872
|
+
#
|
|
873
|
+
# @api private
|
|
390
874
|
module JsonLogicEvaluator
|
|
391
875
|
module_function
|
|
392
876
|
|
|
@@ -505,4 +989,6 @@ module Smplkit
|
|
|
505
989
|
end
|
|
506
990
|
end
|
|
507
991
|
end
|
|
992
|
+
|
|
993
|
+
FlagsClient = Flags::FlagsClient
|
|
508
994
|
end
|