arturo 0.2.3.8 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|