subflag-rails 0.3.0 → 0.5.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 +287 -60
- data/lib/generators/subflag/install_generator.rb +105 -33
- data/lib/generators/subflag/templates/create_subflag_flags.rb.tt +22 -0
- data/lib/generators/subflag/templates/{initializer.rb → initializer.rb.tt} +16 -2
- data/lib/subflag/rails/backends/active_record_provider.rb +113 -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 +45 -0
- data/lib/subflag/rails/engine.rb +34 -0
- data/lib/subflag/rails/models/flag.rb +154 -0
- data/lib/subflag/rails/targeting.rb +48 -0
- data/lib/subflag/rails/targeting_engine.rb +191 -0
- data/lib/subflag/rails/version.rb +1 -1
- data/lib/subflag/rails.rb +60 -7
- metadata +17 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd3dfb04635e013ed8a9da2282150acf511c8704aace3c439a06f7e5dd0ac24a
|
|
4
|
+
data.tar.gz: adc804044699b6cea75d8b44fc742d08f2a96b24092a8e66d19b419f41c45fa1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb94613ee232272a8799ff0f7623c02d2d6e398ed0b7fbb231ff48bdf1e160ff99cb2110622567374012de4418e9b40129f7b14b0fabe7624904c399696f6dfc
|
|
7
|
+
data.tar.gz: 6327d0185659e82b95a922380cfbae030a42f91676a5a00d1a5fb43120c46eedd8d88432c0ec5cc7d20724494abd57be11ac6c47e2da26b536daa159174bf9ac
|
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, [built-in admin UI](#admin-ui-activerecord) | 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,217 @@ 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 | Default value (what everyone gets) |
|
|
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
|
+
| `targeting_rules` | json | Optional rules for showing different values to different users |
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# Create flags
|
|
358
|
+
Subflag::Rails::Flag.create!(key: "max-projects", value: "100", value_type: "integer")
|
|
359
|
+
|
|
360
|
+
# Query flags
|
|
361
|
+
Subflag::Rails::Flag.enabled.find_each { |f| puts "#{f.key}: #{f.typed_value}" }
|
|
362
|
+
|
|
363
|
+
# Disable a flag
|
|
364
|
+
Subflag::Rails::Flag.find_by(key: "new-checkout")&.update!(enabled: false)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Targeting Rules (ActiveRecord)
|
|
368
|
+
|
|
369
|
+
Show different flag values to different users based on their attributes. Perfect for internal testing before wider rollout.
|
|
370
|
+
|
|
371
|
+
> **Tip:** Use the [Admin UI](#admin-ui-activerecord) to manage targeting rules visually instead of editing JSON.
|
|
372
|
+
|
|
373
|
+
**First, configure user context:**
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
# config/initializers/subflag.rb
|
|
377
|
+
Subflag::Rails.configure do |config|
|
|
378
|
+
config.backend = :active_record
|
|
379
|
+
|
|
380
|
+
config.user_context do |user|
|
|
381
|
+
{
|
|
382
|
+
targeting_key: user.id.to_s,
|
|
383
|
+
email: user.email,
|
|
384
|
+
role: user.role, # e.g., "admin", "developer", "qa"
|
|
385
|
+
plan: user.plan # e.g., "free", "pro", "enterprise"
|
|
386
|
+
}
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Create flags with targeting rules:**
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
# Internal team sees new feature, everyone else sees old
|
|
395
|
+
Subflag::Rails::Flag.create!(
|
|
396
|
+
key: "new-dashboard",
|
|
397
|
+
value: "false", # Default: everyone gets false
|
|
398
|
+
value_type: "boolean",
|
|
399
|
+
targeting_rules: [
|
|
400
|
+
{
|
|
401
|
+
"value" => "true", # Internal team gets true
|
|
402
|
+
"conditions" => {
|
|
403
|
+
"type" => "OR",
|
|
404
|
+
"conditions" => [
|
|
405
|
+
{ "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@yourcompany.com" },
|
|
406
|
+
{ "attribute" => "role", "operator" => "IN", "value" => ["admin", "developer", "qa"] }
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
]
|
|
411
|
+
)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Progressive rollout with multiple rules (first match wins):**
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
Subflag::Rails::Flag.create!(
|
|
418
|
+
key: "max-projects",
|
|
419
|
+
value: "5", # Default: everyone gets 5
|
|
420
|
+
value_type: "integer",
|
|
421
|
+
targeting_rules: [
|
|
422
|
+
{ "value" => "1000", "conditions" => { "type" => "AND", "conditions" => [{ "attribute" => "role", "operator" => "EQUALS", "value" => "admin" }] } },
|
|
423
|
+
{ "value" => "100", "conditions" => { "type" => "AND", "conditions" => [{ "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@yourcompany.com" }] } },
|
|
424
|
+
{ "value" => "25", "conditions" => { "type" => "AND", "conditions" => [{ "attribute" => "plan", "operator" => "EQUALS", "value" => "pro" }] } }
|
|
425
|
+
]
|
|
426
|
+
)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Supported operators:**
|
|
430
|
+
|
|
431
|
+
| Operator | Example | Description |
|
|
432
|
+
|----------|---------|-------------|
|
|
433
|
+
| `EQUALS` | `{ "attribute" => "role", "operator" => "EQUALS", "value" => "admin" }` | Exact match |
|
|
434
|
+
| `NOT_EQUALS` | `{ "attribute" => "env", "operator" => "NOT_EQUALS", "value" => "prod" }` | Not equal |
|
|
435
|
+
| `IN` | `{ "attribute" => "role", "operator" => "IN", "value" => ["admin", "qa"] }` | Value in list |
|
|
436
|
+
| `NOT_IN` | `{ "attribute" => "country", "operator" => "NOT_IN", "value" => ["RU", "CN"] }` | Value not in list |
|
|
437
|
+
| `CONTAINS` | `{ "attribute" => "email", "operator" => "CONTAINS", "value" => "test" }` | String contains |
|
|
438
|
+
| `NOT_CONTAINS` | `{ "attribute" => "email", "operator" => "NOT_CONTAINS", "value" => "spam" }` | String doesn't contain |
|
|
439
|
+
| `STARTS_WITH` | `{ "attribute" => "user_id", "operator" => "STARTS_WITH", "value" => "test-" }` | String prefix |
|
|
440
|
+
| `ENDS_WITH` | `{ "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@company.com" }` | String suffix |
|
|
441
|
+
| `GREATER_THAN` | `{ "attribute" => "age", "operator" => "GREATER_THAN", "value" => 18 }` | Numeric greater than |
|
|
442
|
+
| `LESS_THAN` | `{ "attribute" => "items", "operator" => "LESS_THAN", "value" => 100 }` | Numeric less than |
|
|
443
|
+
| `GREATER_THAN_OR_EQUAL` | `{ "attribute" => "score", "operator" => "GREATER_THAN_OR_EQUAL", "value" => 80 }` | Numeric greater or equal |
|
|
444
|
+
| `LESS_THAN_OR_EQUAL` | `{ "attribute" => "score", "operator" => "LESS_THAN_OR_EQUAL", "value" => 50 }` | Numeric less or equal |
|
|
445
|
+
| `MATCHES` | `{ "attribute" => "email", "operator" => "MATCHES", "value" => ".*@company\\.com$" }` | Regex match |
|
|
446
|
+
|
|
447
|
+
**Combining conditions:**
|
|
448
|
+
|
|
449
|
+
```ruby
|
|
450
|
+
# OR: any condition matches
|
|
451
|
+
{
|
|
452
|
+
"type" => "OR",
|
|
453
|
+
"conditions" => [
|
|
454
|
+
{ "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@company.com" },
|
|
455
|
+
{ "attribute" => "role", "operator" => "EQUALS", "value" => "admin" }
|
|
456
|
+
]
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# AND: all conditions must match
|
|
460
|
+
{
|
|
461
|
+
"type" => "AND",
|
|
462
|
+
"conditions" => [
|
|
463
|
+
{ "attribute" => "plan", "operator" => "EQUALS", "value" => "enterprise" },
|
|
464
|
+
{ "attribute" => "country", "operator" => "IN", "value" => ["US", "CA"] }
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**How evaluation works:**
|
|
470
|
+
|
|
471
|
+
1. Flag disabled? → return code default
|
|
472
|
+
2. For each rule (in order): if context matches → return rule's value
|
|
473
|
+
3. No rules matched? → return flag's default `value`
|
|
474
|
+
|
|
475
|
+
This lets you progressively widen access by adding rules, without changing existing ones.
|
|
476
|
+
|
|
477
|
+
### Admin UI (ActiveRecord)
|
|
478
|
+
|
|
479
|
+
Mount the admin UI to manage flags and targeting rules visually:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
# config/routes.rb
|
|
483
|
+
Rails.application.routes.draw do
|
|
484
|
+
mount Subflag::Rails::Engine => "/subflag"
|
|
485
|
+
# ... your other routes
|
|
486
|
+
end
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
The admin UI provides:
|
|
490
|
+
- List, create, edit, and delete flags
|
|
491
|
+
- Toggle flags enabled/disabled
|
|
492
|
+
- Visual targeting rule builder (no JSON editing)
|
|
493
|
+
- Test rules against sample contexts
|
|
494
|
+
|
|
495
|
+
**Secure the admin UI:**
|
|
496
|
+
|
|
497
|
+
```ruby
|
|
498
|
+
# config/initializers/subflag.rb
|
|
499
|
+
Subflag::Rails.configure do |config|
|
|
500
|
+
config.backend = :active_record
|
|
501
|
+
|
|
502
|
+
# Require authentication for admin UI
|
|
503
|
+
config.admin_auth do
|
|
504
|
+
redirect_to main_app.root_path unless current_user&.admin?
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Visit `/subflag` in your browser to access the admin UI.
|
|
510
|
+
|
|
511
|
+
## Testing
|
|
512
|
+
|
|
513
|
+
Stub flags in your tests:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
# spec/rails_helper.rb (RSpec)
|
|
517
|
+
require "subflag/rails/test_helpers"
|
|
518
|
+
RSpec.configure do |config|
|
|
519
|
+
config.include Subflag::Rails::TestHelpers
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# test/test_helper.rb (Minitest)
|
|
523
|
+
require "subflag/rails/test_helpers"
|
|
524
|
+
class ActiveSupport::TestCase
|
|
525
|
+
include Subflag::Rails::TestHelpers
|
|
526
|
+
end
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
# In your specs/tests
|
|
531
|
+
it "shows new checkout when enabled" do
|
|
532
|
+
stub_subflag(:new_checkout, true)
|
|
533
|
+
stub_subflag(:max_projects, 100)
|
|
534
|
+
|
|
535
|
+
visit checkout_path
|
|
536
|
+
expect(page).to have_content("New Checkout")
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Stub multiple at once
|
|
540
|
+
stub_subflags(
|
|
541
|
+
new_checkout: true,
|
|
542
|
+
max_projects: 100,
|
|
543
|
+
headline: "Welcome!"
|
|
544
|
+
)
|
|
545
|
+
```
|
|
546
|
+
|
|
320
547
|
## Documentation
|
|
321
548
|
|
|
322
549
|
- [Subflag Docs](https://docs.subflag.com)
|
|
@@ -7,49 +7,121 @@ 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
|
|
32
|
+
|
|
33
|
+
# Helper for templates to detect PostgreSQL adapter
|
|
34
|
+
def postgresql?
|
|
35
|
+
return false unless defined?(::ActiveRecord::Base)
|
|
36
|
+
|
|
37
|
+
adapter = ::ActiveRecord::Base.connection_db_config.adapter.to_s rescue nil
|
|
38
|
+
adapter&.include?("postgresql") || adapter&.include?("postgis")
|
|
39
|
+
end
|
|
16
40
|
|
|
17
41
|
def create_initializer
|
|
18
|
-
template "initializer.rb", "config/initializers/subflag.rb"
|
|
42
|
+
template "initializer.rb.tt", "config/initializers/subflag.rb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_flags_migration
|
|
46
|
+
return unless options[:backend] == "active_record"
|
|
47
|
+
|
|
48
|
+
migration_template "create_subflag_flags.rb.tt",
|
|
49
|
+
"db/migrate/create_subflag_flags.rb"
|
|
19
50
|
end
|
|
20
51
|
|
|
21
52
|
def show_instructions
|
|
22
53
|
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
|
-
|
|
54
|
+
|
|
55
|
+
case options[:backend]
|
|
56
|
+
when "active_record"
|
|
57
|
+
say "Subflag installed with ActiveRecord backend!", :green
|
|
58
|
+
say ""
|
|
59
|
+
say "Next steps:"
|
|
60
|
+
say ""
|
|
61
|
+
say "1. Run the migration:"
|
|
62
|
+
say " $ rails db:migrate"
|
|
63
|
+
say ""
|
|
64
|
+
say "2. Create your first flag:"
|
|
65
|
+
say ""
|
|
66
|
+
say " Subflag::Rails::Flag.create!("
|
|
67
|
+
say " key: 'new-checkout',"
|
|
68
|
+
say " value: 'true',"
|
|
69
|
+
say " value_type: 'boolean'"
|
|
70
|
+
say " )"
|
|
71
|
+
say ""
|
|
72
|
+
say "3. Use flags in your code:"
|
|
73
|
+
say ""
|
|
74
|
+
say " if subflag_enabled?(:new_checkout)"
|
|
75
|
+
say " # ..."
|
|
76
|
+
say " end"
|
|
77
|
+
say ""
|
|
78
|
+
say "When you're ready for a dashboard, environments, and user targeting:"
|
|
79
|
+
say " https://subflag.com", :yellow
|
|
80
|
+
say ""
|
|
81
|
+
|
|
82
|
+
when "memory"
|
|
83
|
+
say "Subflag installed with Memory backend!", :green
|
|
84
|
+
say ""
|
|
85
|
+
say "Note: Memory backend is for testing only. Flags reset on restart."
|
|
86
|
+
say ""
|
|
87
|
+
say "Set flags in your tests or initializer:"
|
|
88
|
+
say ""
|
|
89
|
+
say " Subflag::Rails.provider.set(:new_checkout, true)"
|
|
90
|
+
say " Subflag::Rails.provider.set(:max_projects, 100)"
|
|
91
|
+
say ""
|
|
92
|
+
say "Use flags:"
|
|
93
|
+
say ""
|
|
94
|
+
say " subflag_enabled?(:new_checkout) # => true"
|
|
95
|
+
say " subflag_value(:max_projects, default: 3) # => 100"
|
|
96
|
+
say ""
|
|
97
|
+
|
|
98
|
+
else # subflag (cloud)
|
|
99
|
+
say "Subflag installed!", :green
|
|
100
|
+
say ""
|
|
101
|
+
say "Next steps:"
|
|
102
|
+
say ""
|
|
103
|
+
say "1. Add your API key to Rails credentials:"
|
|
104
|
+
say " $ rails credentials:edit"
|
|
105
|
+
say ""
|
|
106
|
+
say " subflag:"
|
|
107
|
+
say " api_key: sdk-production-your-key-here"
|
|
108
|
+
say ""
|
|
109
|
+
say " Or set SUBFLAG_API_KEY environment variable."
|
|
110
|
+
say ""
|
|
111
|
+
say "2. Configure user context in config/initializers/subflag.rb"
|
|
112
|
+
say ""
|
|
113
|
+
say "3. Use flags in your code:"
|
|
114
|
+
say ""
|
|
115
|
+
say " # Controller (auto-scoped to current_user)"
|
|
116
|
+
say " if subflag_enabled?(:new_checkout)"
|
|
117
|
+
say " # ..."
|
|
118
|
+
say " end"
|
|
119
|
+
say ""
|
|
120
|
+
say " max = subflag_value(:max_projects, default: 3)"
|
|
121
|
+
say ""
|
|
122
|
+
say "Docs: https://docs.subflag.com/rails"
|
|
123
|
+
say ""
|
|
124
|
+
end
|
|
53
125
|
end
|
|
54
126
|
end
|
|
55
127
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
# Targeting rules for showing different values to different users
|
|
13
|
+
# Stores an array of { value, conditions } rules as JSON
|
|
14
|
+
# First matching rule wins; falls back to `value` if no match
|
|
15
|
+
t.<%= postgresql? ? 'jsonb' : 'json' %> :targeting_rules
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :subflag_flags, :key, unique: true
|
|
21
|
+
end
|
|
22
|
+
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)
|