shamu 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +26 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +89 -30
  5. data/.yardopts +4 -5
  6. data/Gemfile +24 -12
  7. data/Guardfile +5 -0
  8. data/LABELS.md +22 -0
  9. data/README.md +41 -0
  10. data/Rakefile +12 -0
  11. data/circle.yml +7 -3
  12. data/config.ru +7 -0
  13. data/lib/shamu/active_record.rb +7 -0
  14. data/lib/shamu/attributes/assignment.rb +114 -0
  15. data/lib/shamu/attributes/equality.rb +40 -0
  16. data/lib/shamu/attributes/fluid_assignment.rb +49 -0
  17. data/lib/shamu/attributes/validation.rb +74 -0
  18. data/lib/shamu/attributes.rb +255 -0
  19. data/lib/shamu/auditing/README.md +0 -0
  20. data/lib/shamu/auditing/audit_record.rb +32 -0
  21. data/lib/shamu/auditing/auditing_service.rb +32 -0
  22. data/lib/shamu/auditing/list_scope.rb +22 -0
  23. data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
  24. data/lib/shamu/auditing/support.rb +75 -0
  25. data/lib/shamu/auditing/transaction.rb +58 -0
  26. data/lib/shamu/auditing.rb +12 -0
  27. data/lib/shamu/entities/README.md +1 -0
  28. data/lib/shamu/entities/active_record.rb +123 -0
  29. data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
  30. data/lib/shamu/entities/entity.rb +196 -0
  31. data/lib/shamu/entities/entity_path.rb +87 -0
  32. data/lib/shamu/entities/identity_cache.rb +64 -0
  33. data/lib/shamu/entities/list.rb +54 -0
  34. data/lib/shamu/entities/list_scope/dates.rb +57 -0
  35. data/lib/shamu/entities/list_scope/paging.rb +51 -0
  36. data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
  37. data/lib/shamu/entities/list_scope/sorting.rb +76 -0
  38. data/lib/shamu/entities/list_scope.rb +105 -0
  39. data/lib/shamu/entities/null_entity.rb +88 -0
  40. data/lib/shamu/entities.rb +11 -0
  41. data/lib/shamu/error.rb +23 -5
  42. data/lib/shamu/events/README.md +0 -0
  43. data/lib/shamu/events/active_record/channel.rb +36 -0
  44. data/lib/shamu/events/active_record/message.rb +52 -0
  45. data/lib/shamu/events/active_record/migration.rb +49 -0
  46. data/lib/shamu/events/active_record/runner.rb +28 -0
  47. data/lib/shamu/events/active_record/service.rb +174 -0
  48. data/lib/shamu/events/active_record.rb +13 -0
  49. data/lib/shamu/events/channel_stats.rb +23 -0
  50. data/lib/shamu/events/error.rb +24 -0
  51. data/lib/shamu/events/events_service.rb +136 -0
  52. data/lib/shamu/events/in_memory/async_service.rb +48 -0
  53. data/lib/shamu/events/in_memory/service.rb +97 -0
  54. data/lib/shamu/events/in_memory.rb +10 -0
  55. data/lib/shamu/events/message.rb +38 -0
  56. data/lib/shamu/events/support.rb +60 -0
  57. data/lib/shamu/events.rb +12 -0
  58. data/lib/shamu/features/README.md +0 -0
  59. data/lib/shamu/features/conditions/condition.rb +39 -0
  60. data/lib/shamu/features/conditions/env.rb +37 -0
  61. data/lib/shamu/features/conditions/hosts.rb +25 -0
  62. data/lib/shamu/features/conditions/matching.rb +16 -0
  63. data/lib/shamu/features/conditions/not_matching.rb +16 -0
  64. data/lib/shamu/features/conditions/percentage.rb +44 -0
  65. data/lib/shamu/features/conditions/proc.rb +54 -0
  66. data/lib/shamu/features/conditions/roles.rb +23 -0
  67. data/lib/shamu/features/conditions/schedule_at.rb +27 -0
  68. data/lib/shamu/features/conditions.rb +18 -0
  69. data/lib/shamu/features/config_service.rb +10 -0
  70. data/lib/shamu/features/context.rb +80 -0
  71. data/lib/shamu/features/env_store.rb +88 -0
  72. data/lib/shamu/features/errors.rb +29 -0
  73. data/lib/shamu/features/features_service.rb +168 -0
  74. data/lib/shamu/features/list_scope.rb +30 -0
  75. data/lib/shamu/features/selector.rb +50 -0
  76. data/lib/shamu/features/support.rb +51 -0
  77. data/lib/shamu/features/toggle.rb +149 -0
  78. data/lib/shamu/features/toggle_codec.rb +69 -0
  79. data/lib/shamu/features.rb +16 -0
  80. data/lib/shamu/locale/en.yml +22 -2
  81. data/lib/shamu/logger.rb +13 -0
  82. data/lib/shamu/rack/README.md +0 -0
  83. data/lib/shamu/rack/cookies.rb +115 -0
  84. data/lib/shamu/rack/cookies_middleware.rb +26 -0
  85. data/lib/shamu/rack/query_params.rb +41 -0
  86. data/lib/shamu/rack/query_params_middleware.rb +24 -0
  87. data/lib/shamu/rack.rb +12 -0
  88. data/lib/shamu/rails/controller.rb +131 -0
  89. data/lib/shamu/rails/entity.rb +168 -0
  90. data/lib/shamu/rails/features.rb +13 -0
  91. data/lib/shamu/rails/railtie.rb +30 -0
  92. data/lib/shamu/rails.rb +10 -0
  93. data/lib/shamu/rspec/matchers.rb +44 -0
  94. data/lib/shamu/rspec.rb +1 -0
  95. data/lib/shamu/security/README.md +0 -0
  96. data/lib/shamu/security/active_record_policy.rb +106 -0
  97. data/lib/shamu/security/error.rb +65 -0
  98. data/lib/shamu/security/hashed_value.rb +71 -0
  99. data/lib/shamu/security/no_policy.rb +15 -0
  100. data/lib/shamu/security/policy.rb +289 -0
  101. data/lib/shamu/security/policy_refinement.rb +50 -0
  102. data/lib/shamu/security/policy_rule.rb +59 -0
  103. data/lib/shamu/security/principal.rb +72 -0
  104. data/lib/shamu/security/roles.rb +62 -0
  105. data/lib/shamu/security/roles_service.rb +30 -0
  106. data/lib/shamu/security/support.rb +83 -0
  107. data/lib/shamu/security.rb +43 -0
  108. data/lib/shamu/services/README.md +2 -0
  109. data/lib/shamu/services/active_record.rb +58 -0
  110. data/lib/shamu/services/active_record_crud.rb +378 -0
  111. data/lib/shamu/services/error.rb +24 -0
  112. data/lib/shamu/services/lazy_association.rb +31 -0
  113. data/lib/shamu/services/lazy_transform.rb +97 -0
  114. data/lib/shamu/services/request.rb +122 -0
  115. data/lib/shamu/services/request_support.rb +124 -0
  116. data/lib/shamu/services/result.rb +75 -0
  117. data/lib/shamu/services/service.rb +355 -0
  118. data/lib/shamu/services.rb +12 -0
  119. data/lib/shamu/sessions/README.md +2 -0
  120. data/lib/shamu/sessions/cookie_store.rb +79 -0
  121. data/lib/shamu/sessions/session_store.rb +42 -0
  122. data/lib/shamu/sessions.rb +8 -0
  123. data/lib/shamu/to_bool_extension.rb +57 -0
  124. data/lib/shamu/to_model_id_extension.rb +50 -0
  125. data/lib/shamu/version.rb +10 -4
  126. data/lib/shamu.rb +18 -6
  127. data/shamu.gemspec +21 -10
  128. data/spec/internal/README.md +4 -0
  129. data/spec/internal/config/database.yml +3 -0
  130. data/spec/internal/config/routes.rb +3 -0
  131. data/spec/internal/db/schema.rb +3 -0
  132. data/spec/internal/log/.gitignore +1 -0
  133. data/spec/internal/public/favicon.ico +0 -0
  134. data/spec/lib/shamu/active_record_support.rb +32 -0
  135. data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
  136. data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
  137. data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
  138. data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
  139. data/spec/lib/shamu/attributes_spec.rb +331 -0
  140. data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
  141. data/spec/lib/shamu/auditing/support_spec.rb +41 -0
  142. data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
  143. data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
  144. data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
  145. data/spec/lib/shamu/entities/entity_spec.rb +56 -0
  146. data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
  147. data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
  148. data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
  149. data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
  150. data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
  151. data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
  152. data/spec/lib/shamu/entities/list_spec.rb +60 -0
  153. data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
  154. data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
  155. data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
  156. data/spec/lib/shamu/events/events_service_spec.rb +57 -0
  157. data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
  158. data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
  159. data/spec/lib/shamu/events/message_spec.rb +7 -0
  160. data/spec/lib/shamu/events/support_spec.rb +44 -0
  161. data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
  162. data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
  163. data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
  164. data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
  165. data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
  166. data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
  167. data/spec/lib/shamu/features/env_store_spec.rb +48 -0
  168. data/spec/lib/shamu/features/features.yml +34 -0
  169. data/spec/lib/shamu/features/features_service_spec.rb +109 -0
  170. data/spec/lib/shamu/features/secondary.yml +5 -0
  171. data/spec/lib/shamu/features/selector_spec.rb +17 -0
  172. data/spec/lib/shamu/features/support_spec.rb +45 -0
  173. data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
  174. data/spec/lib/shamu/features/toggle_spec.rb +42 -0
  175. data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
  176. data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
  177. data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
  178. data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
  179. data/spec/lib/shamu/rails/controller_spec.rb +74 -0
  180. data/spec/lib/shamu/rails/entity_spec.rb +150 -0
  181. data/spec/lib/shamu/rails/features.yml +13 -0
  182. data/spec/lib/shamu/rails/features_spec.rb +45 -0
  183. data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
  184. data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
  185. data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
  186. data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
  187. data/spec/lib/shamu/security/policy_spec.rb +158 -0
  188. data/spec/lib/shamu/security/roles_spec.rb +46 -0
  189. data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
  190. data/spec/lib/shamu/services/active_record_spec.rb +92 -0
  191. data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
  192. data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
  193. data/spec/lib/shamu/services/request_spec.rb +58 -0
  194. data/spec/lib/shamu/services/request_support_spec.rb +129 -0
  195. data/spec/lib/shamu/services/result_spec.rb +37 -0
  196. data/spec/lib/shamu/services/service_spec.rb +307 -0
  197. data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
  198. data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
  199. data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
  200. data/spec/rails_helper.rb +13 -0
  201. data/spec/spec_helper.rb +17 -12
  202. data/spec/support/active_record.rb +17 -0
  203. data/spec/support/database.rb +14 -0
  204. data/spec/support/logger.rb +0 -0
  205. metadata +383 -9
  206. data/spec/lib/shamu_spec.rb +0 -5
  207. /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