flipper 1.3.5 → 1.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -5
  3. data/.github/workflows/examples.yml +4 -3
  4. data/.github/workflows/release.yml +54 -0
  5. data/CLAUDE.md +87 -0
  6. data/README.md +4 -3
  7. data/examples/cloud/poll_interval/README.md +111 -0
  8. data/examples/cloud/poll_interval/client.rb +108 -0
  9. data/examples/cloud/poll_interval/server.rb +98 -0
  10. data/examples/expressions.rb +35 -11
  11. data/lib/flipper/adapter.rb +17 -1
  12. data/lib/flipper/adapters/actor_limit.rb +27 -1
  13. data/lib/flipper/adapters/cache_base.rb +21 -3
  14. data/lib/flipper/adapters/dual_write.rb +6 -2
  15. data/lib/flipper/adapters/failover.rb +9 -3
  16. data/lib/flipper/adapters/failsafe.rb +2 -2
  17. data/lib/flipper/adapters/http/client.rb +15 -4
  18. data/lib/flipper/adapters/http.rb +37 -2
  19. data/lib/flipper/adapters/instrumented.rb +2 -2
  20. data/lib/flipper/adapters/memoizable.rb +3 -3
  21. data/lib/flipper/adapters/memory.rb +1 -1
  22. data/lib/flipper/adapters/pstore.rb +1 -1
  23. data/lib/flipper/adapters/sync/feature_synchronizer.rb +5 -1
  24. data/lib/flipper/adapters/sync/synchronizer.rb +10 -5
  25. data/lib/flipper/adapters/sync.rb +7 -3
  26. data/lib/flipper/cli.rb +51 -0
  27. data/lib/flipper/cloud/configuration.rb +9 -4
  28. data/lib/flipper/cloud/dsl.rb +2 -2
  29. data/lib/flipper/cloud/middleware.rb +1 -1
  30. data/lib/flipper/cloud/migrate.rb +71 -0
  31. data/lib/flipper/cloud/telemetry.rb +1 -1
  32. data/lib/flipper/cloud.rb +1 -0
  33. data/lib/flipper/dsl.rb +1 -1
  34. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  35. data/lib/flipper/expressions/time.rb +8 -1
  36. data/lib/flipper/gates/expression.rb +2 -2
  37. data/lib/flipper/poller.rb +47 -8
  38. data/lib/flipper/version.rb +1 -1
  39. data/lib/flipper.rb +17 -1
  40. data/spec/flipper/adapter_spec.rb +20 -0
  41. data/spec/flipper/adapters/actor_limit_spec.rb +55 -0
  42. data/spec/flipper/adapters/dual_write_spec.rb +13 -0
  43. data/spec/flipper/adapters/failover_spec.rb +12 -0
  44. data/spec/flipper/adapters/http_spec.rb +151 -0
  45. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +12 -0
  46. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
  47. data/spec/flipper/adapters/sync_spec.rb +13 -0
  48. data/spec/flipper/cli_spec.rb +51 -0
  49. data/spec/flipper/cloud/configuration_spec.rb +6 -0
  50. data/spec/flipper/cloud/dsl_spec.rb +10 -2
  51. data/spec/flipper/cloud/middleware_spec.rb +34 -16
  52. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  53. data/spec/flipper/cloud/telemetry_spec.rb +1 -1
  54. data/spec/flipper/engine_spec.rb +2 -2
  55. data/spec/flipper/expressions/time_spec.rb +16 -0
  56. data/spec/flipper/gates/expression_spec.rb +82 -0
  57. data/spec/flipper/middleware/memoizer_spec.rb +37 -6
  58. data/spec/flipper/poller_spec.rb +347 -4
  59. data/spec/flipper_integration_spec.rb +133 -0
  60. data/spec/flipper_spec.rb +6 -1
  61. metadata +17 -112
  62. data/lib/flipper/expressions/duration.rb +0 -28
  63. data/spec/flipper/expressions/duration_spec.rb +0 -43
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0f7fc2b532a31e1f4b4cf56421b7e296d12ad92eb12d817571d480d211004c6
4
- data.tar.gz: 37fccbcff9644cda8c874f745b32ce5dbcdd650494f9921807985e8a7da83cdf
3
+ metadata.gz: 881ca599099f433a4204f2a058c512b6b9198f529d1db4fd3a3445ba822dcd9b
4
+ data.tar.gz: 5ad5da793cec928e09357f2d46ddb139c159dfc1d5b9ddea4ae4c0c52193a75b
5
5
  SHA512:
6
- metadata.gz: b452ac3f04236bc0adcf1f96b086cc7c29780d694fceddae975687dfbbaf796b41eaa867d2c5748cf61b4071b9d4f2b189f2921fe8a37b85222499fb9207aea2
7
- data.tar.gz: 2eb9788694d89d2777386032f173f3a616438d4f57f29b1b84a2321cd44e4aa4fb342c89c8f891e353710f5a41329f5473bbbe824385bd5a419d8463961c33a8
6
+ metadata.gz: 9b049bcd83af70810efd36f1873fb9d72cf22f95cf460d80829b7ad7ed68b7ef11e619b621ea001c230dfdbee43c453e76326de59be2306ee52b23444b50619b
7
+ data.tar.gz: fb82eca0e34233cf3fe9e001923f3b87ae2acf9f7dfdbbdce553d1b8408d5f23df075d789f5be2f975d6facf077637294f306d774e026005e4a7a13124866a3a
@@ -1,9 +1,11 @@
1
1
  name: CI
2
- on: [push, pull_request]
2
+ on: [push]
3
+
3
4
  jobs:
4
5
  test:
5
6
  name: Test on ruby ${{ matrix.ruby }} and rails ${{ matrix.rails }}
6
7
  runs-on: ubuntu-latest
8
+ timeout-minutes: 20
7
9
  services:
8
10
  redis:
9
11
  image: redis
@@ -13,6 +15,9 @@ jobs:
13
15
  --health-interval 10s
14
16
  --health-timeout 5s
15
17
  --health-retries 5
18
+ memcached:
19
+ image: memcached
20
+ ports: ["11211:11211"]
16
21
  postgres:
17
22
  image: postgres:13
18
23
  ports:
@@ -81,14 +86,12 @@ jobs:
81
86
  steps:
82
87
  - name: Set up MySQL
83
88
  run: sudo /etc/init.d/mysql start
84
- - name: Setup memcached
85
- uses: KeisukeYamashita/memcached-actions@v1
86
89
  - name: Start MongoDB
87
- uses: supercharge/mongodb-github-action@1.12.0
90
+ uses: supercharge/mongodb-github-action@1.12.1
88
91
  with:
89
92
  mongodb-version: 4.0
90
93
  - name: Check out repository code
91
- uses: actions/checkout@v4
94
+ uses: actions/checkout@v6
92
95
  - name: Do some action caching
93
96
  uses: actions/cache@v4
94
97
  with:
@@ -1,10 +1,11 @@
1
1
  name: Examples
2
- on: [push, pull_request]
2
+ on: [push]
3
3
  jobs:
4
4
  test:
5
5
  if: github.repository_owner == 'flippercloud'
6
6
  name: Example on ruby ${{ matrix.ruby }} and rails ${{ matrix.rails }}
7
7
  runs-on: ubuntu-latest
8
+ timeout-minutes: 20
8
9
  services:
9
10
  redis:
10
11
  image: redis
@@ -66,11 +67,11 @@ jobs:
66
67
  - name: Setup memcached
67
68
  uses: KeisukeYamashita/memcached-actions@v1
68
69
  - name: Start MongoDB
69
- uses: supercharge/mongodb-github-action@1.12.0
70
+ uses: supercharge/mongodb-github-action@1.12.1
70
71
  with:
71
72
  mongodb-version: 4.0
72
73
  - name: Check out repository code
73
- uses: actions/checkout@v4
74
+ uses: actions/checkout@v6
74
75
  - name: Do some action caching
75
76
  uses: actions/cache@v4
76
77
  with:
@@ -0,0 +1,54 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write # RubyGems trusted publishing (OIDC)
13
+ contents: write # Create GitHub Releases
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: '3.3'
20
+
21
+ - name: Build all gems
22
+ run: |
23
+ for gemspec in *.gemspec; do
24
+ gem build "$gemspec"
25
+ done
26
+ mkdir -p pkg
27
+ mv *.gem pkg/
28
+ echo "Built gems:"
29
+ ls -la pkg/
30
+
31
+ - name: Configure RubyGems credentials
32
+ uses: rubygems/configure-rubygems-credentials@v1.0.0
33
+
34
+ - name: Publish all gems
35
+ run: |
36
+ # Push core gem first since other gems depend on it
37
+ echo "Pushing flipper core gem..."
38
+ gem push pkg/flipper-[0-9]*.gem || echo "Core gem already pushed, continuing..."
39
+
40
+ for gem_file in pkg/flipper-*.gem; do
41
+ basename="$(basename "$gem_file")"
42
+ if [[ "$basename" =~ ^flipper-[0-9] ]]; then
43
+ continue
44
+ fi
45
+ echo "Pushing $basename..."
46
+ gem push "$gem_file" || echo "Failed to push $basename, continuing..."
47
+ done
48
+
49
+ - name: Create draft GitHub Release
50
+ uses: softprops/action-gh-release@v2
51
+ with:
52
+ draft: true
53
+ generate_release_notes: true
54
+ files: pkg/*.gem
data/CLAUDE.md ADDED
@@ -0,0 +1,87 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Development Commands
6
+
7
+ ### Testing
8
+ - `bundle exec rake` - Run all tests (RSpec, Minitest, and Rails tests)
9
+ - `bundle exec rspec` - Run RSpec tests only
10
+ - `bundle exec rake spec:ui` - Run UI-specific specs
11
+ - `bundle exec rake test` - Run Minitest tests only
12
+ - `bundle exec rake test_rails` - Run Rails generator tests
13
+ - `script/test` - Bootstrap and run tests across multiple Rails versions (5.0-8.0)
14
+
15
+ ### Development Setup
16
+ - `script/bootstrap` - Bundle install dependencies and setup binstubs
17
+ - `script/console` - Start interactive console with Flipper loaded (uses Pry)
18
+ - `script/server` - Start local UI server on port 9999 for testing web interface
19
+
20
+ ### Releasing
21
+ 1. Bump version in `lib/flipper/version.rb`, commit, and push to main
22
+ 2. Tag and push: `git tag v1.x.x && git push origin v1.x.x`
23
+ 3. GitHub Actions (`.github/workflows/release.yml`) builds and publishes all 12 gems via RubyGems trusted publishing, then creates a draft GitHub Release
24
+ 4. Edit and publish the draft release at https://github.com/flippercloud/flipper/releases
25
+
26
+ - `bundle exec rake build` - Build all gems locally into pkg/ directory
27
+ - `script/release` - Manual fallback for local releases (prompts for OTP)
28
+
29
+ ## Architecture Overview
30
+
31
+ Flipper is a feature flag library for Ruby with a modular adapter-based architecture:
32
+
33
+ ### Core Components
34
+
35
+ **DSL Layer** (`lib/flipper/dsl.rb`):
36
+ - Main interface for feature flag operations
37
+ - Delegates to Feature instances
38
+ - Handles memoization and instrumentation
39
+ - Thread-safe instance management
40
+
41
+ **Feature** (`lib/flipper/feature.rb`):
42
+ - Represents individual feature flags
43
+ - Manages enable/disable operations through gates
44
+ - Handles instrumentation events
45
+ - Works with adapters for persistence
46
+
47
+ **Adapters** (`lib/flipper/adapters/`):
48
+ - Pluggable storage backends (Redis, ActiveRecord, Memory, etc.)
49
+ - Common interface for all storage implementations
50
+ - Support for caching, failover, and synchronization patterns
51
+
52
+ **Gates** (`lib/flipper/gates/`):
53
+ - Different targeting mechanisms:
54
+ - Boolean (on/off for everyone)
55
+ - Actor (specific users/entities)
56
+ - Group (predefined user groups)
57
+ - Percentage of Actors (rollout to X% of users)
58
+ - Percentage of Time (probabilistic enabling)
59
+ - Expression (complex conditional logic)
60
+
61
+ ### Multi-Gem Structure
62
+
63
+ The project is structured as multiple gems:
64
+ - `flipper` - Core library
65
+ - `flipper-ui` - Web interface
66
+ - `flipper-api` - REST API
67
+ - `flipper-cloud` - Cloud service integration
68
+ - `flipper-*` - Various adapter gems (redis, active_record, mongo, etc.)
69
+
70
+ ### Key Patterns
71
+
72
+ **Configuration**: Global configuration through `Flipper.configure` with per-thread instances
73
+ **Instrumentation**: Built-in event system for monitoring and debugging
74
+ **Memoization**: Automatic caching of feature checks within request/thread scope
75
+ **Type Safety**: Strong typing system for actors, percentages, and other values
76
+
77
+ ### Serialization and HTTP
78
+
79
+ Use `Flipper::Typecast` for JSON and gzip serialization instead of calling `JSON.generate`/`JSON.parse` or `Zlib` directly:
80
+ - `Typecast.to_json(hash)` / `Typecast.from_json(string)` for JSON serialization
81
+ - `Typecast.to_gzip(string)` / `Typecast.from_gzip(string)` for gzip compression
82
+
83
+ For outbound HTTP requests, use `Flipper::Adapters::Http::Client` instead of raw `Net::HTTP`. It provides timeouts, retries (`max_retries`), SSL verification, and diagnostic headers (user-agent, client-language, client-platform, etc.). See `lib/flipper/cloud/migrate.rb` for an example.
84
+
85
+ ### Testing
86
+
87
+ Uses both RSpec (currently preferred for new tests) and Minitest. Shared adapter specs ensure consistency across all storage backends. Extensive testing across multiple Rails versions (5.0-8.0).
data/README.md CHANGED
@@ -97,9 +97,10 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
97
97
 
98
98
  ## Releasing
99
99
 
100
- 1. Update the version to be whatever it should be and commit.
101
- 2. `script/release`
102
- 3. Create a new [GitHub Release](https://github.com/flippercloud/flipper/releases/new)
100
+ 1. Update the version in `lib/flipper/version.rb` and commit.
101
+ 2. Tag and push: `git tag v1.x.x && git push origin v1.x.x`
102
+ 3. GitHub Actions builds and publishes all gems to RubyGems automatically.
103
+ 4. Edit and publish the draft [GitHub Release](https://github.com/flippercloud/flipper/releases).
103
104
 
104
105
  ## Brought To You By
105
106
 
@@ -0,0 +1,111 @@
1
+ # Poll Interval Dynamic Adjustment Demo
2
+
3
+ This demo shows how the Flipper poller dynamically adjusts its polling interval based on the `poll-interval` header from the server, and how it responds to the `poll-shutdown` header.
4
+
5
+ ## Files
6
+
7
+ - `server.rb` - Test server that responds with configurable headers
8
+ - `client.rb` - Client that polls the server and logs interval changes
9
+ - `README.md` - This file
10
+
11
+ ## How to Run
12
+
13
+ ### Terminal 1: Start the Server
14
+
15
+ ```bash
16
+ bundle exec ruby examples/cloud/poll_interval/server.rb
17
+ ```
18
+
19
+ The server will start on http://localhost:3000 and show a prompt where you can control what headers to send.
20
+
21
+ ### Terminal 2: Start the Client
22
+
23
+ ```bash
24
+ bundle exec ruby examples/cloud/poll_interval/client.rb
25
+ ```
26
+
27
+ The client will start polling the server every 10 seconds (the minimum) and log all activity.
28
+
29
+ ## Testing Scenarios
30
+
31
+ ### 1. Change Poll Interval
32
+
33
+ In the **server terminal**, type a number to set the poll interval:
34
+
35
+ ```
36
+ > 20
37
+ ```
38
+
39
+ In the **client terminal**, you'll see:
40
+
41
+ ```
42
+ [HH:MM:SS] WARN: ⚠️ INTERVAL CHANGED: 10.0s → 20.0s
43
+ ```
44
+
45
+ The client will now poll every 20 seconds instead of 10.
46
+
47
+ ### 2. Try an Invalid Interval (Below Minimum)
48
+
49
+ In the **server terminal**:
50
+
51
+ ```
52
+ > 5
53
+ ```
54
+
55
+ In the **client terminal**, you'll see a warning:
56
+
57
+ ```
58
+ Flipper::Cloud poll interval must be greater than or equal to 10 but was 5.0. Setting interval to 10.
59
+ ```
60
+
61
+ The interval will remain at 10 seconds (the minimum).
62
+
63
+ ### 3. Trigger Shutdown
64
+
65
+ In the **server terminal**:
66
+
67
+ ```
68
+ > shutdown
69
+ ```
70
+
71
+ In the **client terminal**, you'll see:
72
+
73
+ ```
74
+ [HH:MM:SS] WARN: Shutdown requested by server via poll-shutdown header
75
+ [HH:MM:SS] WARN: Poller stopped
76
+ [HH:MM:SS] WARN: Poller thread is no longer running
77
+ ```
78
+
79
+ The poller will stop gracefully.
80
+
81
+ ### 4. Reset Headers
82
+
83
+ In the **server terminal**:
84
+
85
+ ```
86
+ > reset
87
+ ```
88
+
89
+ The server will stop sending special headers. The client will continue with its current interval.
90
+
91
+ ## What You'll Learn
92
+
93
+ - How `poll-interval` header dynamically adjusts polling frequency
94
+ - How `poll-shutdown` header gracefully stops the poller
95
+ - How minimum interval enforcement works (10 seconds minimum)
96
+ - How the poller continues working even if the server returns errors
97
+ - Real-time logging of poller events via instrumentation
98
+
99
+ ## Implementation Details
100
+
101
+ The poller checks response headers in the `ensure` block of the `sync` method, which means:
102
+
103
+ - Interval adjustments happen even if the sync fails with an error
104
+ - Shutdown signals are never missed, even during failures
105
+ - The poller is resilient to network issues
106
+
107
+ The `interval=` setter handles all validation:
108
+
109
+ - Type conversion via `Flipper::Typecast.to_float`
110
+ - Minimum enforcement (10 seconds)
111
+ - Warning messages for invalid values
@@ -0,0 +1,108 @@
1
+ # Example showing poll interval being dynamically adjusted via poll-interval header
2
+ #
3
+ # Usage:
4
+ # 1. Terminal 1: bundle exec ruby examples/cloud/poll_interval/server.rb
5
+ # 2. Terminal 2: bundle exec ruby examples/cloud/poll_interval/client.rb
6
+
7
+ require 'bundler/setup'
8
+ require 'flipper'
9
+ require 'flipper/adapters/http'
10
+ require 'flipper/poller'
11
+ require 'logger'
12
+
13
+ # Setup logging to show what's happening
14
+ logger = Logger.new(STDOUT)
15
+ logger.level = Logger::INFO
16
+ logger.formatter = proc do |severity, datetime, progname, msg|
17
+ "[#{datetime.strftime('%H:%M:%S')}] #{severity}: #{msg}\n"
18
+ end
19
+
20
+ # Create HTTP adapter pointing to localhost:3000
21
+ http_adapter = Flipper::Adapters::Http.new(url: 'http://localhost:3000/flipper')
22
+
23
+ # Create instrumenter to log poller events
24
+ instrumenter = Module.new do
25
+ def self.instrument(name, payload = {})
26
+ case payload[:operation]
27
+ when :poll
28
+ logger.info "Polling remote adapter..."
29
+ when :shutdown_requested
30
+ logger.warn "Shutdown requested by server via poll-shutdown header"
31
+ when :stop
32
+ logger.warn "Poller stopped"
33
+ when :thread_start
34
+ logger.info "Poller thread started"
35
+ end
36
+
37
+ result = yield if block_given?
38
+
39
+ if payload[:operation] == :poll && result
40
+ logger.info "Poll completed successfully"
41
+ end
42
+
43
+ result
44
+ end
45
+
46
+ def self.logger=(l)
47
+ @logger = l
48
+ end
49
+
50
+ def self.logger
51
+ @logger
52
+ end
53
+ end
54
+ instrumenter.logger = logger
55
+
56
+ # Create poller with custom instrumenter and short initial interval
57
+ poller = Flipper::Poller.new(
58
+ remote_adapter: http_adapter,
59
+ interval: 5, # Start with 5 second interval (will be enforced to 10 minimum)
60
+ instrumenter: instrumenter,
61
+ start_automatically: false,
62
+ shutdown_automatically: false
63
+ )
64
+
65
+ logger.info "Starting poller with interval: #{poller.interval} seconds"
66
+ logger.info "Minimum allowed interval: #{Flipper::Poller::MINIMUM_POLL_INTERVAL} seconds"
67
+ logger.info ""
68
+ logger.info "Server can control polling via response headers:"
69
+ logger.info " - poll-interval: <seconds> (adjust poll frequency)"
70
+ logger.info " - poll-shutdown: true (stop polling)"
71
+ logger.info ""
72
+
73
+ # Track interval changes
74
+ last_interval = poller.interval
75
+
76
+ # Start the poller
77
+ poller.start
78
+
79
+ # Monitor for interval changes and log them
80
+ logger.info "Monitoring poller... (Ctrl+C to exit)"
81
+ logger.info ""
82
+
83
+ begin
84
+ loop do
85
+ sleep 2
86
+
87
+ current_interval = poller.interval
88
+
89
+ # Highlight when it changes
90
+ if current_interval != last_interval
91
+ logger.warn "⚠️ INTERVAL CHANGED: #{last_interval}s → #{current_interval}s"
92
+ last_interval = current_interval
93
+ end
94
+
95
+ # Check if poller thread is still alive
96
+ unless poller.thread&.alive?
97
+ logger.warn "Poller thread is no longer running"
98
+ break
99
+ end
100
+ end
101
+ rescue Interrupt
102
+ logger.info ""
103
+ logger.info "Interrupted by user"
104
+ ensure
105
+ logger.info "Stopping poller..."
106
+ poller.stop
107
+ logger.info "Final interval: #{poller.interval} seconds"
108
+ end
@@ -0,0 +1,98 @@
1
+ # Simple test server for demonstrating poll interval changes
2
+ #
3
+ # Usage:
4
+ # 1. Terminal 1: bundle exec ruby examples/cloud/poll_interval/server.rb
5
+ # 2. Terminal 2: bundle exec ruby examples/cloud/poll_interval/client.rb
6
+ #
7
+ # Commands in server terminal:
8
+ # - Type a number (e.g., "15") to set poll-interval header to that value
9
+ # - Type "shutdown" to send poll-shutdown: true header
10
+ # - Type "reset" to stop sending special headers
11
+ # - Ctrl+C to exit
12
+
13
+ require 'bundler/setup'
14
+ require 'webrick'
15
+ require 'json'
16
+
17
+ # State for what headers to send
18
+ $poll_interval = nil
19
+ $poll_shutdown = false
20
+
21
+ # Thread to handle user input for changing headers
22
+ input_thread = Thread.new do
23
+ puts ""
24
+ puts "=" * 60
25
+ puts "Server Controls:"
26
+ puts " Type a number (e.g., '15') to set poll-interval"
27
+ puts " Type 'shutdown' to trigger poll shutdown"
28
+ puts " Type 'reset' to clear all special headers"
29
+ puts "=" * 60
30
+ puts ""
31
+
32
+ loop do
33
+ print "> "
34
+ input = gets&.chomp
35
+ break if input.nil?
36
+
37
+ case input
38
+ when /^\d+$/
39
+ $poll_interval = input.to_i
40
+ puts "✓ Will send poll-interval: #{$poll_interval}"
41
+ when "shutdown"
42
+ $poll_shutdown = true
43
+ puts "✓ Will send poll-shutdown: true"
44
+ when "reset"
45
+ $poll_interval = nil
46
+ $poll_shutdown = false
47
+ puts "✓ Cleared all special headers"
48
+ else
49
+ puts "Unknown command. Use a number, 'shutdown', or 'reset'"
50
+ end
51
+ end
52
+ end
53
+
54
+ # Setup WEBrick server
55
+ server = WEBrick::HTTPServer.new(
56
+ Port: 3000,
57
+ Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO),
58
+ AccessLog: [[
59
+ $stdout,
60
+ WEBrick::AccessLog::COMMON_LOG_FORMAT
61
+ ]]
62
+ )
63
+
64
+ # Handle GET /flipper/features
65
+ server.mount_proc '/flipper/features' do |req, res|
66
+ # Build response
67
+ response_body = {
68
+ features: []
69
+ }
70
+
71
+ res.status = 200
72
+ res['Content-Type'] = 'application/json'
73
+ res.body = JSON.generate(response_body)
74
+
75
+ # Add special headers if configured
76
+ if $poll_interval
77
+ res['poll-interval'] = $poll_interval.to_s
78
+ puts "→ Sent poll-interval: #{$poll_interval}"
79
+ end
80
+
81
+ if $poll_shutdown
82
+ res['poll-shutdown'] = 'true'
83
+ puts "→ Sent poll-shutdown: true"
84
+ end
85
+ end
86
+
87
+ # Trap interrupt and shutdown gracefully
88
+ trap('INT') do
89
+ puts "\nShutting down server..."
90
+ server.shutdown
91
+ input_thread.kill
92
+ end
93
+
94
+ puts "Server starting on http://localhost:3000"
95
+ puts "Endpoint: GET http://localhost:3000/flipper/features"
96
+ puts ""
97
+
98
+ server.start
@@ -31,13 +31,9 @@ class Org < Struct.new(:id, :flipper_properties)
31
31
  include Flipper::Identifier
32
32
  end
33
33
 
34
- NOW = Time.now.to_i
35
- DAY = 60 * 60 * 24
36
-
37
34
  org = Org.new(1, {
38
35
  "type" => "Org",
39
36
  "id" => 1,
40
- "now" => NOW,
41
37
  })
42
38
 
43
39
  user = User.new(1, {
@@ -46,7 +42,6 @@ user = User.new(1, {
46
42
  "plan" => "basic",
47
43
  "age" => 39,
48
44
  "team_user" => true,
49
- "now" => NOW,
50
45
  })
51
46
 
52
47
  admin_user = User.new(2, {
@@ -54,7 +49,6 @@ admin_user = User.new(2, {
54
49
  "id" => 2,
55
50
  "admin" => true,
56
51
  "team_user" => true,
57
- "now" => NOW,
58
52
  })
59
53
 
60
54
  other_user = User.new(3, {
@@ -63,7 +57,6 @@ other_user = User.new(3, {
63
57
  "plan" => "plus",
64
58
  "age" => 18,
65
59
  "org_admin" => true,
66
- "now" => NOW - DAY,
67
60
  })
68
61
 
69
62
  age_expression = Flipper.property(:age).gte(21)
@@ -205,9 +198,40 @@ assert Flipper.enabled?(:something, user)
205
198
  assert Flipper.enabled?(:something, admin_user)
206
199
  refute Flipper.enabled?(:something, other_user)
207
200
 
208
- puts "\n\nEnabling based on time"
209
- scheduled_time_expression = Flipper.property(:now).gte(NOW)
210
- Flipper.enable :something, scheduled_time_expression
201
+ puts "\n\nEnabling based on time (epoch)"
202
+ scheduled_epoch = Flipper.now.gte(Flipper.time(Time.now.to_i - 86_400))
203
+ Flipper.enable :something, scheduled_epoch
211
204
  assert Flipper.enabled?(:something, user)
212
205
  assert Flipper.enabled?(:something, admin_user)
213
- refute Flipper.enabled?(:something, other_user)
206
+ assert Flipper.enabled?(:something, other_user)
207
+ reset
208
+
209
+ puts "\n\nEnabling based on time (datetime)"
210
+ past_time = (Time.now.utc - 86_400).iso8601
211
+ scheduled_datetime = Flipper.now.gte(Flipper.time(past_time))
212
+ Flipper.enable :something, scheduled_datetime
213
+ assert Flipper.enabled?(:something, user)
214
+ assert Flipper.enabled?(:something, admin_user)
215
+ assert Flipper.enabled?(:something, other_user)
216
+ reset
217
+
218
+ puts "\n\nDisabling after a time (expiring feature)"
219
+ future_time = (Time.now.utc + 86_400).iso8601
220
+ expiring_expression = Flipper.now.lt(Flipper.time(future_time))
221
+ Flipper.enable :something, expiring_expression
222
+ assert Flipper.enabled?(:something, user)
223
+ assert Flipper.enabled?(:something, admin_user)
224
+ assert Flipper.enabled?(:something, other_user)
225
+ reset
226
+
227
+ puts "\n\nEnabling within a time window"
228
+ start_time = (Time.now.utc - 86_400).iso8601
229
+ end_time = (Time.now.utc + 86_400).iso8601
230
+ time_window = Flipper.all(
231
+ Flipper.now.gte(Flipper.time(start_time)),
232
+ Flipper.now.lt(Flipper.time(end_time))
233
+ )
234
+ Flipper.enable :something, time_window
235
+ assert Flipper.enabled?(:something, user)
236
+ assert Flipper.enabled?(:something, admin_user)
237
+ assert Flipper.enabled?(:something, other_user)
@@ -31,7 +31,7 @@ module Flipper
31
31
  # Public: Get all features and gate values in one call. Defaults to one call
32
32
  # to features and another to get_multi. Feel free to override per adapter to
33
33
  # make this more efficient.
34
- def get_all
34
+ def get_all(**kwargs)
35
35
  instances = features.map { |key| Flipper::Feature.new(key, self) }
36
36
  get_multi(instances)
37
37
  end
@@ -73,6 +73,22 @@ module Flipper
73
73
  def name
74
74
  @name ||= self.class.name.split('::').last.split(/(?=[A-Z])/).join('_').downcase.to_sym
75
75
  end
76
+
77
+ # Public: Returns a string representation of the adapter stack for debugging.
78
+ # Shows the full chain of wrapped adapters.
79
+ #
80
+ # Examples:
81
+ # "memoizable -> active_support_cache_store -> active_record"
82
+ # "memoizable -> failover(primary: redis, secondary: memory)"
83
+ #
84
+ # Returns a String.
85
+ def adapter_stack
86
+ if respond_to?(:adapter) && adapter
87
+ "#{name} -> #{adapter.adapter_stack}"
88
+ else
89
+ name.to_s
90
+ end
91
+ end
76
92
  end
77
93
  end
78
94