shamu 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +26 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +89 -30
- data/.yardopts +4 -5
- data/Gemfile +24 -12
- data/Guardfile +5 -0
- data/LABELS.md +22 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/circle.yml +7 -3
- data/config.ru +7 -0
- data/lib/shamu/active_record.rb +7 -0
- data/lib/shamu/attributes/assignment.rb +114 -0
- data/lib/shamu/attributes/equality.rb +40 -0
- data/lib/shamu/attributes/fluid_assignment.rb +49 -0
- data/lib/shamu/attributes/validation.rb +74 -0
- data/lib/shamu/attributes.rb +255 -0
- data/lib/shamu/auditing/README.md +0 -0
- data/lib/shamu/auditing/audit_record.rb +32 -0
- data/lib/shamu/auditing/auditing_service.rb +32 -0
- data/lib/shamu/auditing/list_scope.rb +22 -0
- data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
- data/lib/shamu/auditing/support.rb +75 -0
- data/lib/shamu/auditing/transaction.rb +58 -0
- data/lib/shamu/auditing.rb +12 -0
- data/lib/shamu/entities/README.md +1 -0
- data/lib/shamu/entities/active_record.rb +123 -0
- data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
- data/lib/shamu/entities/entity.rb +196 -0
- data/lib/shamu/entities/entity_path.rb +87 -0
- data/lib/shamu/entities/identity_cache.rb +64 -0
- data/lib/shamu/entities/list.rb +54 -0
- data/lib/shamu/entities/list_scope/dates.rb +57 -0
- data/lib/shamu/entities/list_scope/paging.rb +51 -0
- data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
- data/lib/shamu/entities/list_scope/sorting.rb +76 -0
- data/lib/shamu/entities/list_scope.rb +105 -0
- data/lib/shamu/entities/null_entity.rb +88 -0
- data/lib/shamu/entities.rb +11 -0
- data/lib/shamu/error.rb +23 -5
- data/lib/shamu/events/README.md +0 -0
- data/lib/shamu/events/active_record/channel.rb +36 -0
- data/lib/shamu/events/active_record/message.rb +52 -0
- data/lib/shamu/events/active_record/migration.rb +49 -0
- data/lib/shamu/events/active_record/runner.rb +28 -0
- data/lib/shamu/events/active_record/service.rb +174 -0
- data/lib/shamu/events/active_record.rb +13 -0
- data/lib/shamu/events/channel_stats.rb +23 -0
- data/lib/shamu/events/error.rb +24 -0
- data/lib/shamu/events/events_service.rb +136 -0
- data/lib/shamu/events/in_memory/async_service.rb +48 -0
- data/lib/shamu/events/in_memory/service.rb +97 -0
- data/lib/shamu/events/in_memory.rb +10 -0
- data/lib/shamu/events/message.rb +38 -0
- data/lib/shamu/events/support.rb +60 -0
- data/lib/shamu/events.rb +12 -0
- data/lib/shamu/features/README.md +0 -0
- data/lib/shamu/features/conditions/condition.rb +39 -0
- data/lib/shamu/features/conditions/env.rb +37 -0
- data/lib/shamu/features/conditions/hosts.rb +25 -0
- data/lib/shamu/features/conditions/matching.rb +16 -0
- data/lib/shamu/features/conditions/not_matching.rb +16 -0
- data/lib/shamu/features/conditions/percentage.rb +44 -0
- data/lib/shamu/features/conditions/proc.rb +54 -0
- data/lib/shamu/features/conditions/roles.rb +23 -0
- data/lib/shamu/features/conditions/schedule_at.rb +27 -0
- data/lib/shamu/features/conditions.rb +18 -0
- data/lib/shamu/features/config_service.rb +10 -0
- data/lib/shamu/features/context.rb +80 -0
- data/lib/shamu/features/env_store.rb +88 -0
- data/lib/shamu/features/errors.rb +29 -0
- data/lib/shamu/features/features_service.rb +168 -0
- data/lib/shamu/features/list_scope.rb +30 -0
- data/lib/shamu/features/selector.rb +50 -0
- data/lib/shamu/features/support.rb +51 -0
- data/lib/shamu/features/toggle.rb +149 -0
- data/lib/shamu/features/toggle_codec.rb +69 -0
- data/lib/shamu/features.rb +16 -0
- data/lib/shamu/locale/en.yml +22 -2
- data/lib/shamu/logger.rb +13 -0
- data/lib/shamu/rack/README.md +0 -0
- data/lib/shamu/rack/cookies.rb +115 -0
- data/lib/shamu/rack/cookies_middleware.rb +26 -0
- data/lib/shamu/rack/query_params.rb +41 -0
- data/lib/shamu/rack/query_params_middleware.rb +24 -0
- data/lib/shamu/rack.rb +12 -0
- data/lib/shamu/rails/controller.rb +131 -0
- data/lib/shamu/rails/entity.rb +168 -0
- data/lib/shamu/rails/features.rb +13 -0
- data/lib/shamu/rails/railtie.rb +30 -0
- data/lib/shamu/rails.rb +10 -0
- data/lib/shamu/rspec/matchers.rb +44 -0
- data/lib/shamu/rspec.rb +1 -0
- data/lib/shamu/security/README.md +0 -0
- data/lib/shamu/security/active_record_policy.rb +106 -0
- data/lib/shamu/security/error.rb +65 -0
- data/lib/shamu/security/hashed_value.rb +71 -0
- data/lib/shamu/security/no_policy.rb +15 -0
- data/lib/shamu/security/policy.rb +289 -0
- data/lib/shamu/security/policy_refinement.rb +50 -0
- data/lib/shamu/security/policy_rule.rb +59 -0
- data/lib/shamu/security/principal.rb +72 -0
- data/lib/shamu/security/roles.rb +62 -0
- data/lib/shamu/security/roles_service.rb +30 -0
- data/lib/shamu/security/support.rb +83 -0
- data/lib/shamu/security.rb +43 -0
- data/lib/shamu/services/README.md +2 -0
- data/lib/shamu/services/active_record.rb +58 -0
- data/lib/shamu/services/active_record_crud.rb +378 -0
- data/lib/shamu/services/error.rb +24 -0
- data/lib/shamu/services/lazy_association.rb +31 -0
- data/lib/shamu/services/lazy_transform.rb +97 -0
- data/lib/shamu/services/request.rb +122 -0
- data/lib/shamu/services/request_support.rb +124 -0
- data/lib/shamu/services/result.rb +75 -0
- data/lib/shamu/services/service.rb +355 -0
- data/lib/shamu/services.rb +12 -0
- data/lib/shamu/sessions/README.md +2 -0
- data/lib/shamu/sessions/cookie_store.rb +79 -0
- data/lib/shamu/sessions/session_store.rb +42 -0
- data/lib/shamu/sessions.rb +8 -0
- data/lib/shamu/to_bool_extension.rb +57 -0
- data/lib/shamu/to_model_id_extension.rb +50 -0
- data/lib/shamu/version.rb +10 -4
- data/lib/shamu.rb +18 -6
- data/shamu.gemspec +21 -10
- data/spec/internal/README.md +4 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/shamu/active_record_support.rb +32 -0
- data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
- data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
- data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
- data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
- data/spec/lib/shamu/attributes_spec.rb +331 -0
- data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
- data/spec/lib/shamu/auditing/support_spec.rb +41 -0
- data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
- data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
- data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
- data/spec/lib/shamu/entities/entity_spec.rb +56 -0
- data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
- data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
- data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
- data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
- data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
- data/spec/lib/shamu/entities/list_spec.rb +60 -0
- data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
- data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
- data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
- data/spec/lib/shamu/events/events_service_spec.rb +57 -0
- data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
- data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
- data/spec/lib/shamu/events/message_spec.rb +7 -0
- data/spec/lib/shamu/events/support_spec.rb +44 -0
- data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
- data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
- data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
- data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
- data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
- data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
- data/spec/lib/shamu/features/env_store_spec.rb +48 -0
- data/spec/lib/shamu/features/features.yml +34 -0
- data/spec/lib/shamu/features/features_service_spec.rb +109 -0
- data/spec/lib/shamu/features/secondary.yml +5 -0
- data/spec/lib/shamu/features/selector_spec.rb +17 -0
- data/spec/lib/shamu/features/support_spec.rb +45 -0
- data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
- data/spec/lib/shamu/features/toggle_spec.rb +42 -0
- data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
- data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
- data/spec/lib/shamu/rails/controller_spec.rb +74 -0
- data/spec/lib/shamu/rails/entity_spec.rb +150 -0
- data/spec/lib/shamu/rails/features.yml +13 -0
- data/spec/lib/shamu/rails/features_spec.rb +45 -0
- data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
- data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
- data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
- data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
- data/spec/lib/shamu/security/policy_spec.rb +158 -0
- data/spec/lib/shamu/security/roles_spec.rb +46 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
- data/spec/lib/shamu/services/active_record_spec.rb +92 -0
- data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
- data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
- data/spec/lib/shamu/services/request_spec.rb +58 -0
- data/spec/lib/shamu/services/request_support_spec.rb +129 -0
- data/spec/lib/shamu/services/result_spec.rb +37 -0
- data/spec/lib/shamu/services/service_spec.rb +307 -0
- data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
- data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
- data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
- data/spec/rails_helper.rb +13 -0
- data/spec/spec_helper.rb +17 -12
- data/spec/support/active_record.rb +17 -0
- data/spec/support/database.rb +14 -0
- data/spec/support/logger.rb +0 -0
- metadata +383 -9
- data/spec/lib/shamu_spec.rb +0 -5
- /data/{spec/internal/log/test.log → lib/shamu/attributes/README.md} +0 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
module Conditions
|
4
|
+
|
5
|
+
# Match against the current date and time.
|
6
|
+
class ScheduleAt < Conditions::Condition
|
7
|
+
|
8
|
+
# (see Condition#match?)
|
9
|
+
def match?( context )
|
10
|
+
context.time >= timestamp
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def timestamp
|
16
|
+
@timestamp ||=
|
17
|
+
case config
|
18
|
+
when Date then config.to_time
|
19
|
+
when String then Time.zone ? Time.zone.parse( config ) : Time.parse( config )
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# Conditions that must match for a {Selector} to enable a {Toggle}.
|
5
|
+
module Conditions
|
6
|
+
require "shamu/features/conditions/condition"
|
7
|
+
|
8
|
+
require "shamu/features/conditions/env"
|
9
|
+
require "shamu/features/conditions/hosts"
|
10
|
+
require "shamu/features/conditions/matching"
|
11
|
+
require "shamu/features/conditions/not_matching"
|
12
|
+
require "shamu/features/conditions/percentage"
|
13
|
+
require "shamu/features/conditions/proc"
|
14
|
+
require "shamu/features/conditions/roles"
|
15
|
+
require "shamu/features/conditions/schedule_at"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# Present a unified get/set interface for some distributed feature
|
5
|
+
# configuration stored in an external persistence system (redis,
|
6
|
+
# ActiveRecord, consul, etc, etc.).
|
7
|
+
class ConfigService < Services::Service
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "socket"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
module Features
|
5
|
+
|
6
|
+
# Captures the environment and request specific context used to match
|
7
|
+
# {Toggle} selectors and determine if a feature should be enabled.
|
8
|
+
class Context
|
9
|
+
include Shamu::Attributes
|
10
|
+
|
11
|
+
# ============================================================================
|
12
|
+
# @!group Attributes
|
13
|
+
#
|
14
|
+
|
15
|
+
# @!attribute
|
16
|
+
# @return [Time] the current time.
|
17
|
+
attribute :time do
|
18
|
+
Time.zone ? Time.zone.now : Time.now
|
19
|
+
end
|
20
|
+
|
21
|
+
# @!attribute
|
22
|
+
# @return [Array<Symbol>] roles assigned to the current user.
|
23
|
+
attribute :roles
|
24
|
+
|
25
|
+
# @!attribute
|
26
|
+
# @return [String] the name of the host machine.
|
27
|
+
attribute :host do
|
28
|
+
Socket.gethostname
|
29
|
+
end
|
30
|
+
|
31
|
+
# @!attribute
|
32
|
+
# @return [Integer,String] id of the current user - either an Integer, or a UUID.
|
33
|
+
attribute :user_id
|
34
|
+
|
35
|
+
# @!attribute
|
36
|
+
# @return [Scorpion] used to dynamically look up dependencies by
|
37
|
+
# {Conditions}.
|
38
|
+
attribute :scorpion
|
39
|
+
|
40
|
+
#
|
41
|
+
# @!endgroup Attributes
|
42
|
+
|
43
|
+
def initialize( features_service, **attributes )
|
44
|
+
@features_service = features_service
|
45
|
+
super( **attributes )
|
46
|
+
end
|
47
|
+
|
48
|
+
# Retrieve a value from the host machine's environment. Abstracts over the
|
49
|
+
# ENV hash to permit some filtering and to facilitate specs.
|
50
|
+
#
|
51
|
+
# @param [String] name of the environment variable.
|
52
|
+
# @return [String] the environment variable.
|
53
|
+
def env( name )
|
54
|
+
ENV[name]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if feature is enabled.
|
58
|
+
def enabled?( name )
|
59
|
+
features_service.enabled?( name )
|
60
|
+
end
|
61
|
+
|
62
|
+
# Remember the toggle selection in persistent storage for the user so that
|
63
|
+
# they will receive the same result each time.
|
64
|
+
def sticky!
|
65
|
+
@sticky = true
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [Boolean] true if the feature election should be remembered
|
70
|
+
# between requests.
|
71
|
+
def sticky?
|
72
|
+
@sticky
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
attr_reader :features_service
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# Expose a read-only runtime environment for consumption by the
|
5
|
+
# {FeaturesService}. By default blends a Rack env request headers (if using
|
6
|
+
# Rack) with the host env. The request env overrides the host.
|
7
|
+
#
|
8
|
+
# When {#fetch fetching}, EnvStore will look for an `X-Shamu-Features` header
|
9
|
+
# sent in the HTTP request. It should be constructed using {.pack} to
|
10
|
+
# build a verifiable hash of feature settings.
|
11
|
+
#
|
12
|
+
# If a rack value is not set, EnvStore will fall back to looking for the
|
13
|
+
# toggle in the host's environment with the name `TOGGLE_{ toggle name
|
14
|
+
# upcased and underscored }`. For example `buy_now/one_click` will look for
|
15
|
+
# `TOGGLE_BUY_NOW_ONE_CLICK` in the environment.
|
16
|
+
class EnvStore < Services::Service
|
17
|
+
|
18
|
+
RACK_ENV_KEY = "HTTP_X_SHAMU_FEATURES".freeze
|
19
|
+
RACK_PARAMS_KEY = "shamu.features".freeze
|
20
|
+
RACK_PARAMS_FEATURES_KEY = "shamu.features.from_params".freeze
|
21
|
+
RACK_HEADER_FEATURES_KEY = "shamu.features.from_header".freeze
|
22
|
+
|
23
|
+
# ============================================================================
|
24
|
+
# @!group Dependencies
|
25
|
+
#
|
26
|
+
|
27
|
+
# @!attribute
|
28
|
+
# @return [ToggleCode] code used to pack and unpack the features.
|
29
|
+
attr_dependency :codec, ToggleCodec
|
30
|
+
|
31
|
+
#
|
32
|
+
# @!endgroup Dependencies
|
33
|
+
|
34
|
+
# Fetch a value from the environment.
|
35
|
+
def fetch( key, &block )
|
36
|
+
return env_fetch( key, &block ) unless defined? Rack
|
37
|
+
rack_params_fetch( key, &block )
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [String] the expected ENV key name for the given toggle name.
|
41
|
+
def self.env_key_name( key )
|
42
|
+
key = key.upcase
|
43
|
+
key.tr! "/", "_"
|
44
|
+
key
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def env_fetch( key, &block )
|
50
|
+
key = self.class.env_key_name( key )
|
51
|
+
if ENV.key?( key )
|
52
|
+
ENV[ key ].to_bool
|
53
|
+
elsif block_given?
|
54
|
+
yield
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def rack_header_fetch( key, &block )
|
59
|
+
rack_env = scorpion.fetch( Scorpion::Rack::Env )
|
60
|
+
return env_fetch( key, &block ) unless header = rack_env[RACK_ENV_KEY]
|
61
|
+
|
62
|
+
features = rack_env.fetch( RACK_HEADER_FEATURES_KEY ) do
|
63
|
+
rack_env[ RACK_HEADER_FEATURES_KEY ] = codec.unpack( header )
|
64
|
+
end
|
65
|
+
|
66
|
+
features.fetch( key ) do
|
67
|
+
env_fetch( key, &block )
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def rack_params_fetch( key, &block )
|
72
|
+
rack_env = scorpion.fetch( Scorpion::Rack::Env )
|
73
|
+
request = ::Rack::Request.new( rack_env )
|
74
|
+
|
75
|
+
return rack_header_fetch( key, &block ) unless param = request.params[ RACK_PARAMS_KEY ]
|
76
|
+
|
77
|
+
features = rack_env.fetch( RACK_PARAMS_FEATURES_KEY ) do
|
78
|
+
rack_env[ RACK_PARAMS_FEATURES_KEY ] = codec.unpack( param )
|
79
|
+
end
|
80
|
+
|
81
|
+
features.fetch( key ) do
|
82
|
+
rack_header_fetch( key, &block )
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# An error occcured in the Features domain.
|
5
|
+
class Error < Shamu::Error
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def translation_scope
|
10
|
+
super.dup.insert( 1, :features )
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
# An feature toggle was checked that has been marked as retired.
|
16
|
+
class RetiredToggleError < Error
|
17
|
+
|
18
|
+
# @!attribute
|
19
|
+
# @return [Toggle] the retired toggle
|
20
|
+
attr_reader :toggle
|
21
|
+
|
22
|
+
def initialize( toggle )
|
23
|
+
@toggle = toggle
|
24
|
+
|
25
|
+
super translate( :retired_toggle_checked, name: toggle.name, retire_at: toggle.retire_at.to_s )
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require "listen"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
module Features
|
5
|
+
|
6
|
+
# ...
|
7
|
+
class FeaturesService < Services::Service
|
8
|
+
include Security::Support
|
9
|
+
|
10
|
+
SESSION_KEY = "shamu.toggles".freeze
|
11
|
+
|
12
|
+
# ============================================================================
|
13
|
+
# @!group Dependencies
|
14
|
+
#
|
15
|
+
|
16
|
+
# @!attribute
|
17
|
+
# @return [Shamu::Sessions::SessionStore]
|
18
|
+
#
|
19
|
+
# A persistent storage for a user session where the feature service can
|
20
|
+
# persist sticky feature toggles.
|
21
|
+
attr_dependency :session_store, Shamu::Sessions::SessionStore
|
22
|
+
|
23
|
+
# @!attribute
|
24
|
+
# @return [Shamu::Features::ToggleCodec]
|
25
|
+
#
|
26
|
+
# Used to pack and unpack sticky toggle overrides in a persistent user
|
27
|
+
# session.
|
28
|
+
attr_dependency :toggle_codec, Shamu::Features::ToggleCodec
|
29
|
+
|
30
|
+
# @!attribute
|
31
|
+
# @return [Shamu::Features::EnvStore]
|
32
|
+
#
|
33
|
+
# Read-only access to Rack and host ENV toggle overrides.
|
34
|
+
attr_dependency :env_store, Shamu::Features::EnvStore
|
35
|
+
|
36
|
+
#
|
37
|
+
# @!endgroup Dependencies
|
38
|
+
|
39
|
+
# @!method initialize( config_path )
|
40
|
+
# @param
|
41
|
+
# @return [FeaturesService]
|
42
|
+
initialize do |config_path = nil, **|
|
43
|
+
@config_path = config_path || self.class.default_config_path
|
44
|
+
end
|
45
|
+
|
46
|
+
# Indicates if the feature is enabled for the current request/session.
|
47
|
+
#
|
48
|
+
# @param [String] name of the feature.
|
49
|
+
# @return [Boolean] true if the feature is enabled.
|
50
|
+
def enabled?( name )
|
51
|
+
context = build_context
|
52
|
+
|
53
|
+
if toggle = toggles[name]
|
54
|
+
resolve_known( toggle, context )
|
55
|
+
else
|
56
|
+
resolve_unknown( name )
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# List all the known toggles with the given prefix.
|
61
|
+
# @param [String] name prefix
|
62
|
+
# @return [Hash] the known toggles.
|
63
|
+
def list( prefix = nil )
|
64
|
+
if prefix.present?
|
65
|
+
toggles.each_with_object({}) do |(name, toggle), result|
|
66
|
+
next unless name.start_with?( prefix )
|
67
|
+
result[name] = toggle
|
68
|
+
end
|
69
|
+
else
|
70
|
+
toggles.dup
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
attr_reader :config_path
|
77
|
+
|
78
|
+
def toggles
|
79
|
+
@toggles ||= begin
|
80
|
+
if File.exist?( config_path )
|
81
|
+
listener = Listen.to File.dirname( config_path ), only: File.basename( config_path ) do
|
82
|
+
@toggles = Toggle.load( config_path )
|
83
|
+
end
|
84
|
+
listener.start
|
85
|
+
|
86
|
+
Toggle.load( config_path )
|
87
|
+
else
|
88
|
+
logger.warn "Feature configuration file does not exist: #{ config_path }"
|
89
|
+
{}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def resolve_unknown( name )
|
95
|
+
logger.info "The '#{ name }' feature toggle has not been configured. Add to #{ config_path }."
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
def resolve_known( toggle, context )
|
100
|
+
fail RetiredToggleError.new( toggle ) if toggle.retired?( context ) # rubocop:disable Style/RaiseArgs
|
101
|
+
|
102
|
+
store_value = resolve_store_toggle( toggle )
|
103
|
+
return store_value unless store_value.nil?
|
104
|
+
|
105
|
+
resolve_toggle( toggle, context )
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_context
|
109
|
+
Features::Context.new self,
|
110
|
+
scorpion: scorpion,
|
111
|
+
user_id: security_principal.user_id,
|
112
|
+
roles: roles_service.roles_for( security_principal.user_id )
|
113
|
+
end
|
114
|
+
|
115
|
+
def resolve_toggle( toggle, context )
|
116
|
+
toggle.enabled?( context ).tap do |result|
|
117
|
+
persist_sticky( toggle.name, result ) if context.sticky?
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def resolve_store_toggle( toggle )
|
122
|
+
# session_store is for sticky overrides
|
123
|
+
sticky_overrides.fetch( toggle.name ) do
|
124
|
+
# env_store is for host and service header overrides
|
125
|
+
env_store.fetch( toggle.name )
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def persist_sticky( name, result )
|
130
|
+
sticky_overrides[ name ] = result
|
131
|
+
session_store.set( SESSION_KEY, toggle_codec.pack( sticky_overrides ) )
|
132
|
+
end
|
133
|
+
|
134
|
+
def sticky_overrides
|
135
|
+
@sticky_overrides ||= begin
|
136
|
+
if token = session_store.fetch( SESSION_KEY )
|
137
|
+
toggle_codec.unpack( token )
|
138
|
+
else
|
139
|
+
{}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
class << self
|
146
|
+
# Looks for a config/features.yml or features.yml in the current
|
147
|
+
# directory. Use {#ddefault_config_path=} to manually set the default
|
148
|
+
# config file.
|
149
|
+
#
|
150
|
+
# @return [String] the default path to load toggle information from.
|
151
|
+
def default_config_path
|
152
|
+
@default_config_path ||= begin
|
153
|
+
path = File.expand_path( "config/features.yml" )
|
154
|
+
path = File.expand_path( "features.yml" ) unless File.exist?( path )
|
155
|
+
path
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# @param [String] path of the default config file.
|
160
|
+
# @return [String]
|
161
|
+
def default_config_path=( path ) # rubocop:disable Style/TrivialAccessors
|
162
|
+
@default_config_path = path
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# Select the features to be listed.
|
5
|
+
class ListScope < Entities::ListScope
|
6
|
+
|
7
|
+
# ============================================================================
|
8
|
+
# @!group Attributes
|
9
|
+
#
|
10
|
+
|
11
|
+
# @!attribute
|
12
|
+
# @return [Symbol] the desired type of toggle.
|
13
|
+
attribute :type, inclusion: { in: Features::Toggle::TYPES }
|
14
|
+
|
15
|
+
# @!attribute
|
16
|
+
# @return [Symbol] only include toggles that have retired but are still
|
17
|
+
# configured.
|
18
|
+
attribute :retired, coerce: :to_bool
|
19
|
+
|
20
|
+
# @!attribute
|
21
|
+
# @return [String] include toggles with a name that is prefixed with the
|
22
|
+
# given value.
|
23
|
+
attribute :prefix, coerce: :to_s
|
24
|
+
|
25
|
+
#
|
26
|
+
# @!endgroup Attributes
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# A selector used to match conditions against environment configuration.
|
5
|
+
class Selector
|
6
|
+
|
7
|
+
# ============================================================================
|
8
|
+
# @!group Attributes
|
9
|
+
#
|
10
|
+
|
11
|
+
# @!attribute
|
12
|
+
# @return [Array<Condition>] conditions that must match for the selector
|
13
|
+
# to match.
|
14
|
+
attr_reader :conditions
|
15
|
+
|
16
|
+
# @!attribute
|
17
|
+
# @return [Boolean] true if the feature should not be enabled when the
|
18
|
+
# selector matches.
|
19
|
+
attr_reader :reject
|
20
|
+
|
21
|
+
# @!attribute
|
22
|
+
# @return [Toggle] that owns the selector.
|
23
|
+
attr_reader :toggle
|
24
|
+
|
25
|
+
#
|
26
|
+
# @!endgroup Attributes
|
27
|
+
|
28
|
+
def initialize( toggle, config )
|
29
|
+
@conditions = []
|
30
|
+
|
31
|
+
config.each do |name, condition_config|
|
32
|
+
if name == "reject"
|
33
|
+
@reject = condition_config.to_bool
|
34
|
+
else
|
35
|
+
@conditions << Conditions::Condition.create( name, condition_config, toggle )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@conditions.freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [Context] context the feature evaluation context.
|
43
|
+
# @return [Boolean] true if the selector matches the given environment.
|
44
|
+
def match?( context )
|
45
|
+
conditions.all? { |c| c.match?( context ) }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Features
|
3
|
+
|
4
|
+
# Add feature togggle support to an object.
|
5
|
+
module Support
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
|
10
|
+
# ============================================================================
|
11
|
+
# @!group Dependencies
|
12
|
+
#
|
13
|
+
|
14
|
+
# @!attribute
|
15
|
+
# @return [Features::FeaturesService] the service used to resolve
|
16
|
+
# enabled features.
|
17
|
+
attr_dependency :features_service, Features::FeaturesService, lazy: true
|
18
|
+
|
19
|
+
#
|
20
|
+
# @!endgroup Dependencies
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# @!visibility public
|
27
|
+
#
|
28
|
+
# Only execute the block if the current {Features::Context} has the
|
29
|
+
# named featue enabled.
|
30
|
+
#
|
31
|
+
# @param [String] feature name.
|
32
|
+
# @param [Boolean] override force the feature to be either on or off.
|
33
|
+
# @yield Yields if the feature is enabled.
|
34
|
+
# @yieldreturn the result of the block or nil if the feature wasn't
|
35
|
+
# enabled.
|
36
|
+
def when_feature( feature, override: nil, &block )
|
37
|
+
yield if override.nil? ? feature_enabled?( feature ) : override
|
38
|
+
end
|
39
|
+
|
40
|
+
# @!visibility public
|
41
|
+
#
|
42
|
+
# Determines if the given feature has been toggled.
|
43
|
+
#
|
44
|
+
# @param [Symbol] feature name of the feature to check.
|
45
|
+
# @return [Boolean] true if the feature has been toggled on.
|
46
|
+
def feature_enabled?( feature )
|
47
|
+
features_service.enabled?( feature )
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|