confeature 0.0.0 → 0.1.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/README.md +26 -4
- data/lib/confeature/client.rb +397 -0
- data/lib/confeature/evaluation_details.rb +89 -0
- data/lib/confeature/flag_overrides.rb +186 -0
- data/lib/confeature/hooks.rb +84 -0
- data/lib/confeature/internal/cache.rb +166 -0
- data/lib/confeature/internal/comparators.rb +483 -0
- data/lib/confeature/internal/config_fetcher.rb +254 -0
- data/lib/confeature/internal/eval_log.rb +334 -0
- data/lib/confeature/internal/eval_logger.rb +140 -0
- data/lib/confeature/internal/evaluator.rb +866 -0
- data/lib/confeature/internal/hashing.rb +89 -0
- data/lib/confeature/internal/override_eval.rb +184 -0
- data/lib/confeature/internal/poll_service.rb +218 -0
- data/lib/confeature/internal/prerequisites.rb +88 -0
- data/lib/confeature/internal/sdk_key.rb +121 -0
- data/lib/confeature/internal/segments.rb +79 -0
- data/lib/confeature/internal/semver.rb +163 -0
- data/lib/confeature/log_level.rb +20 -0
- data/lib/confeature/logger.rb +59 -0
- data/lib/confeature/polling_mode.rb +62 -0
- data/lib/confeature/snapshot.rb +158 -0
- data/lib/confeature/user.rb +52 -0
- data/lib/confeature/version.rb +11 -0
- data/lib/confeature.rb +39 -1
- metadata +70 -7
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
module Confeature
|
|
6
|
+
# How flag overrides interact with the remote config.
|
|
7
|
+
#
|
|
8
|
+
# Only `LOCAL_ONLY` is wired in v1 — `LOCAL_OVER_REMOTE` and
|
|
9
|
+
# `REMOTE_OVER_LOCAL` are accepted at the type level and raise
|
|
10
|
+
# `NotImplementedError` at evaluation time (spec 031 §R8 deferral,
|
|
11
|
+
# mirrors Python + Go behaviour).
|
|
12
|
+
module OverrideBehaviour
|
|
13
|
+
LOCAL_ONLY = :local_only
|
|
14
|
+
LOCAL_OVER_REMOTE = :local_over_remote
|
|
15
|
+
REMOTE_OVER_LOCAL = :remote_over_local
|
|
16
|
+
|
|
17
|
+
ALL = [LOCAL_ONLY, LOCAL_OVER_REMOTE, REMOTE_OVER_LOCAL].freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# One slot of a `config_v6.json` setting value record.
|
|
21
|
+
#
|
|
22
|
+
# Exactly one of `b`, `s`, `i`, `d` is non-nil for a valid setting.
|
|
23
|
+
# The override-eval helper picks the first non-nil field in the
|
|
24
|
+
# B → S → I → D order, matching the TS / Python / Go canonical
|
|
25
|
+
# behaviour.
|
|
26
|
+
SettingValue = Struct.new(:b, :s, :i, :d, keyword_init: true) do
|
|
27
|
+
def empty?
|
|
28
|
+
b.nil? && s.nil? && i.nil? && d.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def picked
|
|
32
|
+
return b unless b.nil?
|
|
33
|
+
return s unless s.nil?
|
|
34
|
+
return i unless i.nil?
|
|
35
|
+
|
|
36
|
+
d
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Abstract base for override-data sources. Concrete implementations
|
|
41
|
+
# MUST provide a `get_overrides_sync` method returning a
|
|
42
|
+
# `Hash{String => SettingValue}` (or nil to indicate no sync data).
|
|
43
|
+
class OverrideDataSource
|
|
44
|
+
def get_overrides_sync
|
|
45
|
+
raise NotImplementedError, "subclasses must implement #get_overrides_sync"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# In-memory dict-backed override source — the production default.
|
|
50
|
+
#
|
|
51
|
+
# Converts each primitive value to a `SettingValue` slot once at
|
|
52
|
+
# construction time and returns the same map on every call (stable
|
|
53
|
+
# identity).
|
|
54
|
+
class MapOverrideDataSource < OverrideDataSource
|
|
55
|
+
def initialize(overrides)
|
|
56
|
+
super()
|
|
57
|
+
raise ArgumentError, "overrides must be a Hash" unless overrides.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
@map = overrides.each_with_object({}) do |(key, value), acc|
|
|
60
|
+
acc[key.to_s] = self.class.to_setting_value(value)
|
|
61
|
+
end.freeze
|
|
62
|
+
freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def get_overrides_sync
|
|
66
|
+
@map
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Map a primitive override value to its SettingValue slot.
|
|
70
|
+
#
|
|
71
|
+
# `TrueClass` / `FalseClass` are checked before `Integer` because
|
|
72
|
+
# in Ruby `true.is_a?(Integer)` is false (unlike Python), so order
|
|
73
|
+
# is purely for clarity here, but we keep the explicit ordering to
|
|
74
|
+
# mirror Python's defensive coding.
|
|
75
|
+
def self.to_setting_value(value)
|
|
76
|
+
case value
|
|
77
|
+
when true, false
|
|
78
|
+
SettingValue.new(b: value)
|
|
79
|
+
when Integer
|
|
80
|
+
SettingValue.new(i: value)
|
|
81
|
+
when Float
|
|
82
|
+
SettingValue.new(d: value)
|
|
83
|
+
when String
|
|
84
|
+
SettingValue.new(s: value)
|
|
85
|
+
else
|
|
86
|
+
raise ArgumentError,
|
|
87
|
+
"Unsupported override value type: #{value.class}. " \
|
|
88
|
+
"Supported types are TrueClass/FalseClass, Integer, Float, String."
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Full-config override source — holds a complete parsed `config_v6`
|
|
94
|
+
# blob (`{ "p" => ..., "f" => ..., "s" => ... }`) and routes through
|
|
95
|
+
# the real evaluator offline.
|
|
96
|
+
#
|
|
97
|
+
# Spec 135 / #431 (epic #382, US3): the rule-bearing sibling of
|
|
98
|
+
# `MapOverrideDataSource`. Where the map source emits value-only
|
|
99
|
+
# `SettingValue` slots, this source retains the WHOLE `{ p, f, s }`
|
|
100
|
+
# config — salt (`p.s`) + segments (`s`) + rule-bearing settings (`f`)
|
|
101
|
+
# — so the Ruby evaluator can resolve targeting rules / segments /
|
|
102
|
+
# prerequisites / percentage options purely from the supplied config,
|
|
103
|
+
# with no network fetch.
|
|
104
|
+
#
|
|
105
|
+
# Additive extension (Principle II): ConfigCat's official Ruby SDK only
|
|
106
|
+
# exposes a *file-based* full-config override (`LocalFileDataSource`)
|
|
107
|
+
# plus a value-only `LocalDictionaryDataSource`. This in-memory
|
|
108
|
+
# full-config form is the runtime-agnostic equivalent, mirroring the
|
|
109
|
+
# JS federation's `ConfigOverrideDataSource` / the Python +
|
|
110
|
+
# .NET ports. No ConfigCat-named member is renamed or retyped.
|
|
111
|
+
#
|
|
112
|
+
# The whole config Hash is retained (never just `f`) — the spec-129
|
|
113
|
+
# dropped-salt regression guard (INV-4): percentage bucketing reads
|
|
114
|
+
# `p.s` and segment conditions read `s`.
|
|
115
|
+
#
|
|
116
|
+
# This source intentionally does NOT subclass `OverrideDataSource`'s
|
|
117
|
+
# value-only `get_overrides_sync` contract semantics (its settings are
|
|
118
|
+
# rule-bearing config records, not flat `SettingValue` envelopes). The
|
|
119
|
+
# override router (`Internal::OverrideEval.evaluate`) dispatches on the
|
|
120
|
+
# source TYPE — a `ConfigOverrideDataSource` always routes through the
|
|
121
|
+
# evaluator, never the flat-map path — so it is never mis-read as
|
|
122
|
+
# value-only.
|
|
123
|
+
class ConfigOverrideDataSource < OverrideDataSource
|
|
124
|
+
# @param config [Hash] the full parsed `config_v6` blob (`{ p, f, s }`).
|
|
125
|
+
def initialize(config)
|
|
126
|
+
super()
|
|
127
|
+
raise ArgumentError, "config must be a Hash" unless config.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
@config = config
|
|
130
|
+
freeze
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The full parsed `config_v6` blob (`{ p, f, s }`).
|
|
134
|
+
attr_reader :config
|
|
135
|
+
|
|
136
|
+
# Returns the rule-bearing settings map (`f`).
|
|
137
|
+
#
|
|
138
|
+
# Exposed so `Snapshot#get_all_keys` / `Client#get_all_values` can
|
|
139
|
+
# enumerate the override's flag keys. Resolution itself never goes
|
|
140
|
+
# through this map — the router evaluates the full config through the
|
|
141
|
+
# real evaluator.
|
|
142
|
+
def get_overrides_sync
|
|
143
|
+
settings = @config["f"]
|
|
144
|
+
settings.is_a?(Hash) ? settings : {}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# (data_source, behaviour) pair passed via the client constructor.
|
|
149
|
+
FlagOverrides = Struct.new(:data_source, :behaviour, keyword_init: true) do
|
|
150
|
+
def initialize(data_source:, behaviour:)
|
|
151
|
+
unless OverrideBehaviour::ALL.include?(behaviour)
|
|
152
|
+
raise ArgumentError,
|
|
153
|
+
"behaviour must be one of OverrideBehaviour::LOCAL_ONLY, " \
|
|
154
|
+
"::LOCAL_OVER_REMOTE, ::REMOTE_OVER_LOCAL; got #{behaviour.inspect}"
|
|
155
|
+
end
|
|
156
|
+
super
|
|
157
|
+
freeze
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Build a `FlagOverrides` from a full `config_v6` blob.
|
|
162
|
+
#
|
|
163
|
+
# The in-memory, rule-bearing offline-override factory (spec 135 /
|
|
164
|
+
# #431). With `OverrideBehaviour::LOCAL_ONLY`,
|
|
165
|
+
# `get_value(key, default, user)` resolves targeting rules / segments /
|
|
166
|
+
# prerequisites / percentage options purely from `config` through the
|
|
167
|
+
# real evaluator — no network fetch.
|
|
168
|
+
#
|
|
169
|
+
# Additive (Principle II): mirrors the JS federation's
|
|
170
|
+
# `createFlagOverridesFromConfig` and the Python / .NET ports.
|
|
171
|
+
# ConfigCat's official Ruby SDK exposes the value-only
|
|
172
|
+
# `LocalDictionaryDataSource` + the file-based `LocalFileDataSource`;
|
|
173
|
+
# this is the in-memory full-config equivalent, so a customer can drive
|
|
174
|
+
# the full rule engine from a config blob already resident in memory
|
|
175
|
+
# (the corpus / test-harness use case) on any runtime.
|
|
176
|
+
#
|
|
177
|
+
# @param config [Hash] the full parsed `config_v6` blob (`{ p, f, s }`).
|
|
178
|
+
# @param behaviour [Symbol] one of `OverrideBehaviour::*`.
|
|
179
|
+
# @return [FlagOverrides]
|
|
180
|
+
def self.create_flag_overrides_from_config(config, behaviour)
|
|
181
|
+
FlagOverrides.new(
|
|
182
|
+
data_source: ConfigOverrideDataSource.new(config),
|
|
183
|
+
behaviour: behaviour
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Confeature
|
|
4
|
+
# Subscribe to client lifecycle events. Mirrors ConfigCat's
|
|
5
|
+
# `ConfigCat::Hooks` 1:1 at the public API surface (constitution
|
|
6
|
+
# Principle II, epic #268).
|
|
7
|
+
#
|
|
8
|
+
# Four hook points map to the JS-family reference contract:
|
|
9
|
+
# `client_ready`, `config_changed`, `flag_evaluated`, `error`.
|
|
10
|
+
# Listener exceptions are caught and logged — never escape to the
|
|
11
|
+
# emitter. Obtain via `Confeature::Client#hooks`.
|
|
12
|
+
class Hooks
|
|
13
|
+
def initialize(logger: nil)
|
|
14
|
+
@logger = logger || Confeature.default_logger
|
|
15
|
+
@lock = Mutex.new
|
|
16
|
+
@on_client_ready = []
|
|
17
|
+
@on_config_changed = []
|
|
18
|
+
@on_flag_evaluated = []
|
|
19
|
+
@on_error = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Subscribe to the first successful config load.
|
|
23
|
+
def add_on_client_ready(&block)
|
|
24
|
+
raise ArgumentError, "block required" unless block_given?
|
|
25
|
+
|
|
26
|
+
@lock.synchronize { @on_client_ready << block }
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Subscribe to every fetched config (`config` is the settings hash).
|
|
31
|
+
def add_on_config_changed(&block)
|
|
32
|
+
raise ArgumentError, "block required" unless block_given?
|
|
33
|
+
|
|
34
|
+
@lock.synchronize { @on_config_changed << block }
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Subscribe to every `EvaluationDetails` produced by `get_value_details`.
|
|
39
|
+
def add_on_flag_evaluated(&block)
|
|
40
|
+
raise ArgumentError, "block required" unless block_given?
|
|
41
|
+
|
|
42
|
+
@lock.synchronize { @on_flag_evaluated << block }
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Subscribe to non-fatal SDK errors (HTTP failures, cache I/O).
|
|
47
|
+
def add_on_error(&block)
|
|
48
|
+
raise ArgumentError, "block required" unless block_given?
|
|
49
|
+
|
|
50
|
+
@lock.synchronize { @on_error << block }
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# --- internal invokers ----------------------------------------------
|
|
55
|
+
|
|
56
|
+
def invoke_on_client_ready
|
|
57
|
+
callbacks = @lock.synchronize { @on_client_ready.dup }
|
|
58
|
+
callbacks.each { |cb| safe_call(cb, :on_client_ready) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def invoke_on_config_changed(config)
|
|
62
|
+
callbacks = @lock.synchronize { @on_config_changed.dup }
|
|
63
|
+
callbacks.each { |cb| safe_call(cb, :on_config_changed, config) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def invoke_on_flag_evaluated(details)
|
|
67
|
+
callbacks = @lock.synchronize { @on_flag_evaluated.dup }
|
|
68
|
+
callbacks.each { |cb| safe_call(cb, :on_flag_evaluated, details) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def invoke_on_error(message)
|
|
72
|
+
callbacks = @lock.synchronize { @on_error.dup }
|
|
73
|
+
callbacks.each { |cb| safe_call(cb, :on_error, message) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def safe_call(callback, name, *args)
|
|
79
|
+
callback.call(*args)
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
@logger.error("Hooks: #{name} listener raised: #{e.class}: #{e.message}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
require_relative "sdk_key"
|
|
9
|
+
|
|
10
|
+
module Confeature
|
|
11
|
+
module Internal
|
|
12
|
+
# Filesystem + memory cache for the latest `config_v6.json` body
|
|
13
|
+
# + ETag.
|
|
14
|
+
#
|
|
15
|
+
# Default cache dir: `ENV["XDG_CACHE_HOME"] || File.join(Dir.home,
|
|
16
|
+
# ".cache")` joined with `confeature`. Customers override via the
|
|
17
|
+
# client's `cache_directory:` kwarg (FR-014). When the cache
|
|
18
|
+
# directory is not writable, the implementation emits a single
|
|
19
|
+
# WARNING log entry and falls back to a process-local memory cache
|
|
20
|
+
# without further filesystem attempts (FR-012).
|
|
21
|
+
module Cache
|
|
22
|
+
CacheEntry = Struct.new(:body, :etag, :fetched_at, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
def self.now_seconds
|
|
25
|
+
Time.now.to_f
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Default platform cache dir resolution. Mirrors the
|
|
29
|
+
# XDG-base-dir spec: prefer XDG_CACHE_HOME if set + non-empty,
|
|
30
|
+
# else `~/.cache`. Returns nil if Dir.home is unavailable.
|
|
31
|
+
def self.default_cache_dir
|
|
32
|
+
xdg = ENV.fetch("XDG_CACHE_HOME", nil)
|
|
33
|
+
base = xdg && !xdg.empty? ? xdg : File.join(Dir.home, ".cache")
|
|
34
|
+
File.join(base, "confeature")
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
# Dir.home raises ArgumentError when HOME is unset. Treat that
|
|
37
|
+
# as "no filesystem cache available" — caller falls back to
|
|
38
|
+
# memory.
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Abstract base — subclasses provide read / write.
|
|
43
|
+
class ConfigCache
|
|
44
|
+
def read
|
|
45
|
+
raise NotImplementedError, "subclasses must implement #read"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def write(_entry)
|
|
49
|
+
raise NotImplementedError, "subclasses must implement #write"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# In-process cache — also the fallback when the filesystem
|
|
54
|
+
# refuses.
|
|
55
|
+
class InMemoryCache < ConfigCache
|
|
56
|
+
def initialize
|
|
57
|
+
super
|
|
58
|
+
@entry = nil
|
|
59
|
+
@mutex = Mutex.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def read
|
|
63
|
+
@mutex.synchronize { @entry }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def write(entry)
|
|
67
|
+
@mutex.synchronize { @entry = entry }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Persists CacheEntry as JSON at
|
|
72
|
+
# `{cache_dir}/confeature-{cache_key_tail}.json`.
|
|
73
|
+
#
|
|
74
|
+
# On any IO error during write, switches the instance into
|
|
75
|
+
# memory-only mode and emits a single WARNING log entry.
|
|
76
|
+
# Subsequent reads/writes use the in-memory entry — the
|
|
77
|
+
# filesystem is never retried (per FR-012).
|
|
78
|
+
class FilesystemCache < ConfigCache
|
|
79
|
+
attr_reader :file_path
|
|
80
|
+
|
|
81
|
+
def initialize(cache_dir:, cache_key:, sdk_key:, logger:)
|
|
82
|
+
super()
|
|
83
|
+
@sdk_key = sdk_key
|
|
84
|
+
@logger = logger
|
|
85
|
+
cache_key_tail = cache_key.split(":").last
|
|
86
|
+
@cache_dir = cache_dir
|
|
87
|
+
@file_path = File.join(cache_dir, "confeature-#{cache_key_tail}.json")
|
|
88
|
+
@memory = InMemoryCache.new
|
|
89
|
+
@fallback_triggered = false
|
|
90
|
+
@mutex = Mutex.new
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def read
|
|
94
|
+
return @memory.read if @fallback_triggered
|
|
95
|
+
return @memory.read unless File.exist?(@file_path)
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
payload = JSON.parse(File.read(@file_path, encoding: "UTF-8"))
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
@logger.warning(
|
|
101
|
+
"FilesystemCache.read: failed to load #{@file_path} " \
|
|
102
|
+
"(sdkKey=#{SdkKey.redact(@sdk_key)}): #{e.class}"
|
|
103
|
+
)
|
|
104
|
+
return @memory.read
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
CacheEntry.new(
|
|
109
|
+
body: payload.fetch("body"),
|
|
110
|
+
etag: payload["etag"],
|
|
111
|
+
fetched_at: payload["fetched_at"].to_f
|
|
112
|
+
)
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
@logger.warning(
|
|
115
|
+
"FilesystemCache.read: malformed cache file #{@file_path} " \
|
|
116
|
+
"(sdkKey=#{SdkKey.redact(@sdk_key)}): #{e.class}"
|
|
117
|
+
)
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def write(entry)
|
|
123
|
+
@memory.write(entry)
|
|
124
|
+
return if @fallback_triggered
|
|
125
|
+
|
|
126
|
+
@mutex.synchronize do
|
|
127
|
+
FileUtils.mkdir_p(@cache_dir)
|
|
128
|
+
payload = JSON.generate(
|
|
129
|
+
"body" => entry.body,
|
|
130
|
+
"etag" => entry.etag,
|
|
131
|
+
"fetched_at" => entry.fetched_at
|
|
132
|
+
)
|
|
133
|
+
tmp = "#{@file_path}.tmp"
|
|
134
|
+
File.write(tmp, payload, encoding: "UTF-8")
|
|
135
|
+
File.rename(tmp, @file_path)
|
|
136
|
+
end
|
|
137
|
+
rescue SystemCallError, IOError => e
|
|
138
|
+
@fallback_triggered = true
|
|
139
|
+
@logger.warning(
|
|
140
|
+
"FilesystemCache.write: filesystem unwritable for #{@file_path} " \
|
|
141
|
+
"(sdkKey=#{SdkKey.redact(@sdk_key)}): #{e.class}. " \
|
|
142
|
+
"Falling back to memory-only cache for this client."
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Factory: build the right cache flavour for the supplied config.
|
|
148
|
+
# `cache_directory == :memory` (or `nil` when default unavailable)
|
|
149
|
+
# returns InMemoryCache; otherwise FilesystemCache rooted at the
|
|
150
|
+
# supplied or default directory.
|
|
151
|
+
def self.build(cache_directory:, cache_key:, sdk_key:, logger:)
|
|
152
|
+
return InMemoryCache.new if cache_directory == :memory
|
|
153
|
+
|
|
154
|
+
dir = cache_directory || default_cache_dir
|
|
155
|
+
return InMemoryCache.new if dir.nil?
|
|
156
|
+
|
|
157
|
+
FilesystemCache.new(
|
|
158
|
+
cache_dir: dir,
|
|
159
|
+
cache_key: cache_key,
|
|
160
|
+
sdk_key: sdk_key,
|
|
161
|
+
logger: logger
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|