smplkit 3.0.96 → 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 +9 -2
- data/lib/smplkit/account/models.rb +18 -0
- data/lib/smplkit/api_support.rb +8 -0
- data/lib/smplkit/audit/buffer.rb +3 -1
- data/lib/smplkit/audit/categories.rb +21 -10
- data/lib/smplkit/audit/client.rb +16 -6
- data/lib/smplkit/audit/event_types.rb +26 -10
- data/lib/smplkit/audit/events.rb +93 -17
- data/lib/smplkit/audit/forwarders.rb +27 -16
- data/lib/smplkit/audit/models.rb +46 -31
- data/lib/smplkit/audit/resource_types.rb +21 -9
- data/lib/smplkit/buffers.rb +15 -0
- data/lib/smplkit/client.rb +38 -6
- data/lib/smplkit/config/client.rb +279 -26
- data/lib/smplkit/config/helpers.rb +44 -6
- data/lib/smplkit/config/models.rb +114 -7
- data/lib/smplkit/config_resolution.rb +6 -4
- data/lib/smplkit/errors.rb +6 -3
- data/lib/smplkit/flags/client.rb +135 -7
- data/lib/smplkit/flags/models.rb +110 -8
- data/lib/smplkit/flags/types.rb +2 -2
- data/lib/smplkit/jobs/client.rb +47 -26
- data/lib/smplkit/jobs/models.rb +47 -18
- data/lib/smplkit/logging/client.rb +125 -16
- data/lib/smplkit/logging/helpers.rb +4 -1
- data/lib/smplkit/logging/levels.rb +3 -1
- data/lib/smplkit/logging/models.rb +71 -5
- data/lib/smplkit/logging/normalize.rb +3 -1
- data/lib/smplkit/logging/resolution.rb +4 -4
- data/lib/smplkit/platform/client.rb +132 -7
- data/lib/smplkit/platform/models.rb +103 -3
- data/lib/smplkit/platform/types.rb +14 -0
- data/lib/smplkit/transport.rb +5 -1
- data/lib/smplkit/ws.rb +1 -1
- metadata +1 -1
|
@@ -16,6 +16,13 @@ module Smplkit
|
|
|
16
16
|
class ConfigItem
|
|
17
17
|
attr_accessor :name, :value, :type, :description
|
|
18
18
|
|
|
19
|
+
# Create a typed config item.
|
|
20
|
+
#
|
|
21
|
+
# @param name [String] The item key within its config.
|
|
22
|
+
# @param value [Object] The item's value.
|
|
23
|
+
# @param type [String] The item value type — one of +"STRING"+,
|
|
24
|
+
# +"NUMBER"+, +"BOOLEAN"+, or +"JSON"+.
|
|
25
|
+
# @param description [String, nil] Optional human-readable description.
|
|
19
26
|
def initialize(name:, value:, type:, description: nil)
|
|
20
27
|
@name = name
|
|
21
28
|
@value = value
|
|
@@ -23,6 +30,8 @@ module Smplkit
|
|
|
23
30
|
@description = description
|
|
24
31
|
end
|
|
25
32
|
|
|
33
|
+
# @return [Hash{String => Object}] The item as a plain Hash, omitting a
|
|
34
|
+
# +nil+ description.
|
|
26
35
|
def to_h
|
|
27
36
|
{ "name" => @name, "value" => @value, "type" => @type, "description" => @description }.compact
|
|
28
37
|
end
|
|
@@ -42,10 +51,13 @@ module Smplkit
|
|
|
42
51
|
# setters with +environment:+ (e.g.
|
|
43
52
|
# +cfg.set_string("k", "v", environment: "production")+).
|
|
44
53
|
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
54
|
+
# An override stores only the raw value; the declared type and description
|
|
55
|
+
# come from the base item, so an item's type and description are ignored
|
|
56
|
+
# when an environment override is supplied.
|
|
48
57
|
class ConfigEnvironment
|
|
58
|
+
# @param values [Hash{String => Object}, nil] Initial overrides as a flat
|
|
59
|
+
# +{key => raw_value}+ map. A legacy +{key => {"value" => raw}}+ wrapper
|
|
60
|
+
# is unwrapped to the raw value.
|
|
49
61
|
def initialize(values: nil)
|
|
50
62
|
@values_raw = {}
|
|
51
63
|
return unless values
|
|
@@ -58,6 +70,9 @@ module Smplkit
|
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
# Returns overrides as a plain Hash +{ "key" => raw_value }+.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash{String => Object}] A read-only shallow copy of the
|
|
75
|
+
# overrides.
|
|
61
76
|
def values
|
|
62
77
|
@values_raw.dup
|
|
63
78
|
end
|
|
@@ -65,6 +80,9 @@ module Smplkit
|
|
|
65
80
|
# Returns overrides as a plain Hash +{ "key" => raw_value }+. Retained
|
|
66
81
|
# as a separate accessor for backward compatibility; identical to
|
|
67
82
|
# +values+ now that env overrides are stored flat.
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash{String => Object}] A read-only shallow copy of the
|
|
85
|
+
# overrides.
|
|
68
86
|
def values_raw
|
|
69
87
|
@values_raw.dup
|
|
70
88
|
end
|
|
@@ -82,6 +100,20 @@ module Smplkit
|
|
|
82
100
|
class Config
|
|
83
101
|
attr_accessor :id, :key, :name, :description, :parent_id, :created_at, :updated_at
|
|
84
102
|
|
|
103
|
+
# @param client [ConfigClient, nil] The owning client, or +nil+ for a
|
|
104
|
+
# detached model that cannot save or delete.
|
|
105
|
+
# @param key [String] The config identifier (slug).
|
|
106
|
+
# @param id [String, nil] The server-assigned id, or +nil+ for an unsaved
|
|
107
|
+
# config.
|
|
108
|
+
# @param name [String, nil] Display name.
|
|
109
|
+
# @param description [String, nil] Optional description.
|
|
110
|
+
# @param parent_id [String, nil] Parent config id (slug), or +nil+ for a
|
|
111
|
+
# root config.
|
|
112
|
+
# @param items [Array<ConfigItem>, nil] Base items.
|
|
113
|
+
# @param environments [Hash{String => ConfigEnvironment}, nil]
|
|
114
|
+
# Per-environment overrides keyed by environment id.
|
|
115
|
+
# @param created_at [Object, nil] Creation timestamp.
|
|
116
|
+
# @param updated_at [Object, nil] Last-modified timestamp.
|
|
85
117
|
def initialize(client = nil, key:, id: nil, name: nil, description: nil,
|
|
86
118
|
parent_id: nil, items: nil, environments: nil,
|
|
87
119
|
created_at: nil, updated_at: nil)
|
|
@@ -97,14 +129,25 @@ module Smplkit
|
|
|
97
129
|
@updated_at = updated_at
|
|
98
130
|
end
|
|
99
131
|
|
|
132
|
+
# @return [Array<ConfigItem>] A read-only shallow copy of the base items.
|
|
100
133
|
def items
|
|
101
134
|
@items.dup
|
|
102
135
|
end
|
|
103
136
|
|
|
137
|
+
# @return [Hash{String => ConfigEnvironment}] A read-only shallow copy of
|
|
138
|
+
# the per-environment overrides keyed by environment id.
|
|
104
139
|
def environments
|
|
105
140
|
@environments.dup
|
|
106
141
|
end
|
|
107
142
|
|
|
143
|
+
# Persist this config to the server. Creates a new config if unsaved, or
|
|
144
|
+
# updates the existing one.
|
|
145
|
+
#
|
|
146
|
+
# @return [self]
|
|
147
|
+
# @raise [Smplkit::NotFoundError] If the config no longer exists
|
|
148
|
+
# (update).
|
|
149
|
+
# @raise [Smplkit::ValidationError] If the server rejects the request.
|
|
150
|
+
# @raise [RuntimeError] If the model was constructed without a client.
|
|
108
151
|
def save
|
|
109
152
|
raise "Config was constructed without a client; cannot save" if @client.nil?
|
|
110
153
|
|
|
@@ -119,6 +162,10 @@ module Smplkit
|
|
|
119
162
|
end
|
|
120
163
|
alias save! save
|
|
121
164
|
|
|
165
|
+
# Delete this config from the server.
|
|
166
|
+
#
|
|
167
|
+
# @return [void]
|
|
168
|
+
# @raise [RuntimeError] If the model was constructed without a client.
|
|
122
169
|
def delete
|
|
123
170
|
raise "Config was constructed without a client; cannot delete" if @client.nil?
|
|
124
171
|
|
|
@@ -126,26 +173,86 @@ module Smplkit
|
|
|
126
173
|
end
|
|
127
174
|
alias delete! delete
|
|
128
175
|
|
|
176
|
+
# Set (or replace) an item. When +environment:+ is given, sets an
|
|
177
|
+
# override on that environment.
|
|
178
|
+
#
|
|
179
|
+
# An environment override stores only the raw value; the declared type and
|
|
180
|
+
# description come from the base item, so the +ConfigItem+'s type and
|
|
181
|
+
# description are ignored when +environment:+ is supplied.
|
|
182
|
+
#
|
|
183
|
+
# @param item [ConfigItem] The item to set; its +name+ is the item key.
|
|
184
|
+
# @param environment [String, nil] When given, set the value as an
|
|
185
|
+
# override on this environment rather than on the base config.
|
|
186
|
+
# @return [self]
|
|
187
|
+
def set(item, environment: nil)
|
|
188
|
+
set_typed(item.name, item.value, item.type, environment: environment, description: item.description)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Convenience: set a STRING item (or environment override).
|
|
192
|
+
#
|
|
193
|
+
# @param name [String] The item key to set.
|
|
194
|
+
# @param value [String] The string value.
|
|
195
|
+
# @param environment [String, nil] When given, set the value as an
|
|
196
|
+
# override on this environment rather than on the base config.
|
|
197
|
+
# @param description [String, nil] Optional human-readable description.
|
|
198
|
+
# Ignored when setting an environment override.
|
|
199
|
+
# @return [self]
|
|
129
200
|
def set_string(name, value, environment: nil, description: nil)
|
|
130
201
|
set_typed(name, value, ItemType::STRING, environment: environment, description: description)
|
|
131
202
|
end
|
|
132
203
|
|
|
204
|
+
# Convenience: set a NUMBER item (or environment override).
|
|
205
|
+
#
|
|
206
|
+
# @param name [String] The item key to set.
|
|
207
|
+
# @param value [Integer, Float] The numeric value.
|
|
208
|
+
# @param environment [String, nil] When given, set the value as an
|
|
209
|
+
# override on this environment rather than on the base config.
|
|
210
|
+
# @param description [String, nil] Optional human-readable description.
|
|
211
|
+
# Ignored when setting an environment override.
|
|
212
|
+
# @return [self]
|
|
133
213
|
def set_number(name, value, environment: nil, description: nil)
|
|
134
214
|
set_typed(name, value, ItemType::NUMBER, environment: environment, description: description)
|
|
135
215
|
end
|
|
136
216
|
|
|
217
|
+
# Convenience: set a BOOLEAN item (or environment override).
|
|
218
|
+
#
|
|
219
|
+
# @param name [String] The item key to set.
|
|
220
|
+
# @param value [Boolean] The boolean value.
|
|
221
|
+
# @param environment [String, nil] When given, set the value as an
|
|
222
|
+
# override on this environment rather than on the base config.
|
|
223
|
+
# @param description [String, nil] Optional human-readable description.
|
|
224
|
+
# Ignored when setting an environment override.
|
|
225
|
+
# @return [self]
|
|
137
226
|
def set_boolean(name, value, environment: nil, description: nil)
|
|
138
227
|
set_typed(name, value, ItemType::BOOLEAN, environment: environment, description: description)
|
|
139
228
|
end
|
|
140
229
|
|
|
230
|
+
# Convenience: set a JSON item (or environment override).
|
|
231
|
+
#
|
|
232
|
+
# @param name [String] The item key to set.
|
|
233
|
+
# @param value [Object] Any JSON-serializable value (Hash, Array, or
|
|
234
|
+
# primitive).
|
|
235
|
+
# @param environment [String, nil] When given, set the value as an
|
|
236
|
+
# override on this environment rather than on the base config.
|
|
237
|
+
# @param description [String, nil] Optional human-readable description.
|
|
238
|
+
# Ignored when setting an environment override.
|
|
239
|
+
# @return [self]
|
|
141
240
|
def set_json(name, value, environment: nil, description: nil)
|
|
142
241
|
set_typed(name, value, ItemType::JSON, environment: environment, description: description)
|
|
143
242
|
end
|
|
144
243
|
|
|
145
|
-
|
|
244
|
+
# Remove an item by name. When +environment:+ is given, removes the
|
|
245
|
+
# per-environment override only. Removing an item that isn't present is a
|
|
246
|
+
# no-op.
|
|
247
|
+
#
|
|
248
|
+
# @param name [String] The item key to remove.
|
|
249
|
+
# @param environment [String, nil] When given, remove only this
|
|
250
|
+
# environment's override for +name+, leaving the base item intact.
|
|
251
|
+
# @return [self]
|
|
252
|
+
def remove(name, environment: nil)
|
|
146
253
|
if environment
|
|
147
254
|
env = @environments[environment]
|
|
148
|
-
return unless env
|
|
255
|
+
return self unless env
|
|
149
256
|
|
|
150
257
|
raw = env.values_raw
|
|
151
258
|
raw.delete(name)
|
|
@@ -181,8 +288,8 @@ module Smplkit
|
|
|
181
288
|
@items << ConfigItem.new(name: name, value: value, type: type, description: description)
|
|
182
289
|
end
|
|
183
290
|
else
|
|
184
|
-
#
|
|
185
|
-
#
|
|
291
|
+
# Env overrides carry the raw value only — type and description live
|
|
292
|
+
# on the base item, not on the override.
|
|
186
293
|
env = (@environments[environment] ||= ConfigEnvironment.new)
|
|
187
294
|
raw = env.values_raw
|
|
188
295
|
raw[name] = value
|
|
@@ -4,6 +4,8 @@ require_relative "errors"
|
|
|
4
4
|
|
|
5
5
|
module Smplkit
|
|
6
6
|
# SDK configuration resolution: defaults -> file -> env vars -> constructor args.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
7
9
|
module ConfigResolution
|
|
8
10
|
CONFIG_KEYS = {
|
|
9
11
|
"api_key" => "SMPLKIT_API_KEY",
|
|
@@ -33,7 +35,7 @@ module Smplkit
|
|
|
33
35
|
keyword_init: true
|
|
34
36
|
)
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
ResolvedClientConfig = Struct.new(
|
|
37
39
|
:api_key, :base_domain, :scheme, :debug, :extra_headers,
|
|
38
40
|
keyword_init: true
|
|
39
41
|
)
|
|
@@ -157,8 +159,8 @@ module Smplkit
|
|
|
157
159
|
)
|
|
158
160
|
end
|
|
159
161
|
|
|
160
|
-
def
|
|
161
|
-
|
|
162
|
+
def resolve_client_config(profile: nil, api_key: nil, base_domain: nil,
|
|
163
|
+
scheme: nil, debug: nil, home_dir: nil)
|
|
162
164
|
resolved = {
|
|
163
165
|
"api_key" => nil,
|
|
164
166
|
"base_domain" => "smplkit.com",
|
|
@@ -191,7 +193,7 @@ module Smplkit
|
|
|
191
193
|
|
|
192
194
|
missing_required(resolved, "api_key", active_profile)
|
|
193
195
|
|
|
194
|
-
|
|
196
|
+
ResolvedClientConfig.new(
|
|
195
197
|
api_key: resolved["api_key"].to_s,
|
|
196
198
|
base_domain: resolved["base_domain"].to_s,
|
|
197
199
|
scheme: resolved["scheme"].to_s,
|
data/lib/smplkit/errors.rb
CHANGED
|
@@ -6,9 +6,9 @@ module Smplkit
|
|
|
6
6
|
# A single error object from the server's JSON:API +errors+ array.
|
|
7
7
|
#
|
|
8
8
|
# +code+ is the application-specific machine-readable error code (e.g.
|
|
9
|
-
# +environment_unmanaged+);
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# +environment_unmanaged+); smplkit sets this on every error so callers
|
|
10
|
+
# can branch without string-matching the human +detail+. +meta+ carries
|
|
11
|
+
# additional structured context
|
|
12
12
|
# (e.g. <tt>{"environment" => "staging"}</tt>).
|
|
13
13
|
class ApiErrorDetail
|
|
14
14
|
attr_reader :status, :code, :title, :detail, :source, :meta
|
|
@@ -62,6 +62,7 @@ module Smplkit
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# @api private — Build a human-readable message from a list of error details.
|
|
65
66
|
def self.derive_message(errors)
|
|
66
67
|
return "An API error occurred" if errors.nil? || errors.empty?
|
|
67
68
|
|
|
@@ -92,6 +93,8 @@ module Smplkit
|
|
|
92
93
|
# this.
|
|
93
94
|
class NotInstalledError < Error; end
|
|
94
95
|
|
|
96
|
+
# @api private — Internal helpers that parse JSON:API error bodies and map
|
|
97
|
+
# HTTP status codes onto the error classes above.
|
|
95
98
|
module Errors
|
|
96
99
|
module_function
|
|
97
100
|
|
data/lib/smplkit/flags/client.rb
CHANGED
|
@@ -8,7 +8,7 @@ require "digest"
|
|
|
8
8
|
# Smpl Flags has two surfaces on a single client, mirroring how the config,
|
|
9
9
|
# audit, and jobs clients expose their full surface from one class:
|
|
10
10
|
#
|
|
11
|
-
# * *
|
|
11
|
+
# * *CRUD surface* — pure CRUD, no live connection: +new_boolean_flag+ /
|
|
12
12
|
# +new_string_flag+ / +new_number_flag+ / +new_json_flag+ constructors, +get+
|
|
13
13
|
# / +list+ / +delete+ CRUD, and the flag-declaration discovery buffer
|
|
14
14
|
# (+register+ / +flush+ / +flush_sync+ / +pending_count+). The client owns the
|
|
@@ -33,6 +33,14 @@ require "digest"
|
|
|
33
33
|
module Smplkit
|
|
34
34
|
module Flags
|
|
35
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.
|
|
36
44
|
class FlagChangeEvent
|
|
37
45
|
attr_reader :id, :source, :deleted
|
|
38
46
|
|
|
@@ -54,6 +62,8 @@ module Smplkit
|
|
|
54
62
|
end
|
|
55
63
|
|
|
56
64
|
# Thread-safe LRU resolution cache with hit/miss stats.
|
|
65
|
+
#
|
|
66
|
+
# @api private
|
|
57
67
|
class ResolutionCache
|
|
58
68
|
DEFAULT_MAX_SIZE = 10_000
|
|
59
69
|
|
|
@@ -96,18 +106,29 @@ module Smplkit
|
|
|
96
106
|
end
|
|
97
107
|
|
|
98
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.
|
|
99
115
|
FlagStats = Struct.new(:cache_hits, :cache_misses, keyword_init: true)
|
|
100
116
|
|
|
101
117
|
# Convert a list of Context objects to the nested evaluation dict.
|
|
118
|
+
#
|
|
119
|
+
# @api private
|
|
102
120
|
def self.contexts_to_eval_dict(contexts)
|
|
103
121
|
contexts.to_h { |ctx| [ctx.type, ctx.to_eval_hash] }
|
|
104
122
|
end
|
|
105
123
|
|
|
106
124
|
# Compute a stable hash for a context evaluation dict.
|
|
125
|
+
#
|
|
126
|
+
# @api private
|
|
107
127
|
def self.hash_context(eval_dict)
|
|
108
128
|
Digest::MD5.hexdigest(JSON.generate(deep_sort(eval_dict)))
|
|
109
129
|
end
|
|
110
130
|
|
|
131
|
+
# @api private
|
|
111
132
|
def self.deep_sort(value)
|
|
112
133
|
case value
|
|
113
134
|
when Hash
|
|
@@ -127,15 +148,17 @@ module Smplkit
|
|
|
127
148
|
# defaults). The app transport backs the standalone contexts client
|
|
128
149
|
# (evaluation-context registration); the app base URL is returned so a
|
|
129
150
|
# standalone client can open its own WebSocket against the event gateway.
|
|
151
|
+
#
|
|
152
|
+
# @api private
|
|
130
153
|
def self.flags_transport(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
|
|
131
|
-
cfg = ConfigResolution.
|
|
154
|
+
cfg = ConfigResolution.resolve_client_config(
|
|
132
155
|
profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
|
|
133
156
|
)
|
|
134
157
|
resolved_key = api_key.nil? ? cfg.api_key : api_key
|
|
135
158
|
merged = {}
|
|
136
159
|
merged.merge!(cfg.extra_headers || {})
|
|
137
160
|
merged.merge!(extra_headers || {})
|
|
138
|
-
tcfg = ConfigResolution::
|
|
161
|
+
tcfg = ConfigResolution::ResolvedClientConfig.new(
|
|
139
162
|
api_key: resolved_key, base_domain: cfg.base_domain, scheme: cfg.scheme,
|
|
140
163
|
debug: cfg.debug, extra_headers: merged
|
|
141
164
|
)
|
|
@@ -156,7 +179,7 @@ module Smplkit
|
|
|
156
179
|
# beta = flags.boolean_flag("beta", default: false)
|
|
157
180
|
# beta.get # => ...
|
|
158
181
|
#
|
|
159
|
-
# The
|
|
182
|
+
# The CRUD surface (+new_*+ / +get+ / +list+ / +delete+ and discovery)
|
|
160
183
|
# is pure CRUD. The live surface (+boolean_flag+ / +string_flag+ /
|
|
161
184
|
# +number_flag+ / +json_flag+ / +refresh+ / +stats+ / +on_change+) connects
|
|
162
185
|
# lazily on first use — the first call flushes discovery, fetches all flag
|
|
@@ -208,6 +231,14 @@ module Smplkit
|
|
|
208
231
|
# ----------------------------------------------------------------
|
|
209
232
|
|
|
210
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.
|
|
211
242
|
def new_boolean_flag(id, default:, name: nil, description: nil)
|
|
212
243
|
BooleanFlag.new(
|
|
213
244
|
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
@@ -218,6 +249,17 @@ module Smplkit
|
|
|
218
249
|
end
|
|
219
250
|
|
|
220
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.
|
|
221
263
|
def new_string_flag(id, default:, name: nil, description: nil, values: nil)
|
|
222
264
|
StringFlag.new(
|
|
223
265
|
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
@@ -226,6 +268,17 @@ module Smplkit
|
|
|
226
268
|
end
|
|
227
269
|
|
|
228
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.
|
|
229
282
|
def new_number_flag(id, default:, name: nil, description: nil, values: nil)
|
|
230
283
|
NumberFlag.new(
|
|
231
284
|
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
@@ -234,6 +287,17 @@ module Smplkit
|
|
|
234
287
|
end
|
|
235
288
|
|
|
236
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.
|
|
237
301
|
def new_json_flag(id, default:, name: nil, description: nil, values: nil)
|
|
238
302
|
JsonFlag.new(
|
|
239
303
|
self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
|
|
@@ -242,12 +306,22 @@ module Smplkit
|
|
|
242
306
|
end
|
|
243
307
|
|
|
244
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.
|
|
245
313
|
def get(id)
|
|
246
314
|
response = ApiSupport::ErrorMapping.call { @api.get_flag(id) }
|
|
247
315
|
model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
|
|
248
316
|
end
|
|
249
317
|
|
|
250
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.
|
|
251
325
|
def list(page_number: nil, page_size: nil)
|
|
252
326
|
opts = {}
|
|
253
327
|
opts[:page_number] = page_number unless page_number.nil?
|
|
@@ -257,6 +331,10 @@ module Smplkit
|
|
|
257
331
|
end
|
|
258
332
|
|
|
259
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.
|
|
260
338
|
def delete(id)
|
|
261
339
|
ApiSupport::ErrorMapping.call { @api.delete_flag(id) }
|
|
262
340
|
nil
|
|
@@ -277,6 +355,14 @@ module Smplkit
|
|
|
277
355
|
# ----------------------------------------------------------------
|
|
278
356
|
|
|
279
357
|
# Buffer flag declarations for bulk-discovery upload; optionally flush now.
|
|
358
|
+
#
|
|
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]
|
|
280
366
|
def register(items, flush: false)
|
|
281
367
|
batch = items.is_a?(Array) ? items : [items]
|
|
282
368
|
batch.each { |d| @buffer.add(d) }
|
|
@@ -295,6 +381,8 @@ module Smplkit
|
|
|
295
381
|
# against an unhealthy +flags+ service is automatically retried by the
|
|
296
382
|
# next +flush+ call (periodic background flush, install retry, or final
|
|
297
383
|
# flush on close).
|
|
384
|
+
#
|
|
385
|
+
# @return [void]
|
|
298
386
|
def flush
|
|
299
387
|
batch = @buffer.peek
|
|
300
388
|
return if batch.empty?
|
|
@@ -305,11 +393,15 @@ module Smplkit
|
|
|
305
393
|
end
|
|
306
394
|
|
|
307
395
|
# Synchronous flush — alias of +flush+ for the periodic-flush path.
|
|
396
|
+
#
|
|
397
|
+
# @return [void]
|
|
308
398
|
def flush_sync
|
|
309
399
|
flush
|
|
310
400
|
end
|
|
311
401
|
|
|
312
402
|
# Number of pending flag declarations awaiting flush.
|
|
403
|
+
#
|
|
404
|
+
# @return [Integer] number of pending flag declarations awaiting flush.
|
|
313
405
|
def pending_count
|
|
314
406
|
@buffer.pending_count
|
|
315
407
|
end
|
|
@@ -319,6 +411,11 @@ module Smplkit
|
|
|
319
411
|
# ----------------------------------------------------------------
|
|
320
412
|
|
|
321
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.
|
|
322
419
|
def boolean_flag(id, default:)
|
|
323
420
|
ensure_connected
|
|
324
421
|
handle = BooleanFlag.new(self, id: id, name: id, type: "BOOLEAN", default: default)
|
|
@@ -328,6 +425,11 @@ module Smplkit
|
|
|
328
425
|
end
|
|
329
426
|
|
|
330
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.
|
|
331
433
|
def string_flag(id, default:)
|
|
332
434
|
ensure_connected
|
|
333
435
|
handle = StringFlag.new(self, id: id, name: id, type: "STRING", default: default)
|
|
@@ -337,6 +439,11 @@ module Smplkit
|
|
|
337
439
|
end
|
|
338
440
|
|
|
339
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.
|
|
340
447
|
def number_flag(id, default:)
|
|
341
448
|
ensure_connected
|
|
342
449
|
handle = NumberFlag.new(self, id: id, name: id, type: "NUMERIC", default: default)
|
|
@@ -346,6 +453,11 @@ module Smplkit
|
|
|
346
453
|
end
|
|
347
454
|
|
|
348
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.
|
|
349
461
|
def json_flag(id, default:)
|
|
350
462
|
ensure_connected
|
|
351
463
|
handle = JsonFlag.new(self, id: id, name: id, type: "JSON", default: default)
|
|
@@ -361,12 +473,16 @@ module Smplkit
|
|
|
361
473
|
# Re-fetch all flag definitions and clear cache.
|
|
362
474
|
#
|
|
363
475
|
# Connects lazily on first use — no explicit install step.
|
|
476
|
+
#
|
|
477
|
+
# @return [void]
|
|
364
478
|
def refresh
|
|
365
479
|
ensure_connected
|
|
366
480
|
do_refresh("manual")
|
|
367
481
|
end
|
|
368
482
|
|
|
369
483
|
# Return evaluation statistics. Connects lazily on first use.
|
|
484
|
+
#
|
|
485
|
+
# @return [FlagStats] the evaluation statistics.
|
|
370
486
|
def stats
|
|
371
487
|
ensure_connected
|
|
372
488
|
FlagStats.new(cache_hits: @cache.cache_hits, cache_misses: @cache.cache_misses)
|
|
@@ -378,6 +494,11 @@ module Smplkit
|
|
|
378
494
|
# client.flags.on_change("checkout-v2") { |e| ... } # flag-scoped
|
|
379
495
|
#
|
|
380
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.
|
|
381
502
|
def on_change(flag_id = nil, &block)
|
|
382
503
|
ensure_connected
|
|
383
504
|
raise ArgumentError, "on_change requires a block" unless block
|
|
@@ -395,6 +516,8 @@ module Smplkit
|
|
|
395
516
|
# Tears down the owned WebSocket (standalone install). A wired client
|
|
396
517
|
# borrows the parent's transport, WebSocket, and contexts client and
|
|
397
518
|
# closes none of them.
|
|
519
|
+
#
|
|
520
|
+
# @return [void]
|
|
398
521
|
def close
|
|
399
522
|
if @owns_ws && @ws_manager
|
|
400
523
|
@ws_manager.stop
|
|
@@ -408,6 +531,9 @@ module Smplkit
|
|
|
408
531
|
alias _close close
|
|
409
532
|
|
|
410
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.
|
|
411
537
|
def self.open(**kwargs)
|
|
412
538
|
client = new(**kwargs)
|
|
413
539
|
begin
|
|
@@ -652,9 +778,9 @@ module Smplkit
|
|
|
652
778
|
|
|
653
779
|
# Evaluate a flag definition against the given context.
|
|
654
780
|
#
|
|
655
|
-
#
|
|
656
|
-
# 1. Look up the environment. If missing, return flag-level default.
|
|
657
|
-
# 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.
|
|
658
784
|
# 3. Iterate rules; first match wins.
|
|
659
785
|
# 4. No match -> env default or flag default.
|
|
660
786
|
def evaluate_flag(flag_def, environment, eval_dict)
|
|
@@ -743,6 +869,8 @@ module Smplkit
|
|
|
743
869
|
# correct — the Java SDK followed the same pattern. Operators supported:
|
|
744
870
|
# +==+, +!=+, +<+, +<=+, +>+, +>=+, +in+, +var+, +and+, +or+, +!+, +if+,
|
|
745
871
|
# +missing+, +none+.
|
|
872
|
+
#
|
|
873
|
+
# @api private
|
|
746
874
|
module JsonLogicEvaluator
|
|
747
875
|
module_function
|
|
748
876
|
|