toggly 1.2.0 → 1.3.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: 810c3cb9858347a1cb79212512e006bf2362ee97cd09c7a528eb334c0bf949cc
4
- data.tar.gz: 7f6ef5ce376a28c1101ce65c1d82080c02e6b254487ed0908479c1a622adffeb
3
+ metadata.gz: 8e7e0a87a56b437ad26a416ec236125cdffab5f1ed3ac0a65260da717c58c04d
4
+ data.tar.gz: 4e3a36875f88ee85dbe81ae51cb72226b97cd3951d8034ddf600d2f8a5ce8da6
5
5
  SHA512:
6
- metadata.gz: e0c8fbd59c7cfa8baafd73f580da168fe0fe4114e802f0e55bec3c406994e8a4a169b773dbf6e97862f831157d7b011df18f6210cda33740a56a6f515c05fcfc
7
- data.tar.gz: 92d791902d6bf591f9731647caa1963b2c589bf12255afe376b1f1fa9591437d9af10388503b54bbbf06e911afb41fb2b557be60c9564dd09eec423037fcbdab
6
+ metadata.gz: 0e8ddc1874675e6536462478a8245f964470951cede5dae334c13383cd34af96627a6be8a1f8778eb0b2941dc74433714113b85167f88a472ff88d7e1b763094
7
+ data.tar.gz: d743ba544845147aff474191418ab201a3945c3e8edad5ccb78d34364d175a8a8aa1b0cf38ac5c3122f95517670b75416261821b5dd75d214fe0d3483b789a72
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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.3.0] - 2026-10-04
9
+
10
+ ### Added
11
+ - `Toggly::SegmentMembership` for Backend-key targeting-list membership updates.
12
+
13
+ ## [1.2.1] - 2026-09-28
14
+
15
+ ### Fixed
16
+
17
+ - Always apply HTTP 200 definition bodies when the storage revision matches.
18
+ Same-revision responses still count as cache hits for telemetry after the
19
+ body is applied [OPS-1568].
20
+
8
21
  ## [1.2.0] - 2026-09-24
9
22
 
10
23
  ### Added
data/lib/toggly/client.rb CHANGED
@@ -24,6 +24,8 @@ module Toggly
24
24
  # )
25
25
  # client = Toggly::Client.new(config)
26
26
  class Client
27
+ UNHANDLED_VARIANT_VALUE = Object.new.freeze
28
+
27
29
  include CacheTelemetry
28
30
  include SnapshotSupport
29
31
 
@@ -359,30 +361,43 @@ module Toggly
359
361
  return block.call(value) if block
360
362
  return nil if as.nil?
361
363
 
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
364
+ scalar_value = decode_scalar_variant_value(value, as)
365
+ return scalar_value unless scalar_value.equal?(UNHANDLED_VARIANT_VALUE)
375
366
 
376
367
  # is_a? requires a Module; symbols like :boolean are handled above.
377
368
  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
369
 
381
- nil
370
+ decode_variant_object(value, as)
382
371
  rescue StandardError
383
372
  nil
384
373
  end
385
374
 
375
+ # Scalars / :boolean before Class#new — String.new({}) would soft-fail to "".
376
+ def decode_scalar_variant_value(value, as)
377
+ return value.is_a?(String) ? value : nil if as == String
378
+ return numeric_variant_value(value, Integer) if as == Integer
379
+ return numeric_variant_value(value, Float) if as == Float
380
+ return [true, false].include?(value) ? value : nil if [TrueClass, FalseClass, :boolean].include?(as)
381
+
382
+ UNHANDLED_VARIANT_VALUE
383
+ end
384
+
385
+ def numeric_variant_value(value, type)
386
+ return nil if value.is_a?(TrueClass) || value.is_a?(FalseClass)
387
+ return value if type == Integer && value.is_a?(Integer)
388
+ return value.to_f if type == Float && value.is_a?(Numeric)
389
+
390
+ nil
391
+ end
392
+
393
+ def decode_variant_object(value, as)
394
+ return nil unless value.is_a?(Hash)
395
+ return as.new(**value.transform_keys(&:to_sym)) if as.respond_to?(:new)
396
+ return as.json_create(value) if as.respond_to?(:json_create)
397
+
398
+ nil
399
+ end
400
+
386
401
  # `definitions` / `definitions-signed` → local rule eval. The sole
387
402
  # source of truth for `enabled?`; also carries `variants` / `allocation`
388
403
  # for the catalog-local `get_variant` assignment.
@@ -189,9 +189,11 @@ module Toggly
189
189
  log_debug("Definitions not modified")
190
190
  FetchResult.new(definitions: nil, cache_outcome: :hit)
191
191
  when :same_revision
192
- log_debug("Definitions revision matches existing (ETag or Last-Modified)")
193
- store_revision_headers(response_etag, response_lm)
194
- FetchResult.new(definitions: nil, cache_outcome: :hit)
192
+ # Same storage revision is a telemetry hit, but HTTP 200 still carries
193
+ # an authoritative body — apply it (parity with Go/Rust/Python).
194
+ log_debug("Definitions revision matches existing; applying HTTP 200 body")
195
+ result = handle_new_content(response, response_etag, response_lm)
196
+ FetchResult.new(definitions: result.definitions, cache_outcome: :hit)
195
197
  when :new_content
196
198
  handle_new_content(response, response_etag, response_lm)
197
199
  when :error_status
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Toggly
8
+ # Backend-key client for targeting-list membership on app.toggly.io.
9
+ class SegmentMembership
10
+ def initialize(app_key:, app_base_url: "https://app.toggly.io")
11
+ raise ArgumentError, "app_key is required" if app_key.nil? || app_key.empty?
12
+
13
+ @app_key = app_key
14
+ @base = app_base_url.sub(%r{/+\z}, "")
15
+ end
16
+
17
+ def list_segments
18
+ request(Net::HTTP::Get, "/api/v2/segments")
19
+ end
20
+
21
+ def add_segment_members(segment, identifiers)
22
+ request(Net::HTTP::Post, items_path(segment), { identifiers: identifiers })
23
+ end
24
+
25
+ def remove_segment_members(segment, identifiers)
26
+ request(Net::HTTP::Delete, items_path(segment), { identifiers: identifiers })
27
+ end
28
+
29
+ def replace_segment_members(segment, identifiers)
30
+ request(Net::HTTP::Put, items_path(segment), { identifiers: identifiers })
31
+ end
32
+
33
+ private
34
+
35
+ def items_path(segment)
36
+ "/api/v2/segments/#{URI.encode_www_form_component(segment).gsub("+", "%20")}/items"
37
+ end
38
+
39
+ def request(klass, path, body = nil)
40
+ uri = URI.parse("#{@base}#{path}")
41
+ http = Net::HTTP.new(uri.host, uri.port)
42
+ http.use_ssl = uri.scheme == "https"
43
+ http.open_timeout = 30
44
+ http.read_timeout = 30
45
+ req = klass.new(uri)
46
+ req["Authorization"] = @app_key
47
+ req["Accept"] = "application/json"
48
+ if body
49
+ req["Content-Type"] = "application/json"
50
+ req.body = JSON.generate(body)
51
+ end
52
+ response = http.request(req)
53
+ unless response.is_a?(Net::HTTPSuccess)
54
+ raise Toggly::NetworkError.new(
55
+ "Segment membership failed",
56
+ status_code: response.code.to_i,
57
+ response_body: response.body
58
+ )
59
+ end
60
+
61
+ # Mutations may return 204/empty bodies; do not JSON-parse blank payloads.
62
+ body = response.body.to_s
63
+ return nil if body.strip.empty?
64
+
65
+ JSON.parse(body)
66
+ end
67
+ end
68
+ end
@@ -112,13 +112,12 @@ module Toggly
112
112
  return nil if list.empty?
113
113
 
114
114
  pct = compute_percentile(identity, seed, feature_key, ignore_case)
115
- list.each do |entry|
115
+ matching_entry = list.find do |entry|
116
116
  from = entry[:from].to_f
117
117
  to = entry[:to].to_f
118
- matched = to >= 100.0 ? pct >= from : (pct >= from && pct < to)
119
- return entry[:variant] if matched
118
+ to >= 100.0 ? pct >= from : (pct >= from && pct < to)
120
119
  end
121
- nil
120
+ matching_entry&.fetch(:variant, nil)
122
121
  end
123
122
 
124
123
  # MF-parity percentile hash.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Toggly
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/toggly.rb CHANGED
@@ -37,6 +37,7 @@ require_relative "toggly/snapshot_providers/file"
37
37
  require_relative "toggly/definition_cache"
38
38
  require_relative "toggly/definitions_provider"
39
39
  require_relative "toggly/client"
40
+ require_relative "toggly/segment_membership"
40
41
 
41
42
  # Toggly - Feature Flag Management SDK for Ruby
42
43
  #
@@ -71,6 +72,8 @@ require_relative "toggly/client"
71
72
  # }
72
73
  # )
73
74
  module Toggly
75
+ NOT_CONFIGURED_ERROR = "Toggly not configured. Call Toggly.configure first."
76
+
74
77
  class << self
75
78
  # Global client instance
76
79
  attr_accessor :client
@@ -92,7 +95,7 @@ module Toggly
92
95
  # @param context [Context, nil] Optional evaluation context
93
96
  # @return [Boolean]
94
97
  def enabled?(feature_key, context: nil)
95
- raise Error, "Toggly not configured. Call Toggly.configure first." unless @client
98
+ raise Error, NOT_CONFIGURED_ERROR unless @client
96
99
 
97
100
  @client.enabled?(feature_key, context: context)
98
101
  end
@@ -113,7 +116,7 @@ module Toggly
113
116
  # @param context [Context, nil]
114
117
  # @return [VariantResult, nil]
115
118
  def get_variant(feature_key, context: nil)
116
- raise Error, "Toggly not configured. Call Toggly.configure first." unless @client
119
+ raise Error, NOT_CONFIGURED_ERROR unless @client
117
120
 
118
121
  @client.get_variant(feature_key, context: context)
119
122
  end
@@ -124,7 +127,7 @@ module Toggly
124
127
  # @param context [Context, nil]
125
128
  # @return [Object, nil]
126
129
  def get_variant_value(feature_key, context: nil, as: nil, &)
127
- raise Error, "Toggly not configured. Call Toggly.configure first." unless @client
130
+ raise Error, NOT_CONFIGURED_ERROR unless @client
128
131
 
129
132
  @client.get_variant_value(feature_key, context: context, as: as, &)
130
133
  end
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.2.0
4
+ version: 1.3.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-25 00:00:00.000000000 Z
11
+ date: 2026-10-06 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.
@@ -52,6 +52,7 @@ files:
52
52
  - lib/toggly/http_request_mapper.rb
53
53
  - lib/toggly/registry.rb
54
54
  - lib/toggly/request_context.rb
55
+ - lib/toggly/segment_membership.rb
55
56
  - lib/toggly/snapshot_providers/base.rb
56
57
  - lib/toggly/snapshot_providers/file.rb
57
58
  - lib/toggly/snapshot_providers/memory.rb