toggly 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +8 -2
- data/lib/toggly/client.rb +80 -3
- data/lib/toggly/config.rb +30 -0
- data/lib/toggly/version.rb +1 -1
- data/lib/toggly.rb +23 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 810c3cb9858347a1cb79212512e006bf2362ee97cd09c7a528eb334c0bf949cc
|
|
4
|
+
data.tar.gz: 7f6ef5ce376a28c1101ce65c1d82080c02e6b254487ed0908479c1a622adffeb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0c8fbd59c7cfa8baafd73f580da168fe0fe4114e802f0e55bec3c406994e8a4a169b773dbf6e97862f831157d7b011df18f6210cda33740a56a6f515c05fcfc
|
|
7
|
+
data.tar.gz: 92d791902d6bf591f9731647caa1963b2c589bf12255afe376b1f1fa9591437d9af10388503b54bbbf06e911afb41fb2b557be60c9564dd09eec423037fcbdab
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Soft-typed `get_variant_value(feature, as: Klass)` (and optional block form).
|
|
13
|
+
Missing assignment or bind failure returns `nil` — never raises solely for
|
|
14
|
+
shape mismatch. Untyped `get_variant_value` (no `as:`) is unchanged
|
|
15
|
+
[OPS-1365].
|
|
16
|
+
|
|
17
|
+
## [1.1.0] - 2026-09-23
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Ambient targeting identity for catalog-local variants: `Config#identity`,
|
|
22
|
+
`Client#identity` / `Client#set_identity`. When `get_variant` /
|
|
23
|
+
`get_variant_value` are called with a nil context or a blank
|
|
24
|
+
`context.identity`, the client identity is used as the allocator
|
|
25
|
+
`userId`. Groups still come only from the per-call context (never Config).
|
|
26
|
+
Prefer config / `set_identity` (or Rails ambient context) over building
|
|
27
|
+
`Context.new(identity: …)` at every call site.
|
|
28
|
+
`Config#identity=` is configuration-time only (`Toggly.configure` /
|
|
29
|
+
`Config.new`); after `Client` is built, use `Client#set_identity`.
|
|
30
|
+
|
|
8
31
|
## [1.0.0] - 2026-09-23
|
|
9
32
|
|
|
10
33
|
### Changed (Breaking)
|
data/README.md
CHANGED
|
@@ -47,13 +47,19 @@ bit-for-bit — verified against the shared
|
|
|
47
47
|
`variant-allocator-corpus/cases.json` gold corpus.
|
|
48
48
|
|
|
49
49
|
```ruby
|
|
50
|
-
|
|
50
|
+
# Prefer ambient identity: Config#identity, Client#set_identity, or Rails
|
|
51
|
+
# feature_variant (uses toggly_context). Per-call context is an override.
|
|
52
|
+
client = Toggly::Client.new(app_key: '...', environment: 'Production', identity: 'user-1')
|
|
53
|
+
client.set_identity('user-2') # optional runtime update
|
|
54
|
+
|
|
55
|
+
variant = client.get_variant('checkout-flow') # uses client identity
|
|
56
|
+
variant = client.get_variant('checkout-flow', context: Toggly::Context.new(identity: 'override'))
|
|
51
57
|
variant&.name # assigned variant name, or nil
|
|
52
58
|
variant&.configuration_value # untyped configuration payload
|
|
53
59
|
variant&.enabled # effective enabled after StatusOverride
|
|
54
60
|
variant&.reason # "User" | "Group" | "Percentile" | "DefaultWhenEnabled" | "DefaultWhenDisabled"
|
|
55
61
|
|
|
56
|
-
client.get_variant_value('checkout-flow'
|
|
62
|
+
client.get_variant_value('checkout-flow') # shortcut for configuration_value
|
|
57
63
|
```
|
|
58
64
|
|
|
59
65
|
`enabled?` stays filter-based only. The `enable_variants` /
|
data/lib/toggly/client.rb
CHANGED
|
@@ -47,6 +47,7 @@ module Toggly
|
|
|
47
47
|
# True once a revision (including empty) or durable snapshot was applied.
|
|
48
48
|
@definitions_loaded = false
|
|
49
49
|
@mutex = Mutex.new
|
|
50
|
+
@identity = normalize_client_identity(@config.identity)
|
|
50
51
|
@ready = false
|
|
51
52
|
@closed = false
|
|
52
53
|
|
|
@@ -121,6 +122,16 @@ module Toggly
|
|
|
121
122
|
# `enable_variants` / `evaluated-variants-signed` dual-rail removed in
|
|
122
123
|
# 1.0 (see CHANGELOG).
|
|
123
124
|
#
|
|
125
|
+
# Targeting identity resolution (first non-empty wins):
|
|
126
|
+
# 1. per-call `context.identity`
|
|
127
|
+
# 2. {#identity} from Config / {#set_identity}
|
|
128
|
+
# 3. empty (MF anonymous)
|
|
129
|
+
#
|
|
130
|
+
# Groups come only from the per-call context (never Config). Prefer
|
|
131
|
+
# Rails `feature_variant` / ambient `toggly_context`, or
|
|
132
|
+
# `set_identity` / `Config#identity` for non-HTTP hosts — not a
|
|
133
|
+
# hand-built identity on every call.
|
|
134
|
+
#
|
|
124
135
|
# NOTE: this is the actual A/B assignment. It is unrelated to the
|
|
125
136
|
# `variant:` telemetry label on `record_usage` / `record_view`, which is
|
|
126
137
|
# a free-form usage tag (defaults to "enabled"/"disabled").
|
|
@@ -137,7 +148,7 @@ module Toggly
|
|
|
137
148
|
assignment = VariantAllocator.assign(
|
|
138
149
|
definition,
|
|
139
150
|
enabled: enabled,
|
|
140
|
-
identity: context
|
|
151
|
+
identity: variant_user_id(context),
|
|
141
152
|
groups: context&.groups || []
|
|
142
153
|
)
|
|
143
154
|
return nil if assignment.variant_name.nil?
|
|
@@ -155,8 +166,32 @@ module Toggly
|
|
|
155
166
|
# @param feature_key [String, Symbol] The feature key
|
|
156
167
|
# @param context [Context, nil] Optional targeting context (userId + groups)
|
|
157
168
|
# @return [Object, nil]
|
|
158
|
-
def get_variant_value(feature_key, context: nil)
|
|
159
|
-
get_variant(feature_key, context: context)&.configuration_value
|
|
169
|
+
def get_variant_value(feature_key, context: nil, as: nil, &block)
|
|
170
|
+
value = get_variant(feature_key, context: context)&.configuration_value
|
|
171
|
+
return value if as.nil? && !block
|
|
172
|
+
|
|
173
|
+
return nil if value.nil?
|
|
174
|
+
|
|
175
|
+
decode_variant_configuration(value, as, &block)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Default targeting userId used by {#get_variant} / {#get_variant_value}
|
|
179
|
+
# when the per-call context has no identity.
|
|
180
|
+
#
|
|
181
|
+
# @return [String, nil]
|
|
182
|
+
def identity
|
|
183
|
+
@mutex.synchronize { @identity }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Set the default targeting userId (overrides Config#identity at runtime).
|
|
187
|
+
# Pass +nil+ or an empty string to clear.
|
|
188
|
+
#
|
|
189
|
+
# Named +set_identity+ (not +identity=+) to match Python/Java/Go SDK APIs.
|
|
190
|
+
#
|
|
191
|
+
# @param value [String, nil]
|
|
192
|
+
# @return [String, nil]
|
|
193
|
+
def set_identity(value) # rubocop:disable Naming/AccessorMethodName
|
|
194
|
+
@mutex.synchronize { @identity = normalize_client_identity(value) }
|
|
160
195
|
end
|
|
161
196
|
|
|
162
197
|
# Get detailed evaluation result
|
|
@@ -320,6 +355,34 @@ module Toggly
|
|
|
320
355
|
|
|
321
356
|
private
|
|
322
357
|
|
|
358
|
+
def decode_variant_configuration(value, as, &block)
|
|
359
|
+
return block.call(value) if block
|
|
360
|
+
return nil if as.nil?
|
|
361
|
+
|
|
362
|
+
# Scalars / :boolean before Class#new — String.new({}) would soft-fail to "".
|
|
363
|
+
if as == String
|
|
364
|
+
return value.is_a?(String) ? value : nil
|
|
365
|
+
end
|
|
366
|
+
if as == Integer
|
|
367
|
+
return value.is_a?(Integer) && !value.is_a?(TrueClass) && !value.is_a?(FalseClass) ? value : nil
|
|
368
|
+
end
|
|
369
|
+
if as == Float
|
|
370
|
+
return value.is_a?(Numeric) && !value.is_a?(TrueClass) && !value.is_a?(FalseClass) ? value.to_f : nil
|
|
371
|
+
end
|
|
372
|
+
if [TrueClass, FalseClass, :boolean].include?(as)
|
|
373
|
+
return [true, false].include?(value) ? value : nil
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# is_a? requires a Module; symbols like :boolean are handled above.
|
|
377
|
+
return value if as.is_a?(Module) && value.is_a?(as)
|
|
378
|
+
return as.new(**value.transform_keys(&:to_sym)) if as.respond_to?(:new) && value.is_a?(Hash)
|
|
379
|
+
return as.json_create(value) if as.respond_to?(:json_create) && value.is_a?(Hash)
|
|
380
|
+
|
|
381
|
+
nil
|
|
382
|
+
rescue StandardError
|
|
383
|
+
nil
|
|
384
|
+
end
|
|
385
|
+
|
|
323
386
|
# `definitions` / `definitions-signed` → local rule eval. The sole
|
|
324
387
|
# source of truth for `enabled?`; also carries `variants` / `allocation`
|
|
325
388
|
# for the catalog-local `get_variant` assignment.
|
|
@@ -452,6 +515,20 @@ module Toggly
|
|
|
452
515
|
@telemetry.record_check(feature_key, enabled, identity)
|
|
453
516
|
end
|
|
454
517
|
|
|
518
|
+
# Allocator userId: per-call context identity when present, else client identity.
|
|
519
|
+
def variant_user_id(context)
|
|
520
|
+
return context.identity if context&.identity?
|
|
521
|
+
|
|
522
|
+
identity
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def normalize_client_identity(value)
|
|
526
|
+
return nil if value.nil?
|
|
527
|
+
|
|
528
|
+
text = value.to_s
|
|
529
|
+
text.empty? ? nil : text
|
|
530
|
+
end
|
|
531
|
+
|
|
455
532
|
def log_info(message)
|
|
456
533
|
@config.logger&.info("[Toggly] #{message}")
|
|
457
534
|
end
|
data/lib/toggly/config.rb
CHANGED
|
@@ -12,6 +12,17 @@ module Toggly
|
|
|
12
12
|
# @return [String] Application key from Toggly dashboard
|
|
13
13
|
attr_reader :app_key
|
|
14
14
|
|
|
15
|
+
# Default targeting userId for catalog-local +get_variant+ /
|
|
16
|
+
# +get_variant_value+ when neither a per-call +Context#identity+ nor
|
|
17
|
+
# Rails ambient context supplies one. Groups are never taken from Config.
|
|
18
|
+
#
|
|
19
|
+
# Set at configuration time only ({Config.new}, +Toggly.configure+ block).
|
|
20
|
+
# After a {Client} is built, mutating this value has no effect — use
|
|
21
|
+
# {Client#set_identity} for runtime updates.
|
|
22
|
+
#
|
|
23
|
+
# @return [String, nil]
|
|
24
|
+
attr_reader :identity
|
|
25
|
+
|
|
15
26
|
# @return [String] Environment name (e.g., "Production", "Staging")
|
|
16
27
|
attr_accessor :environment
|
|
17
28
|
|
|
@@ -89,6 +100,7 @@ module Toggly
|
|
|
89
100
|
|
|
90
101
|
def initialize(**options)
|
|
91
102
|
@app_key = options[:app_key]
|
|
103
|
+
@identity = normalize_identity(options[:identity])
|
|
92
104
|
@environment = options[:environment] || DEFAULT_ENVIRONMENT
|
|
93
105
|
@base_url = normalize_url(options[:base_url] || DEFAULT_BASE_URL)
|
|
94
106
|
@definitions_url = options[:definitions_url]
|
|
@@ -172,12 +184,23 @@ module Toggly
|
|
|
172
184
|
(@app_key.nil? || @app_key.empty?) && !@defaults.empty?
|
|
173
185
|
end
|
|
174
186
|
|
|
187
|
+
# Configuration-time identity only (e.g. inside +Toggly.configure+ before
|
|
188
|
+
# {Client} is constructed). Does not update an existing client's targeting
|
|
189
|
+
# identity — call {Client#set_identity} after initialization.
|
|
190
|
+
#
|
|
191
|
+
# @param value [String, nil]
|
|
192
|
+
# @return [String, nil]
|
|
193
|
+
def identity=(value)
|
|
194
|
+
@identity = normalize_identity(value)
|
|
195
|
+
end
|
|
196
|
+
|
|
175
197
|
# Convert to hash
|
|
176
198
|
#
|
|
177
199
|
# @return [Hash]
|
|
178
200
|
def to_h
|
|
179
201
|
{
|
|
180
202
|
app_key: @app_key,
|
|
203
|
+
identity: @identity,
|
|
181
204
|
environment: @environment,
|
|
182
205
|
base_url: @base_url,
|
|
183
206
|
definitions_url: @definitions_url,
|
|
@@ -208,5 +231,12 @@ module Toggly
|
|
|
208
231
|
|
|
209
232
|
url.end_with?("/") ? url : "#{url}/"
|
|
210
233
|
end
|
|
234
|
+
|
|
235
|
+
def normalize_identity(value)
|
|
236
|
+
return nil if value.nil?
|
|
237
|
+
|
|
238
|
+
text = value.to_s
|
|
239
|
+
text.empty? ? nil : text
|
|
240
|
+
end
|
|
211
241
|
end
|
|
212
242
|
end
|
data/lib/toggly/version.rb
CHANGED
data/lib/toggly.rb
CHANGED
|
@@ -106,6 +106,29 @@ module Toggly
|
|
|
106
106
|
!enabled?(feature_key, context: context)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Assign a catalog-local variant using the global client.
|
|
110
|
+
# See {Client#get_variant}.
|
|
111
|
+
#
|
|
112
|
+
# @param feature_key [String, Symbol]
|
|
113
|
+
# @param context [Context, nil]
|
|
114
|
+
# @return [VariantResult, nil]
|
|
115
|
+
def get_variant(feature_key, context: nil)
|
|
116
|
+
raise Error, "Toggly not configured. Call Toggly.configure first." unless @client
|
|
117
|
+
|
|
118
|
+
@client.get_variant(feature_key, context: context)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Configuration value for the assigned variant. See {Client#get_variant_value}.
|
|
122
|
+
#
|
|
123
|
+
# @param feature_key [String, Symbol]
|
|
124
|
+
# @param context [Context, nil]
|
|
125
|
+
# @return [Object, nil]
|
|
126
|
+
def get_variant_value(feature_key, context: nil, as: nil, &)
|
|
127
|
+
raise Error, "Toggly not configured. Call Toggly.configure first." unless @client
|
|
128
|
+
|
|
129
|
+
@client.get_variant_value(feature_key, context: context, as: as, &)
|
|
130
|
+
end
|
|
131
|
+
|
|
109
132
|
# Reset the global client (mainly for testing)
|
|
110
133
|
def reset!
|
|
111
134
|
@client&.close
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toggly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ops.ai
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: High-performance Ruby SDK for Toggly feature flag management. Works with
|
|
14
14
|
or without Toggly.io.
|