diff --git a/lib/schematic/schematic_client.rb b/lib/schematic/schematic_client.rb index 28e7e11..bcedb11 100644 --- a/lib/schematic/schematic_client.rb +++ b/lib/schematic/schematic_client.rb @@ -202,7 +202,7 @@ def check_flags(company: nil, user: nil, keys: nil) cache_key = build_cache_key(key, company, user) found = false @flag_check_cache_providers.each do |provider| - cached = provider.get(cache_key) + cached = coerce_cached_response(provider.get(cache_key)) next unless cached cached_results[key] = { flag: key, value: cached.value, reason: cached.reason } @@ -384,11 +384,20 @@ def close # --- Internal Flag Checking --- + # Cache providers that serialize (RedisCacheProvider) hand back a Hash, not + # the CheckFlagResponse that was stored; the in-memory cache returns the + # object itself. Normalize so callers can use the response API either way. + def coerce_cached_response(cached) + return cached unless cached.is_a?(Hash) + + CheckFlagResponse.new(cached) + end + def check_flag_via_api(flag_key, company, user) # Check cache cache_key = build_cache_key(flag_key, company, user) @flag_check_cache_providers.each do |provider| - cached = provider.get(cache_key) + cached = coerce_cached_response(provider.get(cache_key)) if cached @logger.debug("Flag '#{flag_key}' found in cache (value=#{cached.value})") return cached diff --git a/test/custom.test.rb b/test/custom.test.rb index 41bfadb..ff2dd2c 100644 --- a/test/custom.test.rb +++ b/test/custom.test.rb @@ -3522,3 +3522,58 @@ def build_buffer "expected every request to carry at most 100 events, got #{sizes.inspect}" end end + +# ============================================================================= +# SchematicClient - Serializing cache providers hand back a Hash +# ============================================================================= +describe "SchematicClient - cached flag responses from a serializing provider" do + # Mimics RedisCacheProvider: values round-trip through JSON, so #get returns + # a symbol-keyed Hash rather than the CheckFlagResponse that was stored. + class HashReturningCache + def initialize + @store = {} + end + + def get(key) + raw = @store[key] + raw && JSON.parse(raw, symbolize_names: true) + end + + def set(key, value, ttl: nil) # rubocop:disable Lint/UnusedMethodArgument + @store[key] = JSON.generate(value.respond_to?(:to_h) ? value.to_h : value) + end + + def delete(key) + @store.delete(key) + end + end + + after { WebMock.reset! } + + it "serves check_flag and check_flags from a Hash-returning cache" do + base_url = "https://api.schematichq.com" + client = Schematic::SchematicClient.new(api_key: "api_test_key_123", cache_providers: [HashReturningCache.new]) + + stub_request(:post, "#{base_url}/flags/hash-cached-flag/check") + .to_return( + status: 200, + headers: { "Content-Type" => "application/json" }, + body: { data: { value: true, flag: "hash-cached-flag", reason: "match" }, params: {} }.to_json + ) + + assert client.check_flag("hash-cached-flag") + # Second call is served from the cache as a Hash; must not raise. + resp = client.check_flag_with_entitlement("hash-cached-flag") + + assert resp.value + assert_equal "match", resp.reason + assert_requested(:post, "#{base_url}/flags/hash-cached-flag/check", times: 1) + + results = client.check_flags(keys: ["hash-cached-flag"]) + + assert_equal [{ flag: "hash-cached-flag", value: true, reason: "match" }], results + assert_requested(:post, "#{base_url}/flags/hash-cached-flag/check", times: 1) + + client.close + end +end