toggly-rails 0.1.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 +7 -0
- data/CHANGELOG.md +40 -0
- data/LICENSE +21 -0
- data/README.md +54 -0
- data/lib/toggly/rails/cache_snapshot_provider.rb +66 -0
- data/lib/toggly/rails/configuration.rb +106 -0
- data/lib/toggly/rails/context_builder.rb +70 -0
- data/lib/toggly/rails/controller_concern.rb +112 -0
- data/lib/toggly/rails/generators/toggly/install/install_generator.rb +57 -0
- data/lib/toggly/rails/generators/toggly/install/templates/toggly.rb.erb +60 -0
- data/lib/toggly/rails/middleware.rb +67 -0
- data/lib/toggly/rails/railtie.rb +60 -0
- data/lib/toggly/rails/tasks.rake +78 -0
- data/lib/toggly/rails/testing.rb +146 -0
- data/lib/toggly/rails/version.rb +7 -0
- data/lib/toggly/rails/view_helpers.rb +84 -0
- data/lib/toggly-rails.rb +73 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c6508b1078a5f1691f9ceaf7be6b3c51ff247f347ebbf7b109eacd683216fe7b
|
|
4
|
+
data.tar.gz: c2befaafb3b4605966294e8e9fff0e3499ac245edeb2ad9b5c457cefddcc3a8b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8343572f95ce19a45bdb6127b64cbe494940cba780e98f80fc211dec31ea09750261d6dc60e43ea3ff5284d5014bc2e3f8731218d20b47bf69824d89d92c488d
|
|
7
|
+
data.tar.gz: 61a4937f82a36dd6db037337f7d469e208295d4cfc07e99d739dad0b573a7ef319e80ab79919fe7ad272c91a5b72b5b5447b8cb6f749249fc26ebdf3ec4a6f30
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2024-XX-XX
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of Toggly Ruby SDK
|
|
13
|
+
- `toggly` - Core SDK with zero dependencies
|
|
14
|
+
- Client for feature flag evaluation
|
|
15
|
+
- Context for user identity, groups, and traits
|
|
16
|
+
- Evaluation engine with multiple rule types
|
|
17
|
+
- Percentage rollouts with consistent hashing
|
|
18
|
+
- User and group targeting
|
|
19
|
+
- Contextual targeting with operators
|
|
20
|
+
- Time window rules
|
|
21
|
+
- Memory and file snapshot providers
|
|
22
|
+
- Background refresh support
|
|
23
|
+
- Offline mode with defaults
|
|
24
|
+
|
|
25
|
+
- `toggly-rails` - Rails integration
|
|
26
|
+
- Railtie for auto-configuration
|
|
27
|
+
- Controller concern with `feature_enabled?` helper
|
|
28
|
+
- View helpers (`when_feature_enabled`, `feature_switch`)
|
|
29
|
+
- Rack middleware for request context
|
|
30
|
+
- Context builder from current_user
|
|
31
|
+
- Rails.cache snapshot provider
|
|
32
|
+
- Generator for initializer
|
|
33
|
+
- Rake tasks (list, check, refresh, config)
|
|
34
|
+
- RSpec and Minitest helpers
|
|
35
|
+
|
|
36
|
+
- `toggly-cache` - Redis caching support
|
|
37
|
+
- Redis snapshot provider
|
|
38
|
+
- Connection pool support
|
|
39
|
+
- TTL configuration
|
|
40
|
+
- Touch/extend TTL support
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ops.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# toggly-rails
|
|
2
|
+
|
|
3
|
+
Rails integration for [Toggly](https://toggly.io) feature flag management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem 'toggly-rails'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
rails generate toggly:install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then configure in `config/initializers/toggly.rb`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
Toggly::Rails.configure do |config|
|
|
21
|
+
config.app_key = ENV['TOGGLY_APP_KEY']
|
|
22
|
+
config.environment = Rails.env.production? ? 'Production' : 'Staging'
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Controllers
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class DashboardController < ApplicationController
|
|
32
|
+
def show
|
|
33
|
+
if feature_enabled?(:new_dashboard)
|
|
34
|
+
render :new_dashboard
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Views
|
|
41
|
+
|
|
42
|
+
```erb
|
|
43
|
+
<% if feature_enabled?(:promo) %>
|
|
44
|
+
<div class="promo">Special offer!</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
See the [main README](../README.md) for full documentation.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# Snapshot provider that uses Rails.cache.
|
|
6
|
+
#
|
|
7
|
+
# Stores feature definitions in the Rails cache store
|
|
8
|
+
# for persistence and sharing across processes.
|
|
9
|
+
class CacheSnapshotProvider < Toggly::SnapshotProviders::Base
|
|
10
|
+
# @param key_prefix [String] Cache key prefix
|
|
11
|
+
# @param expires_in [Integer, nil] Cache expiration in seconds (nil = no expiration)
|
|
12
|
+
def initialize(key_prefix: "toggly", expires_in: nil)
|
|
13
|
+
super()
|
|
14
|
+
@key_prefix = key_prefix
|
|
15
|
+
@expires_in = expires_in
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Save definitions to Rails cache
|
|
19
|
+
#
|
|
20
|
+
# @param definitions [Hash<String, FeatureDefinition>] Definitions
|
|
21
|
+
# @param metadata [Hash] Optional metadata
|
|
22
|
+
def save(definitions, metadata = {})
|
|
23
|
+
data = {
|
|
24
|
+
definitions: serialize_definitions(definitions),
|
|
25
|
+
metadata: metadata.merge(saved_at: Time.now.utc.iso8601)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cache_options = {}
|
|
29
|
+
cache_options[:expires_in] = @expires_in if @expires_in
|
|
30
|
+
|
|
31
|
+
::Rails.cache.write(cache_key, data, **cache_options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Load definitions from Rails cache
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash, nil] Hash with :definitions and :metadata
|
|
37
|
+
def load
|
|
38
|
+
data = ::Rails.cache.read(cache_key)
|
|
39
|
+
return nil unless data
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
definitions: deserialize_definitions(data[:definitions]),
|
|
43
|
+
metadata: data[:metadata] || {}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Clear the cached snapshot
|
|
48
|
+
def clear
|
|
49
|
+
::Rails.cache.delete(cache_key)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if snapshot exists in cache
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def exists?
|
|
56
|
+
::Rails.cache.exist?(cache_key)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def cache_key
|
|
62
|
+
"#{@key_prefix}:snapshot"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# Rails-specific configuration for Toggly.
|
|
6
|
+
#
|
|
7
|
+
# Extends the base Toggly configuration with Rails-specific options
|
|
8
|
+
# like context builders and middleware settings.
|
|
9
|
+
class Configuration
|
|
10
|
+
# Core configuration options (delegated to Toggly::Config)
|
|
11
|
+
attr_accessor :app_key, :environment, :base_url, :definitions_url,
|
|
12
|
+
:refresh_interval, :http_timeout,
|
|
13
|
+
:enable_undefined_in_dev, :disable_background_refresh,
|
|
14
|
+
:app_version, :instance_name, :defaults,
|
|
15
|
+
:snapshot_provider, :use_signed_definitions, :allowed_key_ids
|
|
16
|
+
|
|
17
|
+
# Rails-specific options
|
|
18
|
+
|
|
19
|
+
# @return [Proc, nil] Custom context builder proc
|
|
20
|
+
attr_accessor :context_builder
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] Enable request-scoped context (via middleware)
|
|
23
|
+
attr_accessor :request_context_enabled
|
|
24
|
+
|
|
25
|
+
# @return [Symbol] Method to call on current_user for identity
|
|
26
|
+
attr_accessor :identity_method
|
|
27
|
+
|
|
28
|
+
# @return [Symbol] Method to call on current_user for groups
|
|
29
|
+
attr_accessor :groups_method
|
|
30
|
+
|
|
31
|
+
# @return [Hash<Symbol, Proc>] Custom trait extractors
|
|
32
|
+
attr_accessor :trait_extractors
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] Use Rails.cache as snapshot provider
|
|
35
|
+
attr_accessor :use_rails_cache
|
|
36
|
+
|
|
37
|
+
# @return [String] Cache key prefix for Rails.cache
|
|
38
|
+
attr_accessor :cache_key_prefix
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
# Core defaults
|
|
42
|
+
@environment = ::Rails.env.production? ? "Production" : "Staging" if defined?(::Rails)
|
|
43
|
+
@refresh_interval = 300
|
|
44
|
+
@http_timeout = 10
|
|
45
|
+
@enable_undefined_in_dev = false
|
|
46
|
+
@disable_background_refresh = false
|
|
47
|
+
@defaults = {}
|
|
48
|
+
@use_signed_definitions = false
|
|
49
|
+
@allowed_key_ids = []
|
|
50
|
+
|
|
51
|
+
# Rails-specific defaults
|
|
52
|
+
@request_context_enabled = true
|
|
53
|
+
@identity_method = :id
|
|
54
|
+
@groups_method = nil
|
|
55
|
+
@trait_extractors = {}
|
|
56
|
+
@use_rails_cache = false
|
|
57
|
+
@cache_key_prefix = "toggly"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Apply this configuration to a Toggly::Config object
|
|
61
|
+
#
|
|
62
|
+
# @param config [Toggly::Config]
|
|
63
|
+
# @return [void]
|
|
64
|
+
def apply_to(config)
|
|
65
|
+
config.app_key = app_key
|
|
66
|
+
config.environment = environment
|
|
67
|
+
config.base_url = base_url if base_url
|
|
68
|
+
config.definitions_url = definitions_url if definitions_url
|
|
69
|
+
config.refresh_interval = refresh_interval
|
|
70
|
+
config.http_timeout = http_timeout
|
|
71
|
+
config.enable_undefined_in_dev = enable_undefined_in_dev
|
|
72
|
+
config.disable_background_refresh = disable_background_refresh
|
|
73
|
+
config.app_version = app_version if app_version
|
|
74
|
+
config.instance_name = instance_name if instance_name
|
|
75
|
+
config.defaults = defaults
|
|
76
|
+
config.use_signed_definitions = use_signed_definitions
|
|
77
|
+
config.allowed_key_ids = allowed_key_ids
|
|
78
|
+
|
|
79
|
+
# Set up snapshot provider
|
|
80
|
+
config.snapshot_provider = build_snapshot_provider
|
|
81
|
+
|
|
82
|
+
# Use Rails logger
|
|
83
|
+
config.logger = ::Rails.logger if defined?(::Rails)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Add a custom trait extractor
|
|
87
|
+
#
|
|
88
|
+
# @param name [Symbol] Trait name
|
|
89
|
+
# @yield [request, user] Block that returns the trait value
|
|
90
|
+
# @return [void]
|
|
91
|
+
def add_trait(name, &block)
|
|
92
|
+
@trait_extractors[name.to_sym] = block
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def build_snapshot_provider
|
|
98
|
+
return snapshot_provider if snapshot_provider
|
|
99
|
+
return nil unless use_rails_cache && defined?(::Rails)
|
|
100
|
+
|
|
101
|
+
require_relative "cache_snapshot_provider"
|
|
102
|
+
CacheSnapshotProvider.new(key_prefix: cache_key_prefix)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# Builds evaluation context from Rails request and user.
|
|
6
|
+
#
|
|
7
|
+
# Extracts identity, groups, and traits from the current request
|
|
8
|
+
# and authenticated user.
|
|
9
|
+
class ContextBuilder
|
|
10
|
+
# @param config [Configuration] Rails configuration
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@config = config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Build context from request and user
|
|
16
|
+
#
|
|
17
|
+
# @param request [ActionDispatch::Request, nil] Current request
|
|
18
|
+
# @param user [Object, nil] Current authenticated user
|
|
19
|
+
# @return [Toggly::Context]
|
|
20
|
+
def build(request: nil, user: nil)
|
|
21
|
+
identity = extract_identity(user)
|
|
22
|
+
groups = extract_groups(user)
|
|
23
|
+
traits = extract_traits(request, user)
|
|
24
|
+
|
|
25
|
+
Toggly::Context.new(
|
|
26
|
+
identity: identity,
|
|
27
|
+
groups: groups,
|
|
28
|
+
traits: traits
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def extract_identity(user)
|
|
35
|
+
return nil unless user
|
|
36
|
+
return unless @config.identity_method && user.respond_to?(@config.identity_method)
|
|
37
|
+
|
|
38
|
+
user.public_send(@config.identity_method)&.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_groups(user)
|
|
42
|
+
return [] unless user && @config.groups_method
|
|
43
|
+
return [] unless user.respond_to?(@config.groups_method)
|
|
44
|
+
|
|
45
|
+
Array(user.public_send(@config.groups_method)).map(&:to_s)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_traits(request, user)
|
|
49
|
+
traits = {}
|
|
50
|
+
|
|
51
|
+
# Add default request traits
|
|
52
|
+
if request
|
|
53
|
+
traits["request_ip"] = request.remote_ip if request.respond_to?(:remote_ip)
|
|
54
|
+
traits["user_agent"] = request.user_agent if request.respond_to?(:user_agent)
|
|
55
|
+
traits["locale"] = I18n.locale.to_s if defined?(I18n)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Add custom trait extractors
|
|
59
|
+
@config.trait_extractors.each do |name, extractor|
|
|
60
|
+
value = extractor.call(request, user)
|
|
61
|
+
traits[name.to_s] = value unless value.nil?
|
|
62
|
+
rescue StandardError
|
|
63
|
+
# Ignore errors in trait extraction
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
traits
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# Controller concern for feature flag checks.
|
|
6
|
+
#
|
|
7
|
+
# Include this module in your ApplicationController to add
|
|
8
|
+
# feature flag helpers.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# class ApplicationController < ActionController::Base
|
|
12
|
+
# include Toggly::Rails::ControllerConcern
|
|
13
|
+
#
|
|
14
|
+
# # Optional: customize how the current user is identified
|
|
15
|
+
# def toggly_current_user
|
|
16
|
+
# current_user
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
module ControllerConcern
|
|
20
|
+
extend ActiveSupport::Concern
|
|
21
|
+
|
|
22
|
+
included do
|
|
23
|
+
helper_method :feature_enabled?, :feature_disabled? if respond_to?(:helper_method)
|
|
24
|
+
|
|
25
|
+
# Set up user in middleware on each request
|
|
26
|
+
before_action :set_toggly_context, if: -> { Toggly::Rails.configuration&.request_context_enabled }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if a feature is enabled
|
|
30
|
+
#
|
|
31
|
+
# @param feature_key [String, Symbol] Feature key
|
|
32
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def feature_enabled?(feature_key, context: nil)
|
|
35
|
+
ctx = context || toggly_context
|
|
36
|
+
Toggly.enabled?(feature_key, context: ctx)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if a feature is disabled
|
|
40
|
+
#
|
|
41
|
+
# @param feature_key [String, Symbol] Feature key
|
|
42
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def feature_disabled?(feature_key, context: nil)
|
|
45
|
+
!feature_enabled?(feature_key, context: context)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Require a feature to be enabled, otherwise render not found
|
|
49
|
+
#
|
|
50
|
+
# @param feature_key [String, Symbol] Feature key
|
|
51
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
52
|
+
# @return [void]
|
|
53
|
+
def require_feature!(feature_key, context: nil)
|
|
54
|
+
return if feature_enabled?(feature_key, context: context)
|
|
55
|
+
|
|
56
|
+
respond_to do |format|
|
|
57
|
+
format.html { render file: "public/404.html", status: :not_found, layout: false }
|
|
58
|
+
format.json { render json: { error: "Not found" }, status: :not_found }
|
|
59
|
+
format.any { head :not_found }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get the current evaluation context
|
|
64
|
+
#
|
|
65
|
+
# @return [Toggly::Context]
|
|
66
|
+
def toggly_context
|
|
67
|
+
@toggly_context ||= build_toggly_context
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Set a custom context for this request
|
|
71
|
+
#
|
|
72
|
+
# @param context [Toggly::Context] Context to use
|
|
73
|
+
# @return [void]
|
|
74
|
+
def toggly_context=(context)
|
|
75
|
+
@toggly_context = context
|
|
76
|
+
Middleware.set_context(request.env, context) if request
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
protected
|
|
80
|
+
|
|
81
|
+
# Override this method to provide the current user
|
|
82
|
+
#
|
|
83
|
+
# @return [Object, nil]
|
|
84
|
+
def toggly_current_user
|
|
85
|
+
return current_user if respond_to?(:current_user, true)
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def set_toggly_context
|
|
93
|
+
return unless request
|
|
94
|
+
|
|
95
|
+
Middleware.set_user(request.env, toggly_current_user)
|
|
96
|
+
Middleware.set_context(request.env, toggly_context)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_toggly_context
|
|
100
|
+
config = Toggly::Rails.configuration
|
|
101
|
+
return Toggly::Context.anonymous unless config
|
|
102
|
+
|
|
103
|
+
# Use custom context builder if provided
|
|
104
|
+
return config.context_builder.call(request, toggly_current_user) if config.context_builder
|
|
105
|
+
|
|
106
|
+
# Use default context builder
|
|
107
|
+
builder = ContextBuilder.new(config)
|
|
108
|
+
builder.build(request: request, user: toggly_current_user)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Toggly
|
|
6
|
+
module Generators
|
|
7
|
+
# Generator for Toggly Rails initializer.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# rails generate toggly:install
|
|
11
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Creates a Toggly initializer file"
|
|
15
|
+
|
|
16
|
+
class_option :app_key, type: :string, desc: "Your Toggly app key"
|
|
17
|
+
class_option :environment, type: :string, desc: "Environment name"
|
|
18
|
+
|
|
19
|
+
def create_initializer_file
|
|
20
|
+
template "toggly.rb.erb", "config/initializers/toggly.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def show_post_install_message
|
|
24
|
+
say ""
|
|
25
|
+
say "Toggly has been installed! 🎉", :green
|
|
26
|
+
say ""
|
|
27
|
+
say "Next steps:"
|
|
28
|
+
say " 1. Update config/initializers/toggly.rb with your app key"
|
|
29
|
+
say " 2. Or add to your credentials: rails credentials:edit"
|
|
30
|
+
say " toggly:"
|
|
31
|
+
say " app_key: your-app-key"
|
|
32
|
+
say " environment: Production"
|
|
33
|
+
say ""
|
|
34
|
+
say "Usage in controllers:"
|
|
35
|
+
say " if feature_enabled?(:my_feature)"
|
|
36
|
+
say " # feature is on"
|
|
37
|
+
say " end"
|
|
38
|
+
say ""
|
|
39
|
+
say "Usage in views:"
|
|
40
|
+
say " <% if feature_enabled?(:my_feature) %>"
|
|
41
|
+
say " Feature content"
|
|
42
|
+
say " <% end %>"
|
|
43
|
+
say ""
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def app_key
|
|
49
|
+
options[:app_key] || "ENV.fetch('TOGGLY_APP_KEY', nil)"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def environment_name
|
|
53
|
+
options[:environment] || "Rails.env.production? ? 'Production' : 'Staging'"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Toggly Feature Flags Configuration
|
|
4
|
+
# https://docs.toggly.io/sdks/ruby
|
|
5
|
+
|
|
6
|
+
Toggly::Rails.configure do |config|
|
|
7
|
+
# Required: Your Toggly app key
|
|
8
|
+
# Get this from your Toggly dashboard: https://app.toggly.io
|
|
9
|
+
config.app_key = <%= app_key %>
|
|
10
|
+
|
|
11
|
+
# Required: Environment name
|
|
12
|
+
# Common values: "Production", "Staging", "Development"
|
|
13
|
+
config.environment = <%= environment_name %>
|
|
14
|
+
|
|
15
|
+
# Optional: Custom API endpoint (for self-hosted or enterprise)
|
|
16
|
+
# config.base_url = "https://custom.toggly.io"
|
|
17
|
+
|
|
18
|
+
# Optional: How often to refresh definitions (in seconds)
|
|
19
|
+
# config.refresh_interval = 300
|
|
20
|
+
|
|
21
|
+
# Optional: HTTP timeout for API requests (in seconds)
|
|
22
|
+
# config.http_timeout = 10
|
|
23
|
+
|
|
24
|
+
# Optional: Enable features by default in development
|
|
25
|
+
# config.enable_undefined_in_dev = Rails.env.development?
|
|
26
|
+
|
|
27
|
+
# Optional: Disable background refresh (useful in tests)
|
|
28
|
+
# config.disable_background_refresh = Rails.env.test?
|
|
29
|
+
|
|
30
|
+
# Optional: Use Rails.cache for persistence
|
|
31
|
+
# config.use_rails_cache = true
|
|
32
|
+
|
|
33
|
+
# Optional: Custom context builder
|
|
34
|
+
# config.context_builder = ->(request, user) do
|
|
35
|
+
# Toggly::Context.new(
|
|
36
|
+
# identity: user&.id&.to_s,
|
|
37
|
+
# groups: user&.roles&.pluck(:name) || [],
|
|
38
|
+
# traits: {
|
|
39
|
+
# plan: user&.subscription&.plan,
|
|
40
|
+
# country: request&.headers["CF-IPCountry"]
|
|
41
|
+
# }
|
|
42
|
+
# )
|
|
43
|
+
# end
|
|
44
|
+
|
|
45
|
+
# Optional: Configure how user identity is extracted
|
|
46
|
+
# config.identity_method = :id # Method to call on current_user
|
|
47
|
+
|
|
48
|
+
# Optional: Configure how user groups are extracted
|
|
49
|
+
# config.groups_method = :role_names # Method to call on current_user
|
|
50
|
+
|
|
51
|
+
# Optional: Add custom trait extractors
|
|
52
|
+
# config.add_trait(:plan) { |request, user| user&.subscription&.plan }
|
|
53
|
+
# config.add_trait(:country) { |request, _user| request&.headers["CF-IPCountry"] }
|
|
54
|
+
|
|
55
|
+
# Optional: Default values for offline mode
|
|
56
|
+
# config.defaults = {
|
|
57
|
+
# "critical-feature" => true,
|
|
58
|
+
# "experimental-feature" => false
|
|
59
|
+
# }
|
|
60
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# Rack middleware for request-scoped Toggly context.
|
|
6
|
+
#
|
|
7
|
+
# Stores the evaluation context in the request environment
|
|
8
|
+
# for access throughout the request lifecycle.
|
|
9
|
+
class Middleware
|
|
10
|
+
# Environment key for storing the context
|
|
11
|
+
CONTEXT_KEY = "toggly.context"
|
|
12
|
+
USER_KEY = "toggly.current_user"
|
|
13
|
+
|
|
14
|
+
def initialize(app)
|
|
15
|
+
@app = app
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(env)
|
|
19
|
+
# Clear any existing context at the start of the request
|
|
20
|
+
env.delete(CONTEXT_KEY)
|
|
21
|
+
env.delete(USER_KEY)
|
|
22
|
+
|
|
23
|
+
@app.call(env)
|
|
24
|
+
ensure
|
|
25
|
+
# Clean up after request
|
|
26
|
+
env.delete(CONTEXT_KEY)
|
|
27
|
+
env.delete(USER_KEY)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Get the current context from the request environment
|
|
32
|
+
#
|
|
33
|
+
# @param env [Hash] Rack environment
|
|
34
|
+
# @return [Toggly::Context, nil]
|
|
35
|
+
def context(env)
|
|
36
|
+
env[CONTEXT_KEY]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Set the current context in the request environment
|
|
40
|
+
#
|
|
41
|
+
# @param env [Hash] Rack environment
|
|
42
|
+
# @param context [Toggly::Context] Context to set
|
|
43
|
+
# @return [Toggly::Context]
|
|
44
|
+
def set_context(env, context)
|
|
45
|
+
env[CONTEXT_KEY] = context
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set the current user for context building
|
|
49
|
+
#
|
|
50
|
+
# @param env [Hash] Rack environment
|
|
51
|
+
# @param user [Object] Current user
|
|
52
|
+
# @return [Object]
|
|
53
|
+
def set_user(env, user)
|
|
54
|
+
env[USER_KEY] = user
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get the current user from the request environment
|
|
58
|
+
#
|
|
59
|
+
# @param env [Hash] Rack environment
|
|
60
|
+
# @return [Object, nil]
|
|
61
|
+
def user(env)
|
|
62
|
+
env[USER_KEY]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# Rails Railtie for automatic integration.
|
|
6
|
+
#
|
|
7
|
+
# Automatically configures:
|
|
8
|
+
# - Middleware for request context
|
|
9
|
+
# - View helpers
|
|
10
|
+
# - Controller helpers
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
initializer "toggly.middleware" do |app|
|
|
13
|
+
app.middleware.use Toggly::Rails::Middleware
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
initializer "toggly.view_helpers" do
|
|
17
|
+
ActiveSupport.on_load(:action_view) do
|
|
18
|
+
include Toggly::Rails::ViewHelpers
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "toggly.controller_helpers" do
|
|
23
|
+
ActiveSupport.on_load(:action_controller) do
|
|
24
|
+
include Toggly::Rails::ControllerConcern
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Configure Toggly after Rails initializers have run
|
|
29
|
+
config.after_initialize do
|
|
30
|
+
next unless Toggly::Rails.configuration.nil?
|
|
31
|
+
next unless defined?(::Rails) && ::Rails.application
|
|
32
|
+
|
|
33
|
+
credentials = ::Rails.application.credentials
|
|
34
|
+
next unless credentials.respond_to?(:toggly) && credentials.toggly
|
|
35
|
+
|
|
36
|
+
Toggly::Rails.configure do |config|
|
|
37
|
+
config.app_key = credentials.toggly[:app_key]
|
|
38
|
+
config.environment = credentials.toggly[:environment] || (::Rails.env.production? ? "Production" : "Staging")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Gracefully shutdown on Rails restart
|
|
43
|
+
config.after_initialize do
|
|
44
|
+
at_exit do
|
|
45
|
+
Toggly.client&.close
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Add rake tasks
|
|
50
|
+
rake_tasks do
|
|
51
|
+
load "toggly/rails/tasks.rake"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Add generators
|
|
55
|
+
generators do
|
|
56
|
+
require_relative "generators/toggly/install/install_generator"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :toggly do
|
|
4
|
+
desc "Refresh feature definitions from Toggly API"
|
|
5
|
+
task refresh: :environment do
|
|
6
|
+
if Toggly.client
|
|
7
|
+
puts "Refreshing Toggly feature definitions..."
|
|
8
|
+
if Toggly.client.refresh(force: true)
|
|
9
|
+
puts "Successfully refreshed #{Toggly.client.feature_keys.count} features"
|
|
10
|
+
else
|
|
11
|
+
puts "No changes detected"
|
|
12
|
+
end
|
|
13
|
+
else
|
|
14
|
+
puts "Toggly is not configured. Run rails generate toggly:install first."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "List all feature flags"
|
|
19
|
+
task list: :environment do
|
|
20
|
+
if Toggly.client
|
|
21
|
+
features = Toggly.client.features
|
|
22
|
+
if features.empty?
|
|
23
|
+
puts "No features found"
|
|
24
|
+
else
|
|
25
|
+
puts "Feature Flags (#{features.count}):"
|
|
26
|
+
puts "-" * 60
|
|
27
|
+
features.sort_by(&:feature_key).each do |feature|
|
|
28
|
+
status = feature.enabled ? "✓ enabled" : "✗ disabled"
|
|
29
|
+
puts " #{feature.feature_key.ljust(40)} #{status}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
puts "Toggly is not configured. Run rails generate toggly:install first."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Check if a specific feature is enabled"
|
|
38
|
+
task :check, [:feature_key] => :environment do |_t, args|
|
|
39
|
+
feature_key = args[:feature_key]
|
|
40
|
+
if feature_key.nil? || feature_key.empty?
|
|
41
|
+
puts "Usage: rake toggly:check[feature_key]"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if Toggly.client
|
|
46
|
+
if Toggly.enabled?(feature_key)
|
|
47
|
+
puts "Feature '#{feature_key}' is ENABLED"
|
|
48
|
+
else
|
|
49
|
+
puts "Feature '#{feature_key}' is DISABLED"
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
puts "Toggly is not configured. Run rails generate toggly:install first."
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "Show Toggly configuration"
|
|
57
|
+
task config: :environment do
|
|
58
|
+
if Toggly::Rails.configuration
|
|
59
|
+
config = Toggly::Rails.configuration
|
|
60
|
+
puts "Toggly Configuration:"
|
|
61
|
+
puts "-" * 40
|
|
62
|
+
puts " App Key: #{config.app_key ? "#{config.app_key[0..7]}..." : "(not set)"}"
|
|
63
|
+
puts " Environment: #{config.environment}"
|
|
64
|
+
puts " Base URL: #{config.base_url || Toggly::Config::DEFAULT_BASE_URL}"
|
|
65
|
+
puts " Refresh Interval: #{config.refresh_interval}s"
|
|
66
|
+
puts " Request Context: #{config.request_context_enabled ? "enabled" : "disabled"}"
|
|
67
|
+
puts " Rails Cache: #{config.use_rails_cache ? "enabled" : "disabled"}"
|
|
68
|
+
|
|
69
|
+
if Toggly.client
|
|
70
|
+
puts "\nClient Status:"
|
|
71
|
+
puts " Ready: #{Toggly.client.ready ? "yes" : "no"}"
|
|
72
|
+
puts " Features: #{Toggly.client.feature_keys.count}"
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
puts "Toggly is not configured. Run rails generate toggly:install first."
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# Testing helpers for RSpec and Minitest.
|
|
6
|
+
#
|
|
7
|
+
# @example RSpec
|
|
8
|
+
# RSpec.configure do |config|
|
|
9
|
+
# config.include Toggly::Rails::Testing::RSpecHelpers
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# describe "my feature" do
|
|
13
|
+
# it "shows new UI when enabled" do
|
|
14
|
+
# with_feature(:new_ui, enabled: true) do
|
|
15
|
+
# visit dashboard_path
|
|
16
|
+
# expect(page).to have_content("New Dashboard")
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Minitest
|
|
22
|
+
# class MyTest < ActionDispatch::IntegrationTest
|
|
23
|
+
# include Toggly::Rails::Testing::MinitestHelpers
|
|
24
|
+
#
|
|
25
|
+
# test "shows new UI when enabled" do
|
|
26
|
+
# with_feature(:new_ui, enabled: true) do
|
|
27
|
+
# get dashboard_path
|
|
28
|
+
# assert_includes response.body, "New Dashboard"
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
module Testing
|
|
33
|
+
# Stub a feature to return a specific value
|
|
34
|
+
#
|
|
35
|
+
# @param feature_key [String, Symbol] Feature key
|
|
36
|
+
# @param enabled [Boolean] Whether the feature should be enabled
|
|
37
|
+
# @yield Block during which the feature is stubbed
|
|
38
|
+
def with_feature(feature_key, enabled: true)
|
|
39
|
+
key = feature_key.to_s
|
|
40
|
+
original_definitions = Toggly.client&.definitions&.dup || {}
|
|
41
|
+
|
|
42
|
+
# Add or update the feature definition
|
|
43
|
+
Toggly.client.instance_variable_get(:@mutex).synchronize do
|
|
44
|
+
Toggly.client.definitions[key] = Toggly::FeatureDefinition.new(
|
|
45
|
+
feature_key: key,
|
|
46
|
+
enabled: enabled
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
yield
|
|
51
|
+
ensure
|
|
52
|
+
# Restore original definitions
|
|
53
|
+
Toggly.client&.instance_variable_get(:@mutex)&.synchronize do
|
|
54
|
+
Toggly.client.instance_variable_set(:@definitions, original_definitions)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Stub multiple features at once
|
|
59
|
+
#
|
|
60
|
+
# @param features [Hash<String, Boolean>] Hash of feature keys to enabled states
|
|
61
|
+
# @yield Block during which the features are stubbed
|
|
62
|
+
def with_features(features)
|
|
63
|
+
original_definitions = Toggly.client&.definitions&.dup || {}
|
|
64
|
+
|
|
65
|
+
features.each do |key, enabled|
|
|
66
|
+
Toggly.client.instance_variable_get(:@mutex).synchronize do
|
|
67
|
+
Toggly.client.definitions[key.to_s] = Toggly::FeatureDefinition.new(
|
|
68
|
+
feature_key: key.to_s,
|
|
69
|
+
enabled: enabled
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
yield
|
|
75
|
+
ensure
|
|
76
|
+
Toggly.client&.instance_variable_get(:@mutex)&.synchronize do
|
|
77
|
+
Toggly.client.instance_variable_set(:@definitions, original_definitions)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Enable a feature for the duration of a block
|
|
82
|
+
#
|
|
83
|
+
# @param feature_key [String, Symbol] Feature key
|
|
84
|
+
# @yield Block during which the feature is enabled
|
|
85
|
+
def enable_feature(feature_key, &block)
|
|
86
|
+
with_feature(feature_key, enabled: true, &block)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Disable a feature for the duration of a block
|
|
90
|
+
#
|
|
91
|
+
# @param feature_key [String, Symbol] Feature key
|
|
92
|
+
# @yield Block during which the feature is disabled
|
|
93
|
+
def disable_feature(feature_key, &block)
|
|
94
|
+
with_feature(feature_key, enabled: false, &block)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# RSpec helpers
|
|
98
|
+
module RSpecHelpers
|
|
99
|
+
include Testing
|
|
100
|
+
|
|
101
|
+
# RSpec matcher for checking if a feature is enabled
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# expect(:new_feature).to be_feature_enabled
|
|
105
|
+
if defined?(RSpec::Matchers)
|
|
106
|
+
RSpec::Matchers.define :be_feature_enabled do
|
|
107
|
+
match do |feature_key|
|
|
108
|
+
Toggly.enabled?(feature_key)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
failure_message do |feature_key|
|
|
112
|
+
"expected feature '#{feature_key}' to be enabled"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
failure_message_when_negated do |feature_key|
|
|
116
|
+
"expected feature '#{feature_key}' to be disabled"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Minitest helpers
|
|
123
|
+
module MinitestHelpers
|
|
124
|
+
include Testing
|
|
125
|
+
|
|
126
|
+
# Assert that a feature is enabled
|
|
127
|
+
#
|
|
128
|
+
# @param feature_key [String, Symbol] Feature key
|
|
129
|
+
# @param msg [String, nil] Custom failure message
|
|
130
|
+
def assert_feature_enabled(feature_key, msg = nil)
|
|
131
|
+
msg ||= "Expected feature '#{feature_key}' to be enabled"
|
|
132
|
+
assert Toggly.enabled?(feature_key), msg
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Assert that a feature is disabled
|
|
136
|
+
#
|
|
137
|
+
# @param feature_key [String, Symbol] Feature key
|
|
138
|
+
# @param msg [String, nil] Custom failure message
|
|
139
|
+
def assert_feature_disabled(feature_key, msg = nil)
|
|
140
|
+
msg ||= "Expected feature '#{feature_key}' to be disabled"
|
|
141
|
+
assert Toggly.disabled?(feature_key), msg
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
module Rails
|
|
5
|
+
# View helpers for feature flags in Rails views.
|
|
6
|
+
#
|
|
7
|
+
# These helpers are automatically included in ActionView
|
|
8
|
+
# when using the Railtie.
|
|
9
|
+
module ViewHelpers
|
|
10
|
+
# Check if a feature is enabled
|
|
11
|
+
#
|
|
12
|
+
# @param feature_key [String, Symbol] Feature key
|
|
13
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
14
|
+
# @return [Boolean]
|
|
15
|
+
def feature_enabled?(feature_key, context: nil)
|
|
16
|
+
if controller.respond_to?(:feature_enabled?, true)
|
|
17
|
+
controller.send(:feature_enabled?, feature_key, context: context)
|
|
18
|
+
else
|
|
19
|
+
ctx = context || view_toggly_context
|
|
20
|
+
Toggly.enabled?(feature_key, context: ctx)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check if a feature is disabled
|
|
25
|
+
#
|
|
26
|
+
# @param feature_key [String, Symbol] Feature key
|
|
27
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def feature_disabled?(feature_key, context: nil)
|
|
30
|
+
!feature_enabled?(feature_key, context: context)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Render content only if feature is enabled
|
|
34
|
+
#
|
|
35
|
+
# @param feature_key [String, Symbol] Feature key
|
|
36
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
37
|
+
# @yield Content to render when enabled
|
|
38
|
+
# @return [String, nil]
|
|
39
|
+
def when_feature_enabled(feature_key, context: nil, &block)
|
|
40
|
+
return unless feature_enabled?(feature_key, context: context)
|
|
41
|
+
|
|
42
|
+
capture(&block) if block_given?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Render content only if feature is disabled
|
|
46
|
+
#
|
|
47
|
+
# @param feature_key [String, Symbol] Feature key
|
|
48
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
49
|
+
# @yield Content to render when disabled
|
|
50
|
+
# @return [String, nil]
|
|
51
|
+
def when_feature_disabled(feature_key, context: nil, &block)
|
|
52
|
+
return if feature_enabled?(feature_key, context: context)
|
|
53
|
+
|
|
54
|
+
capture(&block) if block_given?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Render enabled or disabled content based on feature state
|
|
58
|
+
#
|
|
59
|
+
# @param feature_key [String, Symbol] Feature key
|
|
60
|
+
# @param enabled [String, nil] Content when enabled
|
|
61
|
+
# @param disabled [String, nil] Content when disabled
|
|
62
|
+
# @param context [Toggly::Context, nil] Optional override context
|
|
63
|
+
# @return [String]
|
|
64
|
+
def feature_switch(feature_key, enabled: nil, disabled: nil, context: nil)
|
|
65
|
+
if feature_enabled?(feature_key, context: context)
|
|
66
|
+
enabled
|
|
67
|
+
else
|
|
68
|
+
disabled
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def view_toggly_context
|
|
75
|
+
# Try to get context from controller
|
|
76
|
+
if controller.respond_to?(:toggly_context, true)
|
|
77
|
+
controller.send(:toggly_context)
|
|
78
|
+
else
|
|
79
|
+
Toggly::Context.anonymous
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/toggly-rails.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toggly"
|
|
4
|
+
require_relative "toggly/rails/version"
|
|
5
|
+
require_relative "toggly/rails/configuration"
|
|
6
|
+
require_relative "toggly/rails/context_builder"
|
|
7
|
+
require_relative "toggly/rails/middleware"
|
|
8
|
+
require_relative "toggly/rails/controller_concern"
|
|
9
|
+
require_relative "toggly/rails/view_helpers"
|
|
10
|
+
require_relative "toggly/rails/railtie" if defined?(Rails::Railtie)
|
|
11
|
+
|
|
12
|
+
module Toggly
|
|
13
|
+
# Rails integration for Toggly feature flags.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic Rails setup (config/initializers/toggly.rb)
|
|
16
|
+
# Toggly::Rails.configure do |config|
|
|
17
|
+
# config.app_key = Rails.application.credentials.toggly_app_key
|
|
18
|
+
# config.environment = Rails.env.production? ? "Production" : "Staging"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Controller usage
|
|
22
|
+
# class ApplicationController < ActionController::Base
|
|
23
|
+
# include Toggly::Rails::ControllerConcern
|
|
24
|
+
#
|
|
25
|
+
# def show
|
|
26
|
+
# if feature_enabled?(:new_dashboard)
|
|
27
|
+
# render :new_dashboard
|
|
28
|
+
# else
|
|
29
|
+
# render :dashboard
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @example View usage
|
|
35
|
+
# <% if feature_enabled?(:new_header) %>
|
|
36
|
+
# <%= render "new_header" %>
|
|
37
|
+
# <% else %>
|
|
38
|
+
# <%= render "header" %>
|
|
39
|
+
# <% end %>
|
|
40
|
+
module Rails
|
|
41
|
+
class << self
|
|
42
|
+
# @return [Configuration] Rails-specific configuration
|
|
43
|
+
attr_accessor :configuration
|
|
44
|
+
|
|
45
|
+
# Configure Toggly for Rails
|
|
46
|
+
#
|
|
47
|
+
# @yield [Configuration] Configuration block
|
|
48
|
+
# @return [void]
|
|
49
|
+
def configure
|
|
50
|
+
self.configuration ||= Configuration.new
|
|
51
|
+
yield(configuration) if block_given?
|
|
52
|
+
|
|
53
|
+
# Create the Toggly client from Rails configuration
|
|
54
|
+
Toggly.configure do |config|
|
|
55
|
+
configuration.apply_to(config)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Reset configuration (mainly for testing)
|
|
60
|
+
def reset!
|
|
61
|
+
self.configuration = nil
|
|
62
|
+
Toggly.reset!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get the current Toggly client
|
|
66
|
+
#
|
|
67
|
+
# @return [Toggly::Client]
|
|
68
|
+
def client
|
|
69
|
+
Toggly.client
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: toggly-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ops.ai
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: toggly
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.1'
|
|
41
|
+
description: Rails integration for Toggly feature flags including Railtie, controller
|
|
42
|
+
concerns, view helpers, Rack middleware, and generators.
|
|
43
|
+
email:
|
|
44
|
+
- support@ops.ai
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- lib/toggly-rails.rb
|
|
53
|
+
- lib/toggly/rails/cache_snapshot_provider.rb
|
|
54
|
+
- lib/toggly/rails/configuration.rb
|
|
55
|
+
- lib/toggly/rails/context_builder.rb
|
|
56
|
+
- lib/toggly/rails/controller_concern.rb
|
|
57
|
+
- lib/toggly/rails/generators/toggly/install/install_generator.rb
|
|
58
|
+
- lib/toggly/rails/generators/toggly/install/templates/toggly.rb.erb
|
|
59
|
+
- lib/toggly/rails/middleware.rb
|
|
60
|
+
- lib/toggly/rails/railtie.rb
|
|
61
|
+
- lib/toggly/rails/tasks.rake
|
|
62
|
+
- lib/toggly/rails/testing.rb
|
|
63
|
+
- lib/toggly/rails/version.rb
|
|
64
|
+
- lib/toggly/rails/view_helpers.rb
|
|
65
|
+
homepage: https://toggly.io
|
|
66
|
+
licenses:
|
|
67
|
+
- MIT
|
|
68
|
+
metadata:
|
|
69
|
+
homepage_uri: https://toggly.io
|
|
70
|
+
source_code_uri: https://github.com/ops-ai/toggly-ruby
|
|
71
|
+
changelog_uri: https://github.com/ops-ai/toggly-ruby/blob/main/CHANGELOG.md
|
|
72
|
+
documentation_uri: https://docs.toggly.io/sdks/ruby
|
|
73
|
+
rubygems_mfa_required: 'true'
|
|
74
|
+
post_install_message:
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.0.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.5.22
|
|
90
|
+
signing_key:
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Rails integration for Toggly feature flags
|
|
93
|
+
test_files: []
|