subflag-rails 0.2.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/CHANGELOG.md +12 -0
- data/README.md +201 -37
- 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 +213 -0
- data/lib/subflag/rails/configuration.rb +38 -0
- data/lib/subflag/rails/evaluation_result.rb +16 -0
- data/lib/subflag/rails/helpers.rb +30 -0
- data/lib/subflag/rails/models/flag.rb +65 -0
- data/lib/subflag/rails/version.rb +1 -1
- data/lib/subflag/rails.rb +93 -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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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
|
+
|
|
5
17
|
## [0.2.0] - 2025-11-30
|
|
6
18
|
|
|
7
19
|
### Changed
|
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,82 +215,190 @@ 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
|
-
##
|
|
218
|
+
## Request Caching
|
|
163
219
|
|
|
164
|
-
|
|
220
|
+
Enable per-request caching to avoid multiple API calls for the same flag:
|
|
165
221
|
|
|
166
222
|
```ruby
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
config.include Subflag::Rails::TestHelpers
|
|
171
|
-
end
|
|
223
|
+
# config/application.rb
|
|
224
|
+
config.middleware.use Subflag::Rails::RequestCache::Middleware
|
|
225
|
+
```
|
|
172
226
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
227
|
+
Now multiple checks for the same flag in one request hit the API only once:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
# Without caching: 3 API calls
|
|
231
|
+
# With caching: 1 API call (cached for subsequent checks)
|
|
232
|
+
subflag_enabled?(:new_checkout) # API call
|
|
233
|
+
subflag_enabled?(:new_checkout) # Cache hit
|
|
234
|
+
subflag_enabled?(:new_checkout) # Cache hit
|
|
235
|
+
```
|
|
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
|
|
177
246
|
end
|
|
178
247
|
```
|
|
179
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
|
+
|
|
256
|
+
## Bulk Flag Evaluation (Prefetch)
|
|
257
|
+
|
|
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:
|
|
259
|
+
|
|
180
260
|
```ruby
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
stub_subflag(:max_projects, 100)
|
|
261
|
+
# config/application.rb (required)
|
|
262
|
+
config.middleware.use Subflag::Rails::RequestCache::Middleware
|
|
263
|
+
```
|
|
185
264
|
|
|
186
|
-
|
|
187
|
-
|
|
265
|
+
```ruby
|
|
266
|
+
class ApplicationController < ActionController::Base
|
|
267
|
+
before_action :prefetch_feature_flags
|
|
268
|
+
|
|
269
|
+
private
|
|
270
|
+
|
|
271
|
+
def prefetch_feature_flags
|
|
272
|
+
subflag_prefetch # Fetches all flags for current_user in one API call
|
|
273
|
+
end
|
|
188
274
|
end
|
|
275
|
+
```
|
|
189
276
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
277
|
+
Now all subsequent flag lookups use the cache — no additional API calls:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# In your controller/view - all lookups are instant (cache hits)
|
|
281
|
+
subflag_enabled?(:new_checkout) # Cache hit
|
|
282
|
+
subflag_value(:max_projects, default: 3) # Cache hit
|
|
283
|
+
subflag_value(:headline, default: "Hi") # Cache hit
|
|
196
284
|
```
|
|
197
285
|
|
|
198
|
-
|
|
286
|
+
### How It Works
|
|
199
287
|
|
|
200
|
-
|
|
288
|
+
1. **Single API call**: `subflag_prefetch` calls `/sdk/evaluate-all` to fetch all flags
|
|
289
|
+
2. **Per-request cache**: Results are stored in `RequestCache` for the duration of the request
|
|
290
|
+
3. **Zero-latency lookups**: Subsequent `subflag_enabled?` and `subflag_value` calls read from cache
|
|
291
|
+
|
|
292
|
+
### Prefetch Without current_user
|
|
201
293
|
|
|
202
294
|
```ruby
|
|
203
|
-
#
|
|
204
|
-
|
|
295
|
+
# No user context
|
|
296
|
+
subflag_prefetch(nil)
|
|
297
|
+
|
|
298
|
+
# With specific user
|
|
299
|
+
subflag_prefetch(admin_user)
|
|
300
|
+
|
|
301
|
+
# With additional context
|
|
302
|
+
subflag_prefetch(current_user, context: { device: "mobile" })
|
|
205
303
|
```
|
|
206
304
|
|
|
207
|
-
|
|
305
|
+
### Direct API
|
|
306
|
+
|
|
307
|
+
You can also use the module method directly:
|
|
208
308
|
|
|
209
309
|
```ruby
|
|
210
|
-
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
subflag_enabled?(:new_checkout) # Cache hit
|
|
214
|
-
subflag_enabled?(:new_checkout) # Cache hit
|
|
310
|
+
Subflag.prefetch_flags(user: current_user)
|
|
311
|
+
# or
|
|
312
|
+
Subflag::Rails.prefetch_flags(user: current_user)
|
|
215
313
|
```
|
|
216
314
|
|
|
217
315
|
## Configuration
|
|
218
316
|
|
|
219
317
|
```ruby
|
|
220
318
|
Subflag::Rails.configure do |config|
|
|
221
|
-
#
|
|
319
|
+
# Backend: :subflag (cloud), :active_record (self-hosted), :memory (testing)
|
|
320
|
+
config.backend = :subflag
|
|
321
|
+
|
|
322
|
+
# API key - required for :subflag backend
|
|
222
323
|
config.api_key = "sdk-production-..."
|
|
223
324
|
|
|
224
325
|
# API URL (default: https://api.subflag.com)
|
|
225
326
|
config.api_url = "https://api.subflag.com"
|
|
226
327
|
|
|
328
|
+
# Cross-request caching via Rails.cache (optional, :subflag backend only)
|
|
329
|
+
# When set, prefetched flags are cached for this duration
|
|
330
|
+
config.cache_ttl = 30.seconds
|
|
331
|
+
|
|
227
332
|
# Logging
|
|
228
333
|
config.logging_enabled = Rails.env.development?
|
|
229
334
|
config.log_level = :debug # :debug, :info, :warn
|
|
230
335
|
|
|
231
|
-
# User context
|
|
336
|
+
# User context - works with all backends
|
|
232
337
|
config.user_context do |user|
|
|
233
338
|
{ targeting_key: user.id.to_s, plan: user.plan }
|
|
234
339
|
end
|
|
235
340
|
end
|
|
236
341
|
```
|
|
237
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
|
+
|
|
238
402
|
## Documentation
|
|
239
403
|
|
|
240
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
|