subflag-rails 0.3.0 → 0.4.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 +142 -60
- data/lib/generators/subflag/install_generator.rb +97 -33
- data/lib/generators/subflag/templates/create_subflag_flags.rb.tt +17 -0
- data/lib/generators/subflag/templates/{initializer.rb → initializer.rb.tt} +16 -2
- data/lib/subflag/rails/backends/active_record_provider.rb +82 -0
- data/lib/subflag/rails/backends/memory_provider.rb +104 -0
- data/lib/subflag/rails/backends/subflag_provider.rb +85 -0
- data/lib/subflag/rails/client.rb +58 -12
- data/lib/subflag/rails/configuration.rb +22 -0
- data/lib/subflag/rails/models/flag.rb +65 -0
- data/lib/subflag/rails/version.rb +1 -1
- data/lib/subflag/rails.rb +58 -7
- metadata +14 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da40f567a605d55076aaeabed1edd404bb41ba2a1703c56595e42611ebf4aaa4
|
|
4
|
+
data.tar.gz: 39df667c30500ebf560e90694213f05b479d60f488575c7f2503409bc640222d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0bb2499ecf945fb2586918f5c3e57cdda5e6ce2f143eb4b6d1d674370412ccee6f6e8fe5d992705ac4b0b28de88465ebe7e140fad1a2c71f691982ebe363c4ce
|
|
7
|
+
data.tar.gz: f255c179b502557624bfcfc81bc06743c13898bef9d7b5225e4519e097fbdfc7b88af2b2729088883747cb3a123f1a51fff521618527e68e7e77ca494212be32
|
data/README.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Subflag Rails
|
|
2
2
|
|
|
3
|
-
Typed feature flags for Rails. Booleans, strings, numbers, and JSON —
|
|
3
|
+
Typed feature flags for Rails. Booleans, strings, numbers, and JSON — with pluggable backends.
|
|
4
|
+
|
|
5
|
+
[Subflag](https://subflag.com)
|
|
6
|
+
|
|
7
|
+
## Backends
|
|
8
|
+
|
|
9
|
+
Choose where your flags live:
|
|
10
|
+
|
|
11
|
+
| Backend | Use Case | Flags Stored In |
|
|
12
|
+
|---------|----------|-----------------|
|
|
13
|
+
| `:subflag` | Production with dashboard, environments, targeting | Subflag Cloud |
|
|
14
|
+
| `:active_record` | Self-hosted, no external dependencies | Your database |
|
|
15
|
+
| `:memory` | Testing and development | In-memory hash |
|
|
16
|
+
|
|
17
|
+
**Same API regardless of backend:**
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
subflag_enabled?(:new_checkout) # Works with any backend
|
|
21
|
+
subflag_value(:max_projects, default: 3) # Works with any backend
|
|
22
|
+
```
|
|
4
23
|
|
|
5
24
|
## Installation
|
|
6
25
|
|
|
@@ -8,9 +27,14 @@ Add to your Gemfile:
|
|
|
8
27
|
|
|
9
28
|
```ruby
|
|
10
29
|
gem 'subflag-rails'
|
|
30
|
+
|
|
31
|
+
# If using Subflag Cloud (backend: :subflag), also add:
|
|
32
|
+
gem 'subflag-openfeature-provider'
|
|
11
33
|
```
|
|
12
34
|
|
|
13
|
-
|
|
35
|
+
### Option 1: Subflag Cloud (Default)
|
|
36
|
+
|
|
37
|
+
Dashboard, environments, percentage rollouts, and user targeting.
|
|
14
38
|
|
|
15
39
|
```bash
|
|
16
40
|
rails generate subflag:install
|
|
@@ -29,6 +53,38 @@ subflag:
|
|
|
29
53
|
|
|
30
54
|
Or set the `SUBFLAG_API_KEY` environment variable.
|
|
31
55
|
|
|
56
|
+
### Option 2: ActiveRecord (Self-Hosted)
|
|
57
|
+
|
|
58
|
+
Flags stored in your database. No external dependencies.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
rails generate subflag:install --backend=active_record
|
|
62
|
+
rails db:migrate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Create flags directly:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
Subflag::Rails::Flag.create!(key: "new-checkout", value: "true", value_type: "boolean")
|
|
69
|
+
Subflag::Rails::Flag.create!(key: "max-projects", value: "100", value_type: "integer")
|
|
70
|
+
Subflag::Rails::Flag.create!(key: "welcome-message", value: "Hello!", value_type: "string")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Option 3: Memory (Testing)
|
|
74
|
+
|
|
75
|
+
In-memory flags for tests and local development.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
rails generate subflag:install --backend=memory
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Set flags programmatically:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Subflag::Rails.provider.set(:new_checkout, true)
|
|
85
|
+
Subflag::Rails.provider.set(:max_projects, 100)
|
|
86
|
+
```
|
|
87
|
+
|
|
32
88
|
## Usage
|
|
33
89
|
|
|
34
90
|
### Controllers & Views
|
|
@@ -159,42 +215,6 @@ In Ruby, use underscores — they're automatically converted to dashes:
|
|
|
159
215
|
subflag_enabled?(:new_checkout) # looks up "new-checkout"
|
|
160
216
|
```
|
|
161
217
|
|
|
162
|
-
## Testing
|
|
163
|
-
|
|
164
|
-
Stub flags in your tests:
|
|
165
|
-
|
|
166
|
-
```ruby
|
|
167
|
-
# spec/rails_helper.rb (RSpec)
|
|
168
|
-
require "subflag/rails/test_helpers"
|
|
169
|
-
RSpec.configure do |config|
|
|
170
|
-
config.include Subflag::Rails::TestHelpers
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# test/test_helper.rb (Minitest)
|
|
174
|
-
require "subflag/rails/test_helpers"
|
|
175
|
-
class ActiveSupport::TestCase
|
|
176
|
-
include Subflag::Rails::TestHelpers
|
|
177
|
-
end
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
```ruby
|
|
181
|
-
# In your specs/tests
|
|
182
|
-
it "shows new checkout when enabled" do
|
|
183
|
-
stub_subflag(:new_checkout, true)
|
|
184
|
-
stub_subflag(:max_projects, 100)
|
|
185
|
-
|
|
186
|
-
visit checkout_path
|
|
187
|
-
expect(page).to have_content("New Checkout")
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Stub multiple at once
|
|
191
|
-
stub_subflags(
|
|
192
|
-
new_checkout: true,
|
|
193
|
-
max_projects: 100,
|
|
194
|
-
headline: "Welcome!"
|
|
195
|
-
)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
218
|
## Request Caching
|
|
199
219
|
|
|
200
220
|
Enable per-request caching to avoid multiple API calls for the same flag:
|
|
@@ -214,6 +234,25 @@ subflag_enabled?(:new_checkout) # Cache hit
|
|
|
214
234
|
subflag_enabled?(:new_checkout) # Cache hit
|
|
215
235
|
```
|
|
216
236
|
|
|
237
|
+
## Cross-Request Caching
|
|
238
|
+
|
|
239
|
+
By default, prefetched flags are only cached for the current request. To cache across multiple requests using `Rails.cache`, set a TTL:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
# config/initializers/subflag.rb
|
|
243
|
+
Subflag::Rails.configure do |config|
|
|
244
|
+
config.api_key = Rails.application.credentials.subflag_api_key
|
|
245
|
+
config.cache_ttl = 30.seconds # Cache flags in Rails.cache for 30 seconds
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
With `cache_ttl` set:
|
|
250
|
+
- First request fetches from API and stores in `Rails.cache`
|
|
251
|
+
- Subsequent requests (within TTL) read from `Rails.cache` — no API call
|
|
252
|
+
- After TTL expires, next request fetches fresh data
|
|
253
|
+
|
|
254
|
+
This significantly reduces API load for high-traffic applications. Choose a TTL that balances freshness with performance — 30 seconds is a good starting point.
|
|
255
|
+
|
|
217
256
|
## Bulk Flag Evaluation (Prefetch)
|
|
218
257
|
|
|
219
258
|
For optimal performance, prefetch all flags for a user in a single API call. This is especially useful when your page checks multiple flags:
|
|
@@ -263,25 +302,6 @@ subflag_prefetch(admin_user)
|
|
|
263
302
|
subflag_prefetch(current_user, context: { device: "mobile" })
|
|
264
303
|
```
|
|
265
304
|
|
|
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
305
|
### Direct API
|
|
286
306
|
|
|
287
307
|
You can also use the module method directly:
|
|
@@ -296,13 +316,16 @@ Subflag::Rails.prefetch_flags(user: current_user)
|
|
|
296
316
|
|
|
297
317
|
```ruby
|
|
298
318
|
Subflag::Rails.configure do |config|
|
|
299
|
-
#
|
|
319
|
+
# Backend: :subflag (cloud), :active_record (self-hosted), :memory (testing)
|
|
320
|
+
config.backend = :subflag
|
|
321
|
+
|
|
322
|
+
# API key - required for :subflag backend
|
|
300
323
|
config.api_key = "sdk-production-..."
|
|
301
324
|
|
|
302
325
|
# API URL (default: https://api.subflag.com)
|
|
303
326
|
config.api_url = "https://api.subflag.com"
|
|
304
327
|
|
|
305
|
-
# Cross-request caching via Rails.cache (optional)
|
|
328
|
+
# Cross-request caching via Rails.cache (optional, :subflag backend only)
|
|
306
329
|
# When set, prefetched flags are cached for this duration
|
|
307
330
|
config.cache_ttl = 30.seconds
|
|
308
331
|
|
|
@@ -310,13 +333,72 @@ Subflag::Rails.configure do |config|
|
|
|
310
333
|
config.logging_enabled = Rails.env.development?
|
|
311
334
|
config.log_level = :debug # :debug, :info, :warn
|
|
312
335
|
|
|
313
|
-
# User context
|
|
336
|
+
# User context - works with all backends
|
|
314
337
|
config.user_context do |user|
|
|
315
338
|
{ targeting_key: user.id.to_s, plan: user.plan }
|
|
316
339
|
end
|
|
317
340
|
end
|
|
318
341
|
```
|
|
319
342
|
|
|
343
|
+
### ActiveRecord Flag Model
|
|
344
|
+
|
|
345
|
+
When using `backend: :active_record`, flags are stored in the `subflag_flags` table:
|
|
346
|
+
|
|
347
|
+
| Column | Type | Description |
|
|
348
|
+
|--------|------|-------------|
|
|
349
|
+
| `key` | string | Flag name (lowercase, dashes, e.g., `new-checkout`) |
|
|
350
|
+
| `value` | text | The flag value as a string |
|
|
351
|
+
| `value_type` | string | Type: `boolean`, `string`, `integer`, `float`, `object` |
|
|
352
|
+
| `enabled` | boolean | Whether the flag is active (default: true) |
|
|
353
|
+
| `description` | text | Optional description |
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
# Create flags
|
|
357
|
+
Subflag::Rails::Flag.create!(key: "max-projects", value: "100", value_type: "integer")
|
|
358
|
+
|
|
359
|
+
# Query flags
|
|
360
|
+
Subflag::Rails::Flag.enabled.find_each { |f| puts "#{f.key}: #{f.typed_value}" }
|
|
361
|
+
|
|
362
|
+
# Disable a flag
|
|
363
|
+
Subflag::Rails::Flag.find_by(key: "new-checkout")&.update!(enabled: false)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Testing
|
|
367
|
+
|
|
368
|
+
Stub flags in your tests:
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
# spec/rails_helper.rb (RSpec)
|
|
372
|
+
require "subflag/rails/test_helpers"
|
|
373
|
+
RSpec.configure do |config|
|
|
374
|
+
config.include Subflag::Rails::TestHelpers
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# test/test_helper.rb (Minitest)
|
|
378
|
+
require "subflag/rails/test_helpers"
|
|
379
|
+
class ActiveSupport::TestCase
|
|
380
|
+
include Subflag::Rails::TestHelpers
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
# In your specs/tests
|
|
386
|
+
it "shows new checkout when enabled" do
|
|
387
|
+
stub_subflag(:new_checkout, true)
|
|
388
|
+
stub_subflag(:max_projects, 100)
|
|
389
|
+
|
|
390
|
+
visit checkout_path
|
|
391
|
+
expect(page).to have_content("New Checkout")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Stub multiple at once
|
|
395
|
+
stub_subflags(
|
|
396
|
+
new_checkout: true,
|
|
397
|
+
max_projects: 100,
|
|
398
|
+
headline: "Welcome!"
|
|
399
|
+
)
|
|
400
|
+
```
|
|
401
|
+
|
|
320
402
|
## Documentation
|
|
321
403
|
|
|
322
404
|
- [Subflag Docs](https://docs.subflag.com)
|
|
@@ -7,49 +7,113 @@ module Subflag
|
|
|
7
7
|
# Generator for setting up Subflag in a Rails application
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
10
|
-
# rails generate subflag:install
|
|
10
|
+
# rails generate subflag:install # Default: Subflag Cloud
|
|
11
|
+
# rails generate subflag:install --backend=subflag # Explicit: Subflag Cloud
|
|
12
|
+
# rails generate subflag:install --backend=active_record # Self-hosted DB
|
|
13
|
+
# rails generate subflag:install --backend=memory # Testing only
|
|
11
14
|
#
|
|
12
15
|
class InstallGenerator < ::Rails::Generators::Base
|
|
16
|
+
include ::Rails::Generators::Migration if defined?(::Rails::Generators::Migration)
|
|
17
|
+
|
|
13
18
|
source_root File.expand_path("templates", __dir__)
|
|
14
19
|
|
|
15
|
-
desc "Creates a Subflag initializer and
|
|
20
|
+
desc "Creates a Subflag initializer and optionally a migration for ActiveRecord backend"
|
|
21
|
+
|
|
22
|
+
class_option :backend, type: :string, default: "subflag",
|
|
23
|
+
desc: "Backend to use: subflag (cloud), active_record (self-hosted), or memory (testing)"
|
|
24
|
+
|
|
25
|
+
def self.next_migration_number(dirname)
|
|
26
|
+
if defined?(::ActiveRecord::Generators::Base)
|
|
27
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
28
|
+
else
|
|
29
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
16
32
|
|
|
17
33
|
def create_initializer
|
|
18
|
-
template "initializer.rb", "config/initializers/subflag.rb"
|
|
34
|
+
template "initializer.rb.tt", "config/initializers/subflag.rb"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_migration
|
|
38
|
+
return unless options[:backend] == "active_record"
|
|
39
|
+
|
|
40
|
+
migration_template "create_subflag_flags.rb.tt",
|
|
41
|
+
"db/migrate/create_subflag_flags.rb"
|
|
19
42
|
end
|
|
20
43
|
|
|
21
44
|
def show_instructions
|
|
22
45
|
say ""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
|
|
47
|
+
case options[:backend]
|
|
48
|
+
when "active_record"
|
|
49
|
+
say "Subflag installed with ActiveRecord backend!", :green
|
|
50
|
+
say ""
|
|
51
|
+
say "Next steps:"
|
|
52
|
+
say ""
|
|
53
|
+
say "1. Run the migration:"
|
|
54
|
+
say " $ rails db:migrate"
|
|
55
|
+
say ""
|
|
56
|
+
say "2. Create your first flag:"
|
|
57
|
+
say ""
|
|
58
|
+
say " Subflag::Rails::Flag.create!("
|
|
59
|
+
say " key: 'new-checkout',"
|
|
60
|
+
say " value: 'true',"
|
|
61
|
+
say " value_type: 'boolean'"
|
|
62
|
+
say " )"
|
|
63
|
+
say ""
|
|
64
|
+
say "3. Use flags in your code:"
|
|
65
|
+
say ""
|
|
66
|
+
say " if subflag_enabled?(:new_checkout)"
|
|
67
|
+
say " # ..."
|
|
68
|
+
say " end"
|
|
69
|
+
say ""
|
|
70
|
+
say "When you're ready for a dashboard, environments, and user targeting:"
|
|
71
|
+
say " https://subflag.com", :yellow
|
|
72
|
+
say ""
|
|
73
|
+
|
|
74
|
+
when "memory"
|
|
75
|
+
say "Subflag installed with Memory backend!", :green
|
|
76
|
+
say ""
|
|
77
|
+
say "Note: Memory backend is for testing only. Flags reset on restart."
|
|
78
|
+
say ""
|
|
79
|
+
say "Set flags in your tests or initializer:"
|
|
80
|
+
say ""
|
|
81
|
+
say " Subflag::Rails.provider.set(:new_checkout, true)"
|
|
82
|
+
say " Subflag::Rails.provider.set(:max_projects, 100)"
|
|
83
|
+
say ""
|
|
84
|
+
say "Use flags:"
|
|
85
|
+
say ""
|
|
86
|
+
say " subflag_enabled?(:new_checkout) # => true"
|
|
87
|
+
say " subflag_value(:max_projects, default: 3) # => 100"
|
|
88
|
+
say ""
|
|
89
|
+
|
|
90
|
+
else # subflag (cloud)
|
|
91
|
+
say "Subflag installed!", :green
|
|
92
|
+
say ""
|
|
93
|
+
say "Next steps:"
|
|
94
|
+
say ""
|
|
95
|
+
say "1. Add your API key to Rails credentials:"
|
|
96
|
+
say " $ rails credentials:edit"
|
|
97
|
+
say ""
|
|
98
|
+
say " subflag:"
|
|
99
|
+
say " api_key: sdk-production-your-key-here"
|
|
100
|
+
say ""
|
|
101
|
+
say " Or set SUBFLAG_API_KEY environment variable."
|
|
102
|
+
say ""
|
|
103
|
+
say "2. Configure user context in config/initializers/subflag.rb"
|
|
104
|
+
say ""
|
|
105
|
+
say "3. Use flags in your code:"
|
|
106
|
+
say ""
|
|
107
|
+
say " # Controller (auto-scoped to current_user)"
|
|
108
|
+
say " if subflag_enabled?(:new_checkout)"
|
|
109
|
+
say " # ..."
|
|
110
|
+
say " end"
|
|
111
|
+
say ""
|
|
112
|
+
say " max = subflag_value(:max_projects, default: 3)"
|
|
113
|
+
say ""
|
|
114
|
+
say "Docs: https://docs.subflag.com/rails"
|
|
115
|
+
say ""
|
|
116
|
+
end
|
|
53
117
|
end
|
|
54
118
|
end
|
|
55
119
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSubflagFlags < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :subflag_flags do |t|
|
|
6
|
+
t.string :key, null: false
|
|
7
|
+
t.string :value_type, null: false, default: "boolean"
|
|
8
|
+
t.text :value, null: false
|
|
9
|
+
t.boolean :enabled, null: false, default: true
|
|
10
|
+
t.text :description
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_index :subflag_flags, :key, unique: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -2,17 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Subflag configuration
|
|
4
4
|
#
|
|
5
|
+
<% if options[:backend] == "subflag" -%>
|
|
5
6
|
# API key is automatically loaded from:
|
|
6
7
|
# 1. Rails credentials (subflag.api_key or subflag_api_key)
|
|
7
8
|
# 2. SUBFLAG_API_KEY environment variable
|
|
9
|
+
<% elsif options[:backend] == "active_record" -%>
|
|
10
|
+
# Using ActiveRecord backend - flags stored in subflag_flags table
|
|
11
|
+
<% else -%>
|
|
12
|
+
# Using Memory backend - flags stored in memory (testing only)
|
|
13
|
+
<% end -%>
|
|
8
14
|
|
|
9
15
|
Subflag::Rails.configure do |config|
|
|
10
|
-
#
|
|
11
|
-
|
|
16
|
+
# Backend: :subflag (cloud), :active_record (self-hosted), :memory (testing)
|
|
17
|
+
config.backend = :<%= options[:backend] %>
|
|
18
|
+
<% if options[:backend] == "subflag" -%>
|
|
19
|
+
|
|
20
|
+
# Your Subflag API key
|
|
21
|
+
# Get one at https://subflag.com
|
|
22
|
+
config.api_key = ENV["SUBFLAG_API_KEY"] || Rails.application.credentials.dig(:subflag, :api_key)
|
|
23
|
+
<% end -%>
|
|
24
|
+
<% if options[:backend] != "memory" -%>
|
|
12
25
|
|
|
13
26
|
# Enable logging in development
|
|
14
27
|
config.logging_enabled = Rails.env.development?
|
|
15
28
|
config.log_level = :debug
|
|
29
|
+
<% end -%>
|
|
16
30
|
|
|
17
31
|
# Configure user context for targeting
|
|
18
32
|
# This enables per-user flag values (e.g., different limits by plan)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
module Rails
|
|
5
|
+
module Backends
|
|
6
|
+
# Provider that reads flags from your Rails database
|
|
7
|
+
#
|
|
8
|
+
# Stores flags in a `subflag_flags` table with typed values.
|
|
9
|
+
# Perfect for teams who want self-hosted feature flags without external dependencies.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Subflag::Rails.configure do |config|
|
|
13
|
+
# config.backend = :active_record
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # Create a flag
|
|
17
|
+
# Subflag::Rails::Flag.create!(
|
|
18
|
+
# key: "max-projects",
|
|
19
|
+
# value: "100",
|
|
20
|
+
# value_type: "integer",
|
|
21
|
+
# enabled: true
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# # Use it
|
|
25
|
+
# subflag_value(:max_projects, default: 3) # => 100
|
|
26
|
+
#
|
|
27
|
+
class ActiveRecordProvider
|
|
28
|
+
def metadata
|
|
29
|
+
{ name: "Subflag ActiveRecord Provider" }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def init; end
|
|
33
|
+
def shutdown; end
|
|
34
|
+
|
|
35
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
36
|
+
resolve(flag_key, default_value, :boolean)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
40
|
+
resolve(flag_key, default_value, :string)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
44
|
+
resolve(flag_key, default_value, :number)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
48
|
+
resolve(flag_key, default_value, :integer)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
52
|
+
resolve(flag_key, default_value, :float)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
56
|
+
resolve(flag_key, default_value, :object)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def resolve(flag_key, default_value, expected_type)
|
|
62
|
+
flag = Subflag::Rails::Flag.find_by(key: flag_key)
|
|
63
|
+
|
|
64
|
+
unless flag&.enabled?
|
|
65
|
+
return resolution(default_value, reason: :default)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
value = flag.typed_value(expected_type)
|
|
69
|
+
resolution(value, reason: :static, variant: "default")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def resolution(value, reason:, variant: nil)
|
|
73
|
+
OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
74
|
+
value: value,
|
|
75
|
+
reason: reason,
|
|
76
|
+
variant: variant
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
module Rails
|
|
5
|
+
module Backends
|
|
6
|
+
# In-memory provider for testing and development
|
|
7
|
+
#
|
|
8
|
+
# Flags are stored in a hash and reset when the process restarts.
|
|
9
|
+
# Useful for unit tests and local development without external dependencies.
|
|
10
|
+
#
|
|
11
|
+
# @example In tests
|
|
12
|
+
# Subflag::Rails.configure do |config|
|
|
13
|
+
# config.backend = :memory
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # Set flags directly
|
|
17
|
+
# Subflag::Rails.provider.set(:new_checkout, true)
|
|
18
|
+
# Subflag::Rails.provider.set(:max_projects, 100)
|
|
19
|
+
#
|
|
20
|
+
# # Use them
|
|
21
|
+
# subflag_enabled?(:new_checkout) # => true
|
|
22
|
+
#
|
|
23
|
+
class MemoryProvider
|
|
24
|
+
def initialize
|
|
25
|
+
@flags = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def metadata
|
|
29
|
+
{ name: "Subflag Memory Provider" }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def init; end
|
|
33
|
+
def shutdown; end
|
|
34
|
+
|
|
35
|
+
# Set a flag value programmatically
|
|
36
|
+
#
|
|
37
|
+
# @param key [String, Symbol] The flag key (underscores converted to dashes)
|
|
38
|
+
# @param value [Object] The flag value
|
|
39
|
+
# @param enabled [Boolean] Whether the flag is enabled (default: true)
|
|
40
|
+
def set(key, value, enabled: true)
|
|
41
|
+
@flags[normalize_key(key)] = { value: value, enabled: enabled }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Clear all flags
|
|
45
|
+
def clear
|
|
46
|
+
@flags.clear
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get all flags (for debugging)
|
|
50
|
+
def all
|
|
51
|
+
@flags.dup
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
55
|
+
resolve(flag_key, default_value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
59
|
+
resolve(flag_key, default_value)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
63
|
+
resolve(flag_key, default_value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
67
|
+
resolve(flag_key, default_value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
71
|
+
resolve(flag_key, default_value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
75
|
+
resolve(flag_key, default_value)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def normalize_key(key)
|
|
81
|
+
key.to_s.tr("_", "-")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve(flag_key, default_value)
|
|
85
|
+
flag = @flags[flag_key.to_s]
|
|
86
|
+
|
|
87
|
+
unless flag && flag[:enabled]
|
|
88
|
+
return resolution(default_value, reason: :default)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
resolution(flag[:value], reason: :static, variant: "default")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def resolution(value, reason:, variant: nil)
|
|
95
|
+
OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
96
|
+
value: value,
|
|
97
|
+
reason: reason,
|
|
98
|
+
variant: variant
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
module Rails
|
|
5
|
+
module Backends
|
|
6
|
+
# Provider wrapper for Subflag Cloud SaaS
|
|
7
|
+
#
|
|
8
|
+
# Delegates to the standalone subflag-openfeature-provider gem.
|
|
9
|
+
# This is the default backend when using Subflag::Rails.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Subflag::Rails.configure do |config|
|
|
13
|
+
# config.backend = :subflag
|
|
14
|
+
# config.api_key = "sdk-production-..."
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
class SubflagProvider
|
|
18
|
+
def initialize(api_key:, api_url:)
|
|
19
|
+
require "subflag"
|
|
20
|
+
@provider = ::Subflag::Provider.new(api_key: api_key, api_url: api_url)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def metadata
|
|
24
|
+
@provider.metadata
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def init
|
|
28
|
+
@provider.init
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shutdown
|
|
32
|
+
@provider.shutdown
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
36
|
+
@provider.fetch_boolean_value(
|
|
37
|
+
flag_key: flag_key,
|
|
38
|
+
default_value: default_value,
|
|
39
|
+
evaluation_context: evaluation_context
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
44
|
+
@provider.fetch_string_value(
|
|
45
|
+
flag_key: flag_key,
|
|
46
|
+
default_value: default_value,
|
|
47
|
+
evaluation_context: evaluation_context
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
52
|
+
@provider.fetch_number_value(
|
|
53
|
+
flag_key: flag_key,
|
|
54
|
+
default_value: default_value,
|
|
55
|
+
evaluation_context: evaluation_context
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
60
|
+
@provider.fetch_integer_value(
|
|
61
|
+
flag_key: flag_key,
|
|
62
|
+
default_value: default_value,
|
|
63
|
+
evaluation_context: evaluation_context
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
68
|
+
@provider.fetch_float_value(
|
|
69
|
+
flag_key: flag_key,
|
|
70
|
+
default_value: default_value,
|
|
71
|
+
evaluation_context: evaluation_context
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
76
|
+
@provider.fetch_object_value(
|
|
77
|
+
flag_key: flag_key,
|
|
78
|
+
default_value: default_value,
|
|
79
|
+
evaluation_context: evaluation_context
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/subflag/rails/client.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Subflag
|
|
4
4
|
module Rails
|
|
5
|
+
# Lightweight struct for caching prefetched flag results
|
|
6
|
+
# Used by ActiveRecord and Memory backends where we don't have Subflag::EvaluationResult
|
|
7
|
+
PrefetchedFlag = Struct.new(:flag_key, :value, :reason, :variant, keyword_init: true)
|
|
8
|
+
|
|
5
9
|
# Client for evaluating feature flags
|
|
6
10
|
#
|
|
7
11
|
# This is the low-level client used by FlagAccessor.
|
|
@@ -10,8 +14,11 @@ module Subflag
|
|
|
10
14
|
class Client
|
|
11
15
|
# Prefetch all flags for a user/context
|
|
12
16
|
#
|
|
13
|
-
# Fetches all flags
|
|
14
|
-
#
|
|
17
|
+
# Fetches all flags and caches them for subsequent lookups.
|
|
18
|
+
# Behavior varies by backend:
|
|
19
|
+
# - :subflag — Single API call to fetch all flags
|
|
20
|
+
# - :active_record — Single DB query to load all enabled flags
|
|
21
|
+
# - :memory — No-op (flags already in memory)
|
|
15
22
|
#
|
|
16
23
|
# @param user [Object, nil] The user object for targeting
|
|
17
24
|
# @param context [Hash, nil] Additional context attributes
|
|
@@ -23,16 +30,17 @@ module Subflag
|
|
|
23
30
|
# subflag_enabled?(:new_feature)
|
|
24
31
|
#
|
|
25
32
|
def prefetch_all(user: nil, context: nil)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
case configuration.backend
|
|
34
|
+
when :subflag
|
|
35
|
+
prefetch_from_subflag_api(user: user, context: context)
|
|
36
|
+
when :active_record
|
|
37
|
+
prefetch_from_active_record(user: user, context: context)
|
|
38
|
+
when :memory
|
|
39
|
+
# Already in memory, nothing to prefetch
|
|
40
|
+
[]
|
|
41
|
+
else
|
|
42
|
+
[]
|
|
32
43
|
end
|
|
33
|
-
|
|
34
|
-
# Otherwise fetch directly from API (per-request cache only)
|
|
35
|
-
prefetch_from_api(ctx, context_hash)
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# Check if a boolean flag is enabled
|
|
@@ -121,6 +129,20 @@ module Subflag
|
|
|
121
129
|
|
|
122
130
|
private
|
|
123
131
|
|
|
132
|
+
# Prefetch flags from Subflag Cloud API
|
|
133
|
+
def prefetch_from_subflag_api(user:, context:)
|
|
134
|
+
ctx = ContextBuilder.build(user: user, context: context)
|
|
135
|
+
context_hash = ctx ? ctx.hash : "no_context"
|
|
136
|
+
|
|
137
|
+
# Use Rails.cache for cross-request caching if enabled
|
|
138
|
+
if configuration.rails_cache_enabled?
|
|
139
|
+
return prefetch_with_rails_cache(ctx, context_hash)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Otherwise fetch directly from API (per-request cache only)
|
|
143
|
+
prefetch_from_api(ctx, context_hash)
|
|
144
|
+
end
|
|
145
|
+
|
|
124
146
|
# Fetch flags from API and populate RequestCache
|
|
125
147
|
def prefetch_from_api(ctx, context_hash)
|
|
126
148
|
subflag_context = build_subflag_context(ctx)
|
|
@@ -151,7 +173,7 @@ module Subflag
|
|
|
151
173
|
cached_data
|
|
152
174
|
end
|
|
153
175
|
|
|
154
|
-
# Populate RequestCache from cached hash data
|
|
176
|
+
# Populate RequestCache from cached hash data (Subflag API)
|
|
155
177
|
def populate_request_cache_from_data(data_array, context_hash)
|
|
156
178
|
return unless RequestCache.enabled?
|
|
157
179
|
|
|
@@ -161,6 +183,30 @@ module Subflag
|
|
|
161
183
|
end
|
|
162
184
|
end
|
|
163
185
|
|
|
186
|
+
# Prefetch flags from ActiveRecord database
|
|
187
|
+
# Loads all enabled flags in one query and caches their values
|
|
188
|
+
def prefetch_from_active_record(user:, context:)
|
|
189
|
+
return [] unless RequestCache.enabled?
|
|
190
|
+
|
|
191
|
+
ctx = ContextBuilder.build(user: user, context: context)
|
|
192
|
+
context_hash = ctx ? ctx.hash : "no_context"
|
|
193
|
+
|
|
194
|
+
prefetched = []
|
|
195
|
+
|
|
196
|
+
Subflag::Rails::Flag.enabled.find_each do |flag|
|
|
197
|
+
prefetch_key = "subflag:prefetch:#{flag.key}:#{context_hash}"
|
|
198
|
+
RequestCache.current_cache[prefetch_key] = PrefetchedFlag.new(
|
|
199
|
+
flag_key: flag.key,
|
|
200
|
+
value: flag.typed_value,
|
|
201
|
+
reason: "STATIC",
|
|
202
|
+
variant: "default"
|
|
203
|
+
)
|
|
204
|
+
prefetched << { flag_key: flag.key, value: flag.typed_value }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
prefetched
|
|
208
|
+
end
|
|
209
|
+
|
|
164
210
|
def build_cache_key(flag_key, ctx, type)
|
|
165
211
|
context_hash = ctx ? ctx.hash : "no_context"
|
|
166
212
|
"subflag:#{flag_key}:#{context_hash}:#{type}"
|
|
@@ -23,6 +23,14 @@ module Subflag
|
|
|
23
23
|
# end
|
|
24
24
|
#
|
|
25
25
|
class Configuration
|
|
26
|
+
VALID_BACKENDS = %i[subflag active_record memory].freeze
|
|
27
|
+
|
|
28
|
+
# @return [Symbol] Backend to use (:subflag, :active_record, :memory)
|
|
29
|
+
# - :subflag — Subflag Cloud SaaS (default)
|
|
30
|
+
# - :active_record — Self-hosted, flags stored in your database
|
|
31
|
+
# - :memory — In-memory store for testing
|
|
32
|
+
attr_reader :backend
|
|
33
|
+
|
|
26
34
|
# @return [String, nil] The Subflag API key
|
|
27
35
|
attr_accessor :api_key
|
|
28
36
|
|
|
@@ -41,6 +49,7 @@ module Subflag
|
|
|
41
49
|
attr_accessor :cache_ttl
|
|
42
50
|
|
|
43
51
|
def initialize
|
|
52
|
+
@backend = :subflag
|
|
44
53
|
@api_key = nil
|
|
45
54
|
@api_url = "https://api.subflag.com"
|
|
46
55
|
@user_context_block = nil
|
|
@@ -49,6 +58,19 @@ module Subflag
|
|
|
49
58
|
@cache_ttl = nil
|
|
50
59
|
end
|
|
51
60
|
|
|
61
|
+
# Set the backend with validation
|
|
62
|
+
#
|
|
63
|
+
# @param value [Symbol] The backend to use
|
|
64
|
+
# @raise [ArgumentError] If the backend is invalid
|
|
65
|
+
def backend=(value)
|
|
66
|
+
value = value.to_sym
|
|
67
|
+
unless VALID_BACKENDS.include?(value)
|
|
68
|
+
raise ArgumentError, "Invalid backend: #{value}. Use one of: #{VALID_BACKENDS.join(', ')}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@backend = value
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
# Check if cross-request caching via Rails.cache is enabled
|
|
53
75
|
#
|
|
54
76
|
# @return [Boolean]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
module Rails
|
|
5
|
+
# ActiveRecord model for storing feature flags in your database
|
|
6
|
+
#
|
|
7
|
+
# @example Create a boolean flag
|
|
8
|
+
# Subflag::Rails::Flag.create!(
|
|
9
|
+
# key: "new-checkout",
|
|
10
|
+
# value: "true",
|
|
11
|
+
# value_type: "boolean"
|
|
12
|
+
# )
|
|
13
|
+
#
|
|
14
|
+
# @example Create an integer flag
|
|
15
|
+
# Subflag::Rails::Flag.create!(
|
|
16
|
+
# key: "max-projects",
|
|
17
|
+
# value: "100",
|
|
18
|
+
# value_type: "integer"
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Create a JSON object flag
|
|
22
|
+
# Subflag::Rails::Flag.create!(
|
|
23
|
+
# key: "feature-limits",
|
|
24
|
+
# value: '{"max_items": 10, "max_users": 5}',
|
|
25
|
+
# value_type: "object"
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
class Flag < ::ActiveRecord::Base
|
|
29
|
+
self.table_name = "subflag_flags"
|
|
30
|
+
|
|
31
|
+
VALUE_TYPES = %w[boolean string integer float object].freeze
|
|
32
|
+
|
|
33
|
+
validates :key, presence: true,
|
|
34
|
+
uniqueness: true,
|
|
35
|
+
format: { with: /\A[a-z0-9\-]+\z/, message: "only lowercase letters, numbers, and dashes" }
|
|
36
|
+
validates :value_type, inclusion: { in: VALUE_TYPES }
|
|
37
|
+
validates :value, presence: true
|
|
38
|
+
|
|
39
|
+
scope :enabled, -> { where(enabled: true) }
|
|
40
|
+
|
|
41
|
+
# Get the flag value cast to its declared type
|
|
42
|
+
#
|
|
43
|
+
# @param expected_type [Symbol, String, nil] Override the value_type for casting
|
|
44
|
+
# @return [Object] The typed value
|
|
45
|
+
def typed_value(expected_type = nil)
|
|
46
|
+
type = expected_type&.to_s || value_type
|
|
47
|
+
|
|
48
|
+
case type.to_s
|
|
49
|
+
when "boolean"
|
|
50
|
+
ActiveModel::Type::Boolean.new.cast(value)
|
|
51
|
+
when "string"
|
|
52
|
+
value.to_s
|
|
53
|
+
when "integer"
|
|
54
|
+
value.to_i
|
|
55
|
+
when "float", "number"
|
|
56
|
+
value.to_f
|
|
57
|
+
when "object"
|
|
58
|
+
value.is_a?(Hash) ? value : JSON.parse(value)
|
|
59
|
+
else
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/subflag/rails.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open_feature/sdk"
|
|
4
|
-
require "subflag"
|
|
5
4
|
|
|
6
5
|
require_relative "rails/version"
|
|
7
6
|
require_relative "rails/configuration"
|
|
@@ -27,14 +26,25 @@ module Subflag
|
|
|
27
26
|
|
|
28
27
|
# Configure Subflag for Rails
|
|
29
28
|
#
|
|
30
|
-
# @example
|
|
29
|
+
# @example Using Subflag Cloud (SaaS)
|
|
31
30
|
# Subflag::Rails.configure do |config|
|
|
31
|
+
# config.backend = :subflag
|
|
32
32
|
# config.api_key = Rails.application.credentials.subflag_api_key
|
|
33
33
|
# config.user_context do |user|
|
|
34
34
|
# { targeting_key: user.id.to_s, email: user.email, plan: user.plan }
|
|
35
35
|
# end
|
|
36
36
|
# end
|
|
37
37
|
#
|
|
38
|
+
# @example Using ActiveRecord (self-hosted)
|
|
39
|
+
# Subflag::Rails.configure do |config|
|
|
40
|
+
# config.backend = :active_record
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# @example Using Memory (testing)
|
|
44
|
+
# Subflag::Rails.configure do |config|
|
|
45
|
+
# config.backend = :memory
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
38
48
|
# @yield [Configuration]
|
|
39
49
|
def configure
|
|
40
50
|
yield(configuration)
|
|
@@ -48,6 +58,16 @@ module Subflag
|
|
|
48
58
|
@client ||= Client.new
|
|
49
59
|
end
|
|
50
60
|
|
|
61
|
+
# Access the current provider instance
|
|
62
|
+
#
|
|
63
|
+
# Useful for the Memory backend where you can set flags directly:
|
|
64
|
+
# Subflag::Rails.provider.set(:my_flag, true)
|
|
65
|
+
#
|
|
66
|
+
# @return [Object] The current OpenFeature provider
|
|
67
|
+
def provider
|
|
68
|
+
@provider
|
|
69
|
+
end
|
|
70
|
+
|
|
51
71
|
# Prefetch all flags for a user/context in a single API call
|
|
52
72
|
#
|
|
53
73
|
# Call this early in a request to fetch all flags at once.
|
|
@@ -72,21 +92,52 @@ module Subflag
|
|
|
72
92
|
def reset!
|
|
73
93
|
@configuration = Configuration.new
|
|
74
94
|
@client = nil
|
|
95
|
+
@provider = nil
|
|
75
96
|
end
|
|
76
97
|
|
|
77
98
|
private
|
|
78
99
|
|
|
79
100
|
def setup_provider
|
|
80
|
-
|
|
101
|
+
@provider = build_provider
|
|
102
|
+
return unless @provider
|
|
81
103
|
|
|
82
|
-
|
|
104
|
+
OpenFeature::SDK.configure do |config|
|
|
105
|
+
config.set_provider(@provider)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_provider
|
|
110
|
+
case configuration.backend
|
|
111
|
+
when :subflag
|
|
112
|
+
build_subflag_provider
|
|
113
|
+
when :active_record
|
|
114
|
+
build_active_record_provider
|
|
115
|
+
when :memory
|
|
116
|
+
build_memory_provider
|
|
117
|
+
else
|
|
118
|
+
raise ArgumentError, "Unknown backend: #{configuration.backend}. Use :subflag, :active_record, or :memory"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_subflag_provider
|
|
123
|
+
return nil unless configuration.api_key
|
|
124
|
+
|
|
125
|
+
require_relative "rails/backends/subflag_provider"
|
|
126
|
+
Backends::SubflagProvider.new(
|
|
83
127
|
api_key: configuration.api_key,
|
|
84
128
|
api_url: configuration.api_url
|
|
85
129
|
)
|
|
130
|
+
end
|
|
86
131
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
132
|
+
def build_active_record_provider
|
|
133
|
+
require_relative "rails/backends/active_record_provider"
|
|
134
|
+
require_relative "rails/models/flag"
|
|
135
|
+
Backends::ActiveRecordProvider.new
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_memory_provider
|
|
139
|
+
require_relative "rails/backends/memory_provider"
|
|
140
|
+
Backends::MemoryProvider.new
|
|
90
141
|
end
|
|
91
142
|
end
|
|
92
143
|
end
|
metadata
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: subflag-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.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-12-
|
|
11
|
+
date: 2025-12-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: openfeature-sdk
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.3
|
|
19
|
+
version: '0.3'
|
|
20
20
|
- - "<"
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
22
|
version: '1.0'
|
|
@@ -26,7 +26,7 @@ dependencies:
|
|
|
26
26
|
requirements:
|
|
27
27
|
- - ">="
|
|
28
28
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: 0.3
|
|
29
|
+
version: '0.3'
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '1.0'
|
|
@@ -170,9 +170,9 @@ dependencies:
|
|
|
170
170
|
- - ">="
|
|
171
171
|
- !ruby/object:Gem::Version
|
|
172
172
|
version: '6.1'
|
|
173
|
-
description:
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
description: Feature flags for Rails with pluggable backends. Use Subflag Cloud (SaaS),
|
|
174
|
+
ActiveRecord (self-hosted), or Memory (testing). Get typed values (boolean, string,
|
|
175
|
+
integer, double, object) with the same API regardless of backend.
|
|
176
176
|
email:
|
|
177
177
|
- support@subflag.com
|
|
178
178
|
executables: []
|
|
@@ -183,15 +183,20 @@ files:
|
|
|
183
183
|
- LICENSE.txt
|
|
184
184
|
- README.md
|
|
185
185
|
- lib/generators/subflag/install_generator.rb
|
|
186
|
-
- lib/generators/subflag/templates/
|
|
186
|
+
- lib/generators/subflag/templates/create_subflag_flags.rb.tt
|
|
187
|
+
- lib/generators/subflag/templates/initializer.rb.tt
|
|
187
188
|
- lib/subflag-rails.rb
|
|
188
189
|
- lib/subflag/rails.rb
|
|
190
|
+
- lib/subflag/rails/backends/active_record_provider.rb
|
|
191
|
+
- lib/subflag/rails/backends/memory_provider.rb
|
|
192
|
+
- lib/subflag/rails/backends/subflag_provider.rb
|
|
189
193
|
- lib/subflag/rails/client.rb
|
|
190
194
|
- lib/subflag/rails/configuration.rb
|
|
191
195
|
- lib/subflag/rails/context_builder.rb
|
|
192
196
|
- lib/subflag/rails/evaluation_result.rb
|
|
193
197
|
- lib/subflag/rails/flag_accessor.rb
|
|
194
198
|
- lib/subflag/rails/helpers.rb
|
|
199
|
+
- lib/subflag/rails/models/flag.rb
|
|
195
200
|
- lib/subflag/rails/railtie.rb
|
|
196
201
|
- lib/subflag/rails/request_cache.rb
|
|
197
202
|
- lib/subflag/rails/test_helpers.rb
|