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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af67c2cd1ab3f935ea72b5f3959e0958f8e7225eb8d176a40fbdb9baf3b3f57d
4
- data.tar.gz: 8911b6582910f651d545a1fd625314f7a5aab4c8972188bc31844156ee37882e
3
+ metadata.gz: 810c3cb9858347a1cb79212512e006bf2362ee97cd09c7a528eb334c0bf949cc
4
+ data.tar.gz: 7f6ef5ce376a28c1101ce65c1d82080c02e6b254487ed0908479c1a622adffeb
5
5
  SHA512:
6
- metadata.gz: 78ff528d676027bb0379688179b888fae05c567f0a1f57b78982890e07fc3c68cf68c763f0ddd63c8611e21c2aea4bcc0e25d31dde2f33e6ba11adbb52653cf4
7
- data.tar.gz: dbe5aad2ad173a579bd7385a24b00a18980be769ea89a46e875ec6ab31d5e5af4246a9ac74789530cc7bf673dd237addee389261e2c5fc515d3989fe520b5653
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
- variant = client.get_variant('checkout-flow', context: Toggly::Context.new(identity: 'user-1'))
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', context: context) # shortcut for configuration_value
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&.identity,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Toggly
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
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.0.0
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-23 00:00:00.000000000 Z
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.