subflag-rails 0.1.0 → 0.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +82 -0
- data/lib/subflag/rails/client.rb +178 -11
- data/lib/subflag/rails/configuration.rb +16 -0
- data/lib/subflag/rails/context_builder.rb +9 -5
- data/lib/subflag/rails/evaluation_result.rb +16 -0
- data/lib/subflag/rails/helpers.rb +30 -0
- data/lib/subflag/rails/version.rb +1 -1
- data/lib/subflag/rails.rb +37 -2
- metadata +12 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d5585ee53dcf0c1864f3d9bf5845a41cd8e3c884fe11ca457bc51b3ded8cfcd
|
|
4
|
+
data.tar.gz: 1014353a80ee041027dd501aea4035f7043b873dbf75506678e2852b948f436a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a0b5adb5b8df94ed4e07ba41e47b4430b719b2cf741695b6f9029239aefa6437535d36fb08d9b2f12d84911126cdfa45d15429b2c024f0d93c694f6ee5e77e8
|
|
7
|
+
data.tar.gz: 582032e633830bb9e5867ae6883545a4ba5fd0ece16556a542f9de3513a5c0853f4ce7a121ad4af3d3ea6bc901ff136abc9f77f12eaa37789d64fa77bd11a750
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2025-12-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Bulk flag evaluation**: `subflag_prefetch` helper fetches all flags in a single API call
|
|
10
|
+
- **Cross-request caching**: `config.cache_ttl` enables caching via `Rails.cache` with configurable TTL
|
|
11
|
+
- `Subflag.prefetch_flags` and `Subflag::Rails.prefetch_flags` module methods
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Requires `subflag-openfeature-provider` >= 0.3.1
|
|
16
|
+
|
|
17
|
+
## [0.2.0] - 2025-11-30
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Updated to use `OpenFeature::SDK::EvaluationContext` for proper context passing
|
|
22
|
+
- Requires `subflag-openfeature-provider` >= 0.1 (works best with 0.2+)
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed OpenFeature SDK require path (`open_feature/sdk` instead of `openfeature/sdk`)
|
|
27
|
+
- Fixed provider class reference (`Subflag::Provider` instead of `Subflag::OpenFeature::Provider`)
|
|
28
|
+
- Fixed OpenFeature client method calls to use keyword arguments
|
|
29
|
+
|
|
5
30
|
## [0.1.0] - 2025-11-30
|
|
6
31
|
|
|
7
32
|
### Added
|
data/README.md
CHANGED
|
@@ -214,6 +214,84 @@ subflag_enabled?(:new_checkout) # Cache hit
|
|
|
214
214
|
subflag_enabled?(:new_checkout) # Cache hit
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
+
## Bulk Flag Evaluation (Prefetch)
|
|
218
|
+
|
|
219
|
+
For optimal performance, prefetch all flags for a user in a single API call. This is especially useful when your page checks multiple flags:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# config/application.rb (required)
|
|
223
|
+
config.middleware.use Subflag::Rails::RequestCache::Middleware
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
class ApplicationController < ActionController::Base
|
|
228
|
+
before_action :prefetch_feature_flags
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def prefetch_feature_flags
|
|
233
|
+
subflag_prefetch # Fetches all flags for current_user in one API call
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Now all subsequent flag lookups use the cache — no additional API calls:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# In your controller/view - all lookups are instant (cache hits)
|
|
242
|
+
subflag_enabled?(:new_checkout) # Cache hit
|
|
243
|
+
subflag_value(:max_projects, default: 3) # Cache hit
|
|
244
|
+
subflag_value(:headline, default: "Hi") # Cache hit
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### How It Works
|
|
248
|
+
|
|
249
|
+
1. **Single API call**: `subflag_prefetch` calls `/sdk/evaluate-all` to fetch all flags
|
|
250
|
+
2. **Per-request cache**: Results are stored in `RequestCache` for the duration of the request
|
|
251
|
+
3. **Zero-latency lookups**: Subsequent `subflag_enabled?` and `subflag_value` calls read from cache
|
|
252
|
+
|
|
253
|
+
### Prefetch Without current_user
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# No user context
|
|
257
|
+
subflag_prefetch(nil)
|
|
258
|
+
|
|
259
|
+
# With specific user
|
|
260
|
+
subflag_prefetch(admin_user)
|
|
261
|
+
|
|
262
|
+
# With additional context
|
|
263
|
+
subflag_prefetch(current_user, context: { device: "mobile" })
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Cross-Request Caching
|
|
267
|
+
|
|
268
|
+
By default, prefetched flags are only cached for the current request. To cache across multiple requests using `Rails.cache`, set a TTL:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# config/initializers/subflag.rb
|
|
272
|
+
Subflag::Rails.configure do |config|
|
|
273
|
+
config.api_key = Rails.application.credentials.subflag_api_key
|
|
274
|
+
config.cache_ttl = 30.seconds # Cache flags in Rails.cache for 30 seconds
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
With `cache_ttl` set:
|
|
279
|
+
- First request fetches from API and stores in `Rails.cache`
|
|
280
|
+
- Subsequent requests (within TTL) read from `Rails.cache` — no API call
|
|
281
|
+
- After TTL expires, next request fetches fresh data
|
|
282
|
+
|
|
283
|
+
This significantly reduces API load for high-traffic applications. Choose a TTL that balances freshness with performance — 30 seconds is a good starting point.
|
|
284
|
+
|
|
285
|
+
### Direct API
|
|
286
|
+
|
|
287
|
+
You can also use the module method directly:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
Subflag.prefetch_flags(user: current_user)
|
|
291
|
+
# or
|
|
292
|
+
Subflag::Rails.prefetch_flags(user: current_user)
|
|
293
|
+
```
|
|
294
|
+
|
|
217
295
|
## Configuration
|
|
218
296
|
|
|
219
297
|
```ruby
|
|
@@ -224,6 +302,10 @@ Subflag::Rails.configure do |config|
|
|
|
224
302
|
# API URL (default: https://api.subflag.com)
|
|
225
303
|
config.api_url = "https://api.subflag.com"
|
|
226
304
|
|
|
305
|
+
# Cross-request caching via Rails.cache (optional)
|
|
306
|
+
# When set, prefetched flags are cached for this duration
|
|
307
|
+
config.cache_ttl = 30.seconds
|
|
308
|
+
|
|
227
309
|
# Logging
|
|
228
310
|
config.logging_enabled = Rails.env.development?
|
|
229
311
|
config.log_level = :debug # :debug, :info, :warn
|
data/lib/subflag/rails/client.rb
CHANGED
|
@@ -8,6 +8,33 @@ module Subflag
|
|
|
8
8
|
# Most users should use `Subflag.flags` instead.
|
|
9
9
|
#
|
|
10
10
|
class Client
|
|
11
|
+
# Prefetch all flags for a user/context
|
|
12
|
+
#
|
|
13
|
+
# Fetches all flags in a single API call and caches them.
|
|
14
|
+
# Subsequent flag lookups will use the cached values.
|
|
15
|
+
#
|
|
16
|
+
# @param user [Object, nil] The user object for targeting
|
|
17
|
+
# @param context [Hash, nil] Additional context attributes
|
|
18
|
+
# @return [Array<Hash>] Array of prefetched flag results (for inspection)
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# Subflag::Rails.client.prefetch_all(user: current_user)
|
|
22
|
+
# # Subsequent lookups use cache - no API calls
|
|
23
|
+
# subflag_enabled?(:new_feature)
|
|
24
|
+
#
|
|
25
|
+
def prefetch_all(user: nil, context: nil)
|
|
26
|
+
ctx = ContextBuilder.build(user: user, context: context)
|
|
27
|
+
context_hash = ctx ? ctx.hash : "no_context"
|
|
28
|
+
|
|
29
|
+
# Use Rails.cache for cross-request caching if enabled
|
|
30
|
+
if configuration.rails_cache_enabled?
|
|
31
|
+
return prefetch_with_rails_cache(ctx, context_hash)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Otherwise fetch directly from API (per-request cache only)
|
|
35
|
+
prefetch_from_api(ctx, context_hash)
|
|
36
|
+
end
|
|
37
|
+
|
|
11
38
|
# Check if a boolean flag is enabled
|
|
12
39
|
#
|
|
13
40
|
# @param flag_key [String] The flag key (already normalized)
|
|
@@ -17,10 +44,18 @@ module Subflag
|
|
|
17
44
|
# @return [Boolean]
|
|
18
45
|
def enabled?(flag_key, user: nil, context: nil, default: false)
|
|
19
46
|
ctx = ContextBuilder.build(user: user, context: context)
|
|
47
|
+
|
|
48
|
+
# Check prefetch cache first
|
|
49
|
+
prefetched = get_prefetched_value(flag_key, ctx, :boolean)
|
|
50
|
+
if !prefetched.nil?
|
|
51
|
+
log_evaluation(flag_key, prefetched, default)
|
|
52
|
+
return prefetched
|
|
53
|
+
end
|
|
54
|
+
|
|
20
55
|
cache_key = build_cache_key(flag_key, ctx, :boolean)
|
|
21
56
|
|
|
22
57
|
result = RequestCache.fetch(cache_key) do
|
|
23
|
-
openfeature_client.fetch_boolean_value(flag_key, default, ctx)
|
|
58
|
+
openfeature_client.fetch_boolean_value(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
24
59
|
end
|
|
25
60
|
|
|
26
61
|
log_evaluation(flag_key, result, default)
|
|
@@ -37,6 +72,15 @@ module Subflag
|
|
|
37
72
|
# @raise [ArgumentError] If default is nil
|
|
38
73
|
def value(flag_key, user: nil, context: nil, default:)
|
|
39
74
|
ctx = ContextBuilder.build(user: user, context: context)
|
|
75
|
+
|
|
76
|
+
# Check prefetch cache first
|
|
77
|
+
expected_type = type_from_default(default)
|
|
78
|
+
prefetched = get_prefetched_value(flag_key, ctx, expected_type)
|
|
79
|
+
if !prefetched.nil?
|
|
80
|
+
log_evaluation(flag_key, prefetched, default)
|
|
81
|
+
return prefetched
|
|
82
|
+
end
|
|
83
|
+
|
|
40
84
|
cache_key = build_cache_key(flag_key, ctx, default.class)
|
|
41
85
|
|
|
42
86
|
result = RequestCache.fetch(cache_key) do
|
|
@@ -57,6 +101,14 @@ module Subflag
|
|
|
57
101
|
# @raise [ArgumentError] If default is nil
|
|
58
102
|
def evaluate(flag_key, user: nil, context: nil, default:)
|
|
59
103
|
ctx = ContextBuilder.build(user: user, context: context)
|
|
104
|
+
|
|
105
|
+
# Check prefetch cache first - returns full EvaluationResult
|
|
106
|
+
prefetched_result = get_prefetched_result(flag_key, ctx)
|
|
107
|
+
if prefetched_result
|
|
108
|
+
log_evaluation(flag_key, prefetched_result.value, default)
|
|
109
|
+
return EvaluationResult.from_subflag(prefetched_result)
|
|
110
|
+
end
|
|
111
|
+
|
|
60
112
|
cache_key = build_cache_key(flag_key, ctx, "details:#{default.class}")
|
|
61
113
|
|
|
62
114
|
details = RequestCache.fetch(cache_key) do
|
|
@@ -69,6 +121,46 @@ module Subflag
|
|
|
69
121
|
|
|
70
122
|
private
|
|
71
123
|
|
|
124
|
+
# Fetch flags from API and populate RequestCache
|
|
125
|
+
def prefetch_from_api(ctx, context_hash)
|
|
126
|
+
subflag_context = build_subflag_context(ctx)
|
|
127
|
+
results = subflag_client.evaluate_all(context: subflag_context)
|
|
128
|
+
|
|
129
|
+
# Cache each result in RequestCache for this request
|
|
130
|
+
results.each do |result|
|
|
131
|
+
cache_prefetched_result(result, context_hash)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
results.map(&:to_h)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Fetch flags using Rails.cache with TTL, falling back to API on cache miss
|
|
138
|
+
def prefetch_with_rails_cache(ctx, context_hash)
|
|
139
|
+
rails_cache_key = "subflag:all_flags:#{context_hash}"
|
|
140
|
+
|
|
141
|
+
cached_data = ::Rails.cache.fetch(rails_cache_key, expires_in: configuration.cache_ttl) do
|
|
142
|
+
# Cache miss - fetch from API
|
|
143
|
+
subflag_context = build_subflag_context(ctx)
|
|
144
|
+
results = subflag_client.evaluate_all(context: subflag_context)
|
|
145
|
+
results.map(&:to_h)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Populate RequestCache from cached data (whether from Rails.cache or fresh fetch)
|
|
149
|
+
populate_request_cache_from_data(cached_data, context_hash)
|
|
150
|
+
|
|
151
|
+
cached_data
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Populate RequestCache from cached hash data
|
|
155
|
+
def populate_request_cache_from_data(data_array, context_hash)
|
|
156
|
+
return unless RequestCache.enabled?
|
|
157
|
+
|
|
158
|
+
data_array.each do |result_hash|
|
|
159
|
+
result = ::Subflag::EvaluationResult.from_response(result_hash)
|
|
160
|
+
cache_prefetched_result(result, context_hash)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
72
164
|
def build_cache_key(flag_key, ctx, type)
|
|
73
165
|
context_hash = ctx ? ctx.hash : "no_context"
|
|
74
166
|
"subflag:#{flag_key}:#{context_hash}:#{type}"
|
|
@@ -81,15 +173,15 @@ module Subflag
|
|
|
81
173
|
def fetch_value_by_type(flag_key, default, ctx)
|
|
82
174
|
case default
|
|
83
175
|
when String
|
|
84
|
-
openfeature_client.fetch_string_value(flag_key, default, ctx)
|
|
176
|
+
openfeature_client.fetch_string_value(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
85
177
|
when Integer
|
|
86
|
-
openfeature_client.fetch_integer_value(flag_key, default, ctx)
|
|
178
|
+
openfeature_client.fetch_integer_value(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
87
179
|
when Float
|
|
88
|
-
openfeature_client.fetch_float_value(flag_key, default, ctx)
|
|
180
|
+
openfeature_client.fetch_float_value(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
89
181
|
when TrueClass, FalseClass
|
|
90
|
-
openfeature_client.fetch_boolean_value(flag_key, default, ctx)
|
|
182
|
+
openfeature_client.fetch_boolean_value(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
91
183
|
when Hash
|
|
92
|
-
openfeature_client.fetch_object_value(flag_key, default, ctx)
|
|
184
|
+
openfeature_client.fetch_object_value(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
93
185
|
when NilClass
|
|
94
186
|
raise ArgumentError, "default is required for value flags (it determines the expected type)"
|
|
95
187
|
else
|
|
@@ -100,15 +192,15 @@ module Subflag
|
|
|
100
192
|
def fetch_details_by_type(flag_key, default, ctx)
|
|
101
193
|
case default
|
|
102
194
|
when String
|
|
103
|
-
openfeature_client.fetch_string_details(flag_key, default, ctx)
|
|
195
|
+
openfeature_client.fetch_string_details(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
104
196
|
when Integer
|
|
105
|
-
openfeature_client.fetch_integer_details(flag_key, default, ctx)
|
|
197
|
+
openfeature_client.fetch_integer_details(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
106
198
|
when Float
|
|
107
|
-
openfeature_client.fetch_float_details(flag_key, default, ctx)
|
|
199
|
+
openfeature_client.fetch_float_details(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
108
200
|
when TrueClass, FalseClass
|
|
109
|
-
openfeature_client.fetch_boolean_details(flag_key, default, ctx)
|
|
201
|
+
openfeature_client.fetch_boolean_details(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
110
202
|
when Hash
|
|
111
|
-
openfeature_client.fetch_object_details(flag_key, default, ctx)
|
|
203
|
+
openfeature_client.fetch_object_details(flag_key: flag_key, default_value: default, evaluation_context: ctx)
|
|
112
204
|
when NilClass
|
|
113
205
|
raise ArgumentError, "default is required for evaluate (it determines the expected type)"
|
|
114
206
|
else
|
|
@@ -138,6 +230,81 @@ module Subflag
|
|
|
138
230
|
logger.debug(message)
|
|
139
231
|
end
|
|
140
232
|
end
|
|
233
|
+
|
|
234
|
+
# Build Subflag::EvaluationContext from OpenFeature context
|
|
235
|
+
def build_subflag_context(openfeature_ctx)
|
|
236
|
+
return nil unless openfeature_ctx
|
|
237
|
+
|
|
238
|
+
::Subflag::EvaluationContext.from_openfeature(openfeature_ctx)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Get or create the Subflag HTTP client for direct API calls
|
|
242
|
+
def subflag_client
|
|
243
|
+
@subflag_client ||= ::Subflag::Client.new(
|
|
244
|
+
api_url: configuration.api_url,
|
|
245
|
+
api_key: configuration.api_key
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Cache a prefetched result for subsequent lookups
|
|
250
|
+
def cache_prefetched_result(result, context_hash)
|
|
251
|
+
# Store with a prefetch-specific key that includes the raw result
|
|
252
|
+
prefetch_key = "subflag:prefetch:#{result.flag_key}:#{context_hash}"
|
|
253
|
+
RequestCache.current_cache[prefetch_key] = result
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Check if a flag was prefetched and return its value
|
|
257
|
+
def get_prefetched_value(flag_key, ctx, expected_type)
|
|
258
|
+
return nil unless RequestCache.enabled?
|
|
259
|
+
|
|
260
|
+
context_hash = ctx ? ctx.hash : "no_context"
|
|
261
|
+
prefetch_key = "subflag:prefetch:#{flag_key}:#{context_hash}"
|
|
262
|
+
result = RequestCache.current_cache[prefetch_key]
|
|
263
|
+
|
|
264
|
+
return nil unless result
|
|
265
|
+
|
|
266
|
+
# Convert value to expected type if needed
|
|
267
|
+
convert_prefetched_value(result.value, expected_type)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Get the full prefetched result for a flag (for evaluate method)
|
|
271
|
+
def get_prefetched_result(flag_key, ctx)
|
|
272
|
+
return nil unless RequestCache.enabled?
|
|
273
|
+
|
|
274
|
+
context_hash = ctx ? ctx.hash : "no_context"
|
|
275
|
+
prefetch_key = "subflag:prefetch:#{flag_key}:#{context_hash}"
|
|
276
|
+
RequestCache.current_cache[prefetch_key]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Convert prefetched value to expected type
|
|
280
|
+
def convert_prefetched_value(value, expected_type)
|
|
281
|
+
case expected_type
|
|
282
|
+
when :boolean
|
|
283
|
+
value == true || value == false ? value : nil
|
|
284
|
+
when :string
|
|
285
|
+
value.is_a?(String) ? value : nil
|
|
286
|
+
when :integer
|
|
287
|
+
value.is_a?(Numeric) ? value.to_i : nil
|
|
288
|
+
when :float
|
|
289
|
+
value.is_a?(Numeric) ? value.to_f : nil
|
|
290
|
+
when :object
|
|
291
|
+
value.is_a?(Hash) ? value : nil
|
|
292
|
+
else
|
|
293
|
+
value
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Get expected type from default value
|
|
298
|
+
def type_from_default(default)
|
|
299
|
+
case default
|
|
300
|
+
when TrueClass, FalseClass then :boolean
|
|
301
|
+
when String then :string
|
|
302
|
+
when Integer then :integer
|
|
303
|
+
when Float then :float
|
|
304
|
+
when Hash then :object
|
|
305
|
+
else :unknown
|
|
306
|
+
end
|
|
307
|
+
end
|
|
141
308
|
end
|
|
142
309
|
end
|
|
143
310
|
end
|
|
@@ -35,12 +35,28 @@ module Subflag
|
|
|
35
35
|
# @return [Symbol] Log level for flag evaluations (:debug, :info, :warn)
|
|
36
36
|
attr_accessor :log_level
|
|
37
37
|
|
|
38
|
+
# @return [Integer, ActiveSupport::Duration, nil] TTL for cross-request caching
|
|
39
|
+
# When set, prefetched flags are cached in Rails.cache for this duration.
|
|
40
|
+
# Set to nil to disable cross-request caching (default).
|
|
41
|
+
attr_accessor :cache_ttl
|
|
42
|
+
|
|
38
43
|
def initialize
|
|
39
44
|
@api_key = nil
|
|
40
45
|
@api_url = "https://api.subflag.com"
|
|
41
46
|
@user_context_block = nil
|
|
42
47
|
@logging_enabled = false
|
|
43
48
|
@log_level = :debug
|
|
49
|
+
@cache_ttl = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if cross-request caching via Rails.cache is enabled
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def rails_cache_enabled?
|
|
56
|
+
return false unless @cache_ttl && @cache_ttl.to_i > 0
|
|
57
|
+
return false unless defined?(::Rails.cache) && ::Rails.cache.present?
|
|
58
|
+
|
|
59
|
+
true
|
|
44
60
|
end
|
|
45
61
|
|
|
46
62
|
# Configure how to extract context from a user object
|
|
@@ -4,11 +4,11 @@ module Subflag
|
|
|
4
4
|
module Rails
|
|
5
5
|
# Builds OpenFeature evaluation context from user objects and additional attributes
|
|
6
6
|
class ContextBuilder
|
|
7
|
-
# Build an OpenFeature
|
|
7
|
+
# Build an OpenFeature EvaluationContext
|
|
8
8
|
#
|
|
9
9
|
# @param user [Object, nil] The user object for targeting
|
|
10
10
|
# @param context [Hash, nil] Additional context attributes
|
|
11
|
-
# @return [
|
|
11
|
+
# @return [OpenFeature::SDK::EvaluationContext, nil] The combined context or nil if empty
|
|
12
12
|
def self.build(user: nil, context: nil)
|
|
13
13
|
new(user: user, context: context).build
|
|
14
14
|
end
|
|
@@ -18,9 +18,9 @@ module Subflag
|
|
|
18
18
|
@context = context || {}
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Build the
|
|
21
|
+
# Build the OpenFeature EvaluationContext
|
|
22
22
|
#
|
|
23
|
-
# @return [
|
|
23
|
+
# @return [OpenFeature::SDK::EvaluationContext, nil]
|
|
24
24
|
def build
|
|
25
25
|
result = {}
|
|
26
26
|
|
|
@@ -34,7 +34,11 @@ module Subflag
|
|
|
34
34
|
result.merge!(@context) if @context.is_a?(Hash)
|
|
35
35
|
|
|
36
36
|
# Return nil if empty (no context to send)
|
|
37
|
-
|
|
37
|
+
return nil if result.empty?
|
|
38
|
+
|
|
39
|
+
# Convert to OpenFeature EvaluationContext
|
|
40
|
+
normalized = normalize_context(result)
|
|
41
|
+
OpenFeature::SDK::EvaluationContext.new(**normalized)
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
private
|
|
@@ -92,6 +92,22 @@ module Subflag
|
|
|
92
92
|
error_message: details[:error_message]
|
|
93
93
|
)
|
|
94
94
|
end
|
|
95
|
+
|
|
96
|
+
# Build from Subflag::EvaluationResult (from Ruby provider)
|
|
97
|
+
#
|
|
98
|
+
# @param result [Subflag::EvaluationResult] The provider's evaluation result
|
|
99
|
+
# @return [EvaluationResult]
|
|
100
|
+
def self.from_subflag(result)
|
|
101
|
+
# Convert uppercase reason string to lowercase symbol
|
|
102
|
+
reason = result.reason&.downcase&.to_sym || :unknown
|
|
103
|
+
|
|
104
|
+
new(
|
|
105
|
+
value: result.value,
|
|
106
|
+
variant: result.variant,
|
|
107
|
+
reason: reason,
|
|
108
|
+
flag_key: result.flag_key
|
|
109
|
+
)
|
|
110
|
+
end
|
|
95
111
|
end
|
|
96
112
|
end
|
|
97
113
|
end
|
|
@@ -93,6 +93,36 @@ module Subflag
|
|
|
93
93
|
Subflag.flags(user: resolved, context: context)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Prefetch all flags for a user in a single API call
|
|
97
|
+
#
|
|
98
|
+
# Call this early in a request (e.g., in a before_action) to fetch
|
|
99
|
+
# all flags at once. Subsequent flag lookups will use the cached values.
|
|
100
|
+
#
|
|
101
|
+
# Automatically scoped to current_user if available.
|
|
102
|
+
#
|
|
103
|
+
# @param user [Object, nil, :auto] User for targeting (default: current_user)
|
|
104
|
+
# @param context [Hash, nil] Additional context attributes
|
|
105
|
+
# @return [Array<Hash>] Array of prefetched flag results (for inspection)
|
|
106
|
+
#
|
|
107
|
+
# @example In ApplicationController
|
|
108
|
+
# class ApplicationController < ActionController::Base
|
|
109
|
+
# before_action :prefetch_feature_flags
|
|
110
|
+
#
|
|
111
|
+
# private
|
|
112
|
+
#
|
|
113
|
+
# def prefetch_feature_flags
|
|
114
|
+
# subflag_prefetch # Uses current_user automatically
|
|
115
|
+
# end
|
|
116
|
+
# end
|
|
117
|
+
#
|
|
118
|
+
# @example Without user context
|
|
119
|
+
# subflag_prefetch(nil)
|
|
120
|
+
#
|
|
121
|
+
def subflag_prefetch(user = :auto, context: nil)
|
|
122
|
+
resolved = resolve_user(user)
|
|
123
|
+
Subflag.prefetch_flags(user: resolved, context: context)
|
|
124
|
+
end
|
|
125
|
+
|
|
96
126
|
private
|
|
97
127
|
|
|
98
128
|
# Resolve user parameter - use current_user if :auto and available
|
data/lib/subflag/rails.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "open_feature/sdk"
|
|
4
4
|
require "subflag"
|
|
5
5
|
|
|
6
6
|
require_relative "rails/version"
|
|
@@ -48,6 +48,26 @@ module Subflag
|
|
|
48
48
|
@client ||= Client.new
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Prefetch all flags for a user/context in a single API call
|
|
52
|
+
#
|
|
53
|
+
# Call this early in a request to fetch all flags at once.
|
|
54
|
+
# Subsequent flag lookups will use the cached values.
|
|
55
|
+
#
|
|
56
|
+
# @param user [Object, nil] The user object for targeting
|
|
57
|
+
# @param context [Hash, nil] Additional context attributes
|
|
58
|
+
# @return [Array<Hash>] Array of prefetched flag results (for inspection)
|
|
59
|
+
#
|
|
60
|
+
# @example Prefetch in a controller
|
|
61
|
+
# before_action :prefetch_flags
|
|
62
|
+
#
|
|
63
|
+
# def prefetch_flags
|
|
64
|
+
# Subflag::Rails.prefetch_flags(user: current_user)
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
def prefetch_flags(user: nil, context: nil)
|
|
68
|
+
client.prefetch_all(user: user, context: context)
|
|
69
|
+
end
|
|
70
|
+
|
|
51
71
|
# Reset configuration (primarily for testing)
|
|
52
72
|
def reset!
|
|
53
73
|
@configuration = Configuration.new
|
|
@@ -59,7 +79,7 @@ module Subflag
|
|
|
59
79
|
def setup_provider
|
|
60
80
|
return unless configuration.api_key
|
|
61
81
|
|
|
62
|
-
provider = ::Subflag::
|
|
82
|
+
provider = ::Subflag::Provider.new(
|
|
63
83
|
api_key: configuration.api_key,
|
|
64
84
|
api_url: configuration.api_url
|
|
65
85
|
)
|
|
@@ -92,5 +112,20 @@ module Subflag
|
|
|
92
112
|
def flags(user: nil, context: nil)
|
|
93
113
|
Rails::FlagAccessor.new(user: user, context: context)
|
|
94
114
|
end
|
|
115
|
+
|
|
116
|
+
# Prefetch all flags for a user/context in a single API call
|
|
117
|
+
#
|
|
118
|
+
# @param user [Object, nil] The user object for targeting
|
|
119
|
+
# @param context [Hash, nil] Additional context attributes
|
|
120
|
+
# @return [Array<Hash>] Array of prefetched flag results
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# Subflag.prefetch_flags(user: current_user)
|
|
124
|
+
# # Subsequent lookups use cache
|
|
125
|
+
# Subflag.flags(user: current_user).new_feature?(default: false)
|
|
126
|
+
#
|
|
127
|
+
def prefetch_flags(user: nil, context: nil)
|
|
128
|
+
Rails.prefetch_flags(user: user, context: context)
|
|
129
|
+
end
|
|
95
130
|
end
|
|
96
131
|
end
|
metadata
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: subflag-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Subflag
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-12-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: subflag-openfeature-provider
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.3.1
|
|
20
|
+
- - "<"
|
|
18
21
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0
|
|
22
|
+
version: '1.0'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
|
-
- - "
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 0.3.1
|
|
30
|
+
- - "<"
|
|
25
31
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0
|
|
32
|
+
version: '1.0'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: railties
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|