arturo 0.2.3.8 → 1.0.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.
- data/README.md +58 -79
- data/app/controllers/arturo/features_controller.rb +20 -25
- data/app/helpers/arturo/features_helper.rb +10 -0
- data/app/models/arturo/feature.rb +6 -18
- data/app/views/arturo/features/_form.html.erb +3 -7
- data/app/views/arturo/features/index.html.erb +1 -1
- data/config/routes.rb +14 -0
- data/lib/arturo.rb +2 -31
- data/lib/arturo/configuration.rb +43 -0
- data/lib/arturo/controller_filters.rb +1 -1
- data/lib/arturo/engine.rb +8 -14
- data/lib/arturo/feature_availability.rb +2 -10
- data/lib/generators/arturo/assets_generator.rb +18 -0
- data/lib/generators/arturo/initializer_generator.rb +13 -0
- data/lib/generators/arturo/migration_generator.rb +27 -0
- data/lib/generators/arturo/routes_generator.rb +15 -0
- data/lib/generators/arturo/templates/arturo.css +2 -2
- data/lib/generators/arturo/templates/arturo.js +1 -1
- data/lib/generators/arturo/templates/initializer.rb +10 -18
- metadata +107 -154
- data/HISTORY.md +0 -16
- data/lib/arturo/feature_caching.rb +0 -141
- data/lib/arturo/feature_management.rb +0 -23
- data/lib/arturo/middleware.rb +0 -60
- data/lib/arturo/range_form_support.rb +0 -17
- data/lib/arturo/test_support.rb +0 -32
- data/lib/generators/arturo/arturo_generator.rb +0 -40
- data/rails/init.rb +0 -7
data/HISTORY.md
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
## v 1.1.0 - cleanup
|
2
|
-
* changed `require_feature!` to `require_feature`
|
3
|
-
* replaced `Arturo.permit_management` and `Arturo.feature_recipient`
|
4
|
-
blocks with instance methods
|
5
|
-
`Arturo::FeatureManagement.may_manage_features?` and
|
6
|
-
`Arturo::FeatureAvailability.feature_recipient`
|
7
|
-
|
8
|
-
## v 1.0.0 - Initial Release
|
9
|
-
* `require_feature!` controller filter
|
10
|
-
* `if_feature_enabled` controller and view method
|
11
|
-
* `feature_enabled?` controller and view method
|
12
|
-
* CRUD for features
|
13
|
-
* `Arturo.permit_management` to configure management permission
|
14
|
-
* `Arturo.feature_recipient` to configure on what basis features are deployed
|
15
|
-
* whitelists and blacklists
|
16
|
-
|
@@ -1,141 +0,0 @@
|
|
1
|
-
module Arturo
|
2
|
-
|
3
|
-
# To be extended by Arturo::Feature if you want to enable
|
4
|
-
# in-memory caching.
|
5
|
-
# NB: Arturo's feature caching only works when using
|
6
|
-
# Arturo::Feature.to_feature or when using the helper methods
|
7
|
-
# in Arturo and Arturo::FeatureAvailability.
|
8
|
-
# NB: if you have multiple application servers, you almost certainly
|
9
|
-
# want to clear this cache after each request:
|
10
|
-
#
|
11
|
-
# class ApplicationController < ActionController::Base
|
12
|
-
# after_filter { Arturo::Feature.clear_feature_cache }
|
13
|
-
# end
|
14
|
-
#
|
15
|
-
# Alternatively, you could redefine Arturo::Feature.feature_cache
|
16
|
-
# to use a shared cache like Memcached.
|
17
|
-
module FeatureCaching
|
18
|
-
|
19
|
-
# A marker in the cache to record the fact that the feature with the
|
20
|
-
# given symbol doesn't exist.
|
21
|
-
NO_SUCH_FEATURE = :NO_SUCH_FEATURE
|
22
|
-
|
23
|
-
def self.extended(base)
|
24
|
-
class << base
|
25
|
-
alias_method_chain :to_feature, :caching
|
26
|
-
attr_accessor :cache_ttl, :feature_cache, :feature_caching_strategy
|
27
|
-
end
|
28
|
-
base.send(:after_save) do |f|
|
29
|
-
f.class.feature_caching_strategy.expire(f.class.feature_cache, f.symbol.to_sym) if f.class.caches_features?
|
30
|
-
end
|
31
|
-
base.cache_ttl = 0
|
32
|
-
base.feature_cache = Arturo::FeatureCaching::Cache.new
|
33
|
-
base.feature_caching_strategy = AllStrategy
|
34
|
-
end
|
35
|
-
|
36
|
-
def caches_features?
|
37
|
-
self.cache_ttl.to_i > 0
|
38
|
-
end
|
39
|
-
|
40
|
-
# Wraps Arturo::Feature.to_feature with in-memory caching.
|
41
|
-
def to_feature_with_caching(feature_or_symbol)
|
42
|
-
if !caches_features?
|
43
|
-
to_feature_without_caching(feature_or_symbol)
|
44
|
-
elsif feature_or_symbol.kind_of?(Arturo::Feature)
|
45
|
-
feature_or_symbol
|
46
|
-
else
|
47
|
-
symbol = feature_or_symbol.to_sym
|
48
|
-
feature = feature_caching_strategy.fetch(feature_cache, symbol) { to_feature_without_caching(symbol) }
|
49
|
-
feature unless feature == NO_SUCH_FEATURE
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def warm_cache!
|
54
|
-
warn "Deprecated, no longer necessary!"
|
55
|
-
end
|
56
|
-
|
57
|
-
class AllStrategy
|
58
|
-
class << self
|
59
|
-
def fetch(cache, symbol, &block)
|
60
|
-
features = cache.read("arturo.all")
|
61
|
-
|
62
|
-
unless cache_is_current?(cache, features)
|
63
|
-
features = Hash[Arturo::Feature.all.map { |f| [f.symbol.to_sym, f] }]
|
64
|
-
mark_as_current!(cache)
|
65
|
-
cache.write("arturo.all", features, :expires_in => Arturo::Feature.cache_ttl * 10)
|
66
|
-
end
|
67
|
-
|
68
|
-
features[symbol] || NO_SUCH_FEATURE
|
69
|
-
end
|
70
|
-
|
71
|
-
def expire(cache, symbol)
|
72
|
-
cache.delete("arturo.all")
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
def cache_is_current?(cache, features)
|
78
|
-
return unless features
|
79
|
-
return true if cache.read("arturo.current")
|
80
|
-
return false if features.values.map(&:updated_at).compact.max != Arturo::Feature.maximum(:updated_at)
|
81
|
-
mark_as_current!(cache)
|
82
|
-
end
|
83
|
-
|
84
|
-
def mark_as_current!(cache)
|
85
|
-
cache.write("arturo.current", true, :expires_in => Arturo::Feature.cache_ttl)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
class OneStrategy
|
91
|
-
def self.fetch(cache, symbol, &block)
|
92
|
-
if feature = cache.read("arturo.#{symbol}")
|
93
|
-
feature
|
94
|
-
else
|
95
|
-
cache.write("arturo.#{symbol}", yield || NO_SUCH_FEATURE, :expires_in => Arturo::Feature.cache_ttl)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def self.expire(cache, symbol)
|
100
|
-
cache.delete("arturo.#{symbol}")
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# Quack like a Rails cache.
|
105
|
-
class Cache
|
106
|
-
def initialize
|
107
|
-
@data = {} # of the form {key => [value, expires_at or nil]}
|
108
|
-
end
|
109
|
-
|
110
|
-
def read(name, options = nil)
|
111
|
-
value, expires_at = *@data[name]
|
112
|
-
if value && (expires_at.blank? || expires_at > Time.now)
|
113
|
-
value
|
114
|
-
else
|
115
|
-
nil
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def delete(name)
|
120
|
-
@data.delete(name)
|
121
|
-
end
|
122
|
-
|
123
|
-
def write(name, value, options = nil)
|
124
|
-
expires_at = if options && options.respond_to?(:[]) && options[:expires_in]
|
125
|
-
Time.now + options.delete(:expires_in)
|
126
|
-
else
|
127
|
-
nil
|
128
|
-
end
|
129
|
-
value.freeze.tap do |val|
|
130
|
-
@data[name] = [value, expires_at]
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def clear
|
135
|
-
@data.clear
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
end
|
140
|
-
|
141
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module Arturo
|
2
|
-
|
3
|
-
# A mixin that is included by Arturo::FeaturesController and is declared
|
4
|
-
# as a helper for all views. It provides a single method,
|
5
|
-
# may_manage_features?, that returns whether or not the current user
|
6
|
-
# may manage features. By default, it is implemented as follows:
|
7
|
-
#
|
8
|
-
# def may_manage_features?
|
9
|
-
# current_user.present? && current_user.admin?
|
10
|
-
# end
|
11
|
-
#
|
12
|
-
# If you would like to change this implementation, it is recommended
|
13
|
-
# you do so in config/initializers/arturo_initializer.rb
|
14
|
-
module FeatureManagement
|
15
|
-
|
16
|
-
# @return [true,false] whether the current user may manage features
|
17
|
-
def may_manage_features?
|
18
|
-
current_user.present? && current_user.admin?
|
19
|
-
end
|
20
|
-
|
21
|
-
end
|
22
|
-
|
23
|
-
end
|
data/lib/arturo/middleware.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
module Arturo
|
2
|
-
# A Rack middleware that requires a feature to be present. By default,
|
3
|
-
# checks feature availability against an `arturo.recipient` object
|
4
|
-
# in the `env`. If that object is missing, this middleware always fails,
|
5
|
-
# even if the feature is available for everyone.
|
6
|
-
#
|
7
|
-
# ## Usage
|
8
|
-
#
|
9
|
-
# use Arturo::Middleware, :feature => :foo
|
10
|
-
#
|
11
|
-
# ## Options
|
12
|
-
#
|
13
|
-
# * feature -- the name of the feature to require, as a Symbol; required
|
14
|
-
#
|
15
|
-
# * recipient -- the key in the `env` hash under which the feature
|
16
|
-
# recipient can be found; defaults to "arturo.recipient".
|
17
|
-
# * on_unavailable -- a Rack-like object
|
18
|
-
# (has `#call(Hash) -> [status, headers, body]`) that
|
19
|
-
# is called when the feature is unavailable; defaults
|
20
|
-
# to returning `[ 404, {}, ['Not Found'] ]`.
|
21
|
-
class Middleware
|
22
|
-
|
23
|
-
MISSING_FEATURE_ERROR = "Cannot create an Arturo::Middleware without a :feature"
|
24
|
-
|
25
|
-
DEFAULT_RECIPIENT_KEY = 'arturo.recipient'
|
26
|
-
|
27
|
-
DEFAULT_ON_UNAVAILABLE = lambda { |env| [ 404, {}, ['Not Found'] ] }
|
28
|
-
|
29
|
-
def initialize(app, options = {})
|
30
|
-
@app = app
|
31
|
-
@feature = options[:feature]
|
32
|
-
raise ArgumentError.new(MISSING_FEATURE_ERROR) unless @feature
|
33
|
-
@recipient_key = options[:recipient] || DEFAULT_RECIPIENT_KEY
|
34
|
-
@on_unavailable = options[:on_unavailable] || DEFAULT_ON_UNAVAILABLE
|
35
|
-
end
|
36
|
-
|
37
|
-
def call(env)
|
38
|
-
if enabled_for_recipient?(env)
|
39
|
-
@app.call(env)
|
40
|
-
else
|
41
|
-
fail(env)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def enabled_for_recipient?(env)
|
48
|
-
::Arturo.feature_enabled_for?(@feature, recipient(env))
|
49
|
-
end
|
50
|
-
|
51
|
-
def recipient(env)
|
52
|
-
env[@recipient_key]
|
53
|
-
end
|
54
|
-
|
55
|
-
def fail(env)
|
56
|
-
@on_unavailable.call(env)
|
57
|
-
end
|
58
|
-
|
59
|
-
end
|
60
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module Arturo
|
2
|
-
module RangeFormSupport
|
3
|
-
|
4
|
-
module HelperMethods
|
5
|
-
def range_field(object_name, method, options = {})
|
6
|
-
ActionView::Helpers::InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("range", options)
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
module FormBuilderMethods
|
11
|
-
def range_field(method, options = {})
|
12
|
-
@template.send('range_field', @object_name, method, objectify_options(options))
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
end
|
17
|
-
end
|
data/lib/arturo/test_support.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
Arturo.instance_eval do
|
2
|
-
|
3
|
-
# Enable a feature; create it if necessary.
|
4
|
-
# For use in testing. Not auto-required on load. To load,
|
5
|
-
#
|
6
|
-
# require 'arturo/test_support'
|
7
|
-
#
|
8
|
-
# @param [Symbol, String] name the feature name
|
9
|
-
def enable_feature!(name)
|
10
|
-
feature = Arturo::Feature.to_feature(name)
|
11
|
-
if feature
|
12
|
-
feature = feature.class.find(feature.id) if feature.frozen?
|
13
|
-
feature.update_attributes(:deployment_percentage => 100)
|
14
|
-
else
|
15
|
-
Arturo::Feature.create!(:symbol => name, :deployment_percentage => 100)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# Disable a feature if it exists.
|
20
|
-
# For use in testing. Not auto-required on load. To load,
|
21
|
-
#
|
22
|
-
# require 'arturo/test_support'
|
23
|
-
#
|
24
|
-
# @param [Symbol, String] name the feature name
|
25
|
-
def disable_feature!(name)
|
26
|
-
if (feature = Arturo::Feature.to_feature(name))
|
27
|
-
feature = feature.class.find(feature.id) if feature.frozen?
|
28
|
-
feature.update_attributes(:deployment_percentage => 0)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
require 'rails_generator'
|
2
|
-
|
3
|
-
class ArturoGenerator < Rails::Generator::Base
|
4
|
-
def manifest
|
5
|
-
record do |m|
|
6
|
-
m.file 'initializer.rb', 'config/initializers/arturo_initializer.rb'
|
7
|
-
m.file 'arturo.css', 'public/stylesheets/arturo.css', :collision => :force
|
8
|
-
m.file 'arturo_customizations.css', 'public/stylesheets/arturo_customizations.css', :collision => :skip
|
9
|
-
m.file 'arturo.js', 'public/javascripts/arturo.js'
|
10
|
-
m.file 'semicolon.png', 'public/images/semicolon.png'
|
11
|
-
m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => 'create_features'
|
12
|
-
add_feature_routes(m)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
protected
|
17
|
-
|
18
|
-
def source_root
|
19
|
-
File.expand_path('../templates', __FILE__)
|
20
|
-
end
|
21
|
-
|
22
|
-
def banner
|
23
|
-
%{Usage: #{$0} #{spec.name}\nCopies an initializer; copies CSS, JS, and PNG assets; generates a migration; adds routes/}
|
24
|
-
end
|
25
|
-
|
26
|
-
def add_feature_routes(manifest)
|
27
|
-
sentinel = 'ActionController::Routing::Routes.draw do |map|'
|
28
|
-
logger.route "map.resources features"
|
29
|
-
unless options[:pretend]
|
30
|
-
manifest.gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
|
31
|
-
"#{match}#{feature_routes}\n"
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def feature_routes
|
37
|
-
"\n map.resources :features, :controller => 'arturo/features'" +
|
38
|
-
"\n map.features 'features', :controller => 'arturo/features', :action => 'update_all', :conditions => { :method => :put }"
|
39
|
-
end
|
40
|
-
end
|
data/rails/init.rb
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
if ActiveSupport::Dependencies.respond_to?(:autoload_once_paths)
|
2
|
-
# 2.3.9+
|
3
|
-
ActiveSupport::Dependencies.autoload_once_paths << lib_path
|
4
|
-
elsif ActiveSupport::Dependencies.respond_to?(:load_once_paths)
|
5
|
-
# 2.3.8-
|
6
|
-
ActiveSupport::Dependencies.load_once_paths << lib_path
|
7
|
-
end
|