bucket_maker 0.0.1
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 +15 -0
- data/.gitignore +18 -0
- data/.travis.yml +8 -0
- data/Appraisals +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +115 -0
- data/Rakefile +17 -0
- data/app/controllers/bucket_maker_controller.rb +32 -0
- data/app/controllers/concerns/bucket_maker_concern.rb +23 -0
- data/bucket_maker.gemspec +37 -0
- data/config/routes.rb +14 -0
- data/lib/bucket_maker.rb +8 -0
- data/lib/bucket_maker/bucket.rb +65 -0
- data/lib/bucket_maker/configuration.rb +66 -0
- data/lib/bucket_maker/engine.rb +13 -0
- data/lib/bucket_maker/models/active_recordable.rb +35 -0
- data/lib/bucket_maker/models/bucketable.rb +92 -0
- data/lib/bucket_maker/models/redisable.rb +59 -0
- data/lib/bucket_maker/series.rb +48 -0
- data/lib/bucket_maker/series_maker.rb +80 -0
- data/lib/bucket_maker/version.rb +3 -0
- data/lib/generators/active_record/bucket_maker_generator.rb +90 -0
- data/lib/generators/bucket_maker/bucket_maker_generator.rb +15 -0
- data/lib/generators/bucket_maker/install_generator.rb +16 -0
- data/lib/generators/templates/active_recordable_migration.rb +12 -0
- data/lib/generators/templates/bucket_maker.rb +41 -0
- data/spec/controllers/bucket_maker_controller_spec.rb +112 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +16 -0
- data/spec/dummy/app/assets/javascripts/welcome.js.coffee +3 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/assets/stylesheets/welcome.css.scss +3 -0
- data/spec/dummy/app/controllers/application_controller.rb +12 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/controllers/welcome_controller.rb +4 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/helpers/welcome_helper.rb +2 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/user.rb +12 -0
- data/spec/dummy/app/views/layouts/application.html.erb +16 -0
- data/spec/dummy/app/views/welcome/index.html.erb +2 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +16 -0
- data/spec/dummy/config/boot.rb +9 -0
- data/spec/dummy/config/buckets.yml +35 -0
- data/spec/dummy/config/database.yml +6 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/test.rb +31 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/bucket_maker.rb +41 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +2 -0
- data/spec/dummy/db/migrate/20131223144333_create_users.rb +9 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/factories/factories.rb +8 -0
- data/spec/routing/routes_spec.rb +81 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/unit/bucket_spec.rb +57 -0
- data/spec/unit/bucketable_spec.rb +100 -0
- data/spec/unit/series_maker_spec.rb +91 -0
- data/spec/unit/series_spec.rb +64 -0
- metadata +339 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
module BucketMaker
|
2
|
+
class Engine < Rails::Engine
|
3
|
+
initializer 'Require concerns path' do |app|
|
4
|
+
concerns_path = 'app/controllers/concerns'
|
5
|
+
|
6
|
+
unless app.paths.keys.include?(concerns_path)
|
7
|
+
app.paths.add(concerns_path)
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'concerns/bucket_maker_concern'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'bucket_maker/models/bucketable'
|
3
|
+
|
4
|
+
module BucketMaker
|
5
|
+
module Models
|
6
|
+
module ActiveRecordable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
if BucketMaker.configured?
|
11
|
+
send(:include, InstanceMethods)
|
12
|
+
|
13
|
+
has_many :buckets, as: :bucketable
|
14
|
+
|
15
|
+
after_create :bucketize unless BucketMaker.configuration.lazy_load
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
include BucketMaker::Models::Bucketable
|
21
|
+
|
22
|
+
def group_for_key(series_key)
|
23
|
+
if b = buckets.select('group_name').where(series_key: series_key).take
|
24
|
+
b.group_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_group_for_key(series_key, group_name)
|
29
|
+
buckets.where(series_key: series_key, group_name: group_name).first_or_create
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'bucket_maker/series_maker'
|
2
|
+
|
3
|
+
module BucketMaker
|
4
|
+
module Models
|
5
|
+
module Bucketable
|
6
|
+
def in_bucket?(series_name, bucket_name, group_name, force=false)
|
7
|
+
# Get the singleton Series Maker
|
8
|
+
series_maker = BucketMaker::SeriesMaker.instance
|
9
|
+
|
10
|
+
# Check if the parameters are valid
|
11
|
+
if series_maker.bucketable?(self, series_name, bucket_name, group_name)
|
12
|
+
# The Key is generated by the Series Maker
|
13
|
+
series_key = series_maker.key_for_series(self, series_name, bucket_name)
|
14
|
+
|
15
|
+
# Get the Group if created before from Persisted Storage
|
16
|
+
saved_group = group_for_key(series_key)
|
17
|
+
|
18
|
+
# If its a forced operation then set the Group to the parameter given
|
19
|
+
if force
|
20
|
+
set_group_for_key(series_key, group_name) if saved_group != group_name.to_s
|
21
|
+
# Always return true for set
|
22
|
+
true
|
23
|
+
else
|
24
|
+
# Lazy Load ??
|
25
|
+
# If there is no Group then its lazy load/create time !
|
26
|
+
unless saved_group
|
27
|
+
# Get the random group for this instance
|
28
|
+
saved_group = series_maker.bucketize(series_name, bucket_name)
|
29
|
+
# Set the Group in Persisted Storage
|
30
|
+
set_group_for_key(series_key, saved_group)
|
31
|
+
end
|
32
|
+
|
33
|
+
# return if its the same bucket as requested
|
34
|
+
saved_group == group_name.to_s
|
35
|
+
end
|
36
|
+
else
|
37
|
+
# return false if invalid parameters
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def not_in_bucket?(series_name, bucket_name, group_name)
|
43
|
+
!in_bucket?(series_name, bucket_name, group_name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def force_to_bucket!(series_name, bucket_name, group_name)
|
47
|
+
# Forcefully place inside the bucket
|
48
|
+
in_bucket?(series_name, bucket_name, group_name, true)
|
49
|
+
end
|
50
|
+
|
51
|
+
def bucketize!
|
52
|
+
# Take each series and bucket
|
53
|
+
BucketMaker::SeriesMaker.instance.for_each_series_with_bucketable do |series_maker, series_name, bucket_name|
|
54
|
+
# Randomize
|
55
|
+
group_name = series_maker.bucketize(series_name, bucket_name)
|
56
|
+
# Get the series key
|
57
|
+
series_key = series_maker.key_for_series(self, series_name, bucket_name)
|
58
|
+
# Set the key
|
59
|
+
set_group_for_key(series_key, group_name)
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def bucketize_for_series_and_bucket!(series_name, bucket_name)
|
65
|
+
# Get the singleton Series Maker
|
66
|
+
series_maker = BucketMaker::SeriesMaker.instance
|
67
|
+
if series_maker.has_bucket_in_series?(series_name, bucket_name)
|
68
|
+
# Randomize
|
69
|
+
group_name = series_maker.bucketize(series_name, bucket_name)
|
70
|
+
# Get the series key
|
71
|
+
series_key = series_maker.key_for_series(self, series_name, bucket_name)
|
72
|
+
# Set the key
|
73
|
+
set_group_for_key(series_key, group_name)
|
74
|
+
true
|
75
|
+
else
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def group_for_key(series_key)
|
81
|
+
raise NotImplementedError, "Implement group_for_key"
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_group_for_key(series_key, group_name)
|
85
|
+
raise NotImplementedError, "Implement set_group_for_key"
|
86
|
+
end
|
87
|
+
|
88
|
+
private :group_for_key, :set_group_for_key
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'bucket_maker/models/bucketable'
|
3
|
+
|
4
|
+
module BucketMaker
|
5
|
+
module Models
|
6
|
+
module Redisable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
if BucketMaker.configured?
|
11
|
+
extend ClassMethods
|
12
|
+
|
13
|
+
send(:include, InstanceMethods)
|
14
|
+
|
15
|
+
after_create :bucketize unless BucketMaker.configuration.lazy_load
|
16
|
+
restart_for_disconnect_in_passenger
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module InstanceMethods
|
21
|
+
include BucketMaker::Models::Bucketable
|
22
|
+
|
23
|
+
def group_for_key(series_key)
|
24
|
+
self.class.redis.get(series_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_group_for_key(series_key, group_name)
|
28
|
+
self.class.redis.setex(series_key, BucketMaker.configuration.redis_expiration_time, group_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
|
34
|
+
def redis
|
35
|
+
@redis || connect_redis
|
36
|
+
end
|
37
|
+
|
38
|
+
def connect_redis
|
39
|
+
disconnect_redis
|
40
|
+
@redis ||= Redis.new(BucketMaker.configuration.redis_options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def disconnect_redis
|
44
|
+
@redis.disconnect if @redis
|
45
|
+
ensure
|
46
|
+
@redis = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def restart_for_disconnect_in_passenger
|
50
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
51
|
+
disconnect_redis if forked # We're in smart spawning mode
|
52
|
+
end if defined?(PhusionPassenger)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module BucketMaker
|
2
|
+
class Series
|
3
|
+
attr_reader :name,
|
4
|
+
:description,
|
5
|
+
:created_after,
|
6
|
+
:buckets
|
7
|
+
|
8
|
+
SERIES_DESCRIPTION = 'description'
|
9
|
+
SERIES_USER_AFTER = 'created_after'
|
10
|
+
SERIES_BUCKETS = 'buckets'
|
11
|
+
|
12
|
+
def initialize(name, options={})
|
13
|
+
@name = name.to_sym
|
14
|
+
@description = options[SERIES_DESCRIPTION] || ''
|
15
|
+
@created_after = if options[SERIES_USER_AFTER]
|
16
|
+
DateTime.parse(options[SERIES_USER_AFTER])
|
17
|
+
else
|
18
|
+
DateTime.parse("1st Jan 1900")
|
19
|
+
end
|
20
|
+
@buckets = options[SERIES_BUCKETS].inject({}) do |result, (bucket_name, bucket_options)|
|
21
|
+
result[bucket_name.to_sym] = BucketMaker::Bucket.new(bucket_name, bucket_options)
|
22
|
+
result
|
23
|
+
end if options[SERIES_BUCKETS]
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_bucket
|
27
|
+
@buckets.each do |bucket_name, bucket|
|
28
|
+
yield bucket_name, bucket
|
29
|
+
end if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def bucket_with_name(bucket_name)
|
33
|
+
@buckets[bucket_name.to_sym]
|
34
|
+
end
|
35
|
+
|
36
|
+
def is_bucketable?(bucketable)
|
37
|
+
bucketable.created_at >= @created_after
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_bucket?(bucket_name)
|
41
|
+
@buckets[bucket_name.to_sym] != nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_group_in_bucket?(bucket_name, group_name)
|
45
|
+
has_bucket?(bucket_name) && @buckets[bucket_name.to_sym].has_group?(group_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
require 'bucket_maker/series'
|
4
|
+
require 'bucket_maker/bucket'
|
5
|
+
|
6
|
+
module BucketMaker
|
7
|
+
class SeriesMaker
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
attr_accessor :series
|
11
|
+
|
12
|
+
attr_reader :configuration
|
13
|
+
|
14
|
+
BUCKET_ROOT = 'series'
|
15
|
+
|
16
|
+
def make!(config)
|
17
|
+
@series = []
|
18
|
+
|
19
|
+
absolute_config_path = Rails.root + config
|
20
|
+
|
21
|
+
if File.exists?(absolute_config_path)
|
22
|
+
@configuration = YAML.load_file(absolute_config_path)
|
23
|
+
|
24
|
+
@series = @configuration[BUCKET_ROOT].inject({}) do |result, (series_name, series_options)|
|
25
|
+
result[series_name.to_sym] = BucketMaker::Series.new(series_name, series_options)
|
26
|
+
result
|
27
|
+
end if @configuration && @configuration[BUCKET_ROOT]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def configured?
|
32
|
+
@configuration != nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def for_each_series_with_bucketable
|
36
|
+
@series.collect do |series_name, series|
|
37
|
+
series.each_bucket do |bucket_name, bucket|
|
38
|
+
yield self, series_name, bucket_name
|
39
|
+
end
|
40
|
+
end.inject(true) {|result, value| result && value } if block_given?
|
41
|
+
end
|
42
|
+
|
43
|
+
def series_with_name(series_name)
|
44
|
+
@series[series_name.to_sym]
|
45
|
+
end
|
46
|
+
|
47
|
+
def has_series?(series_name)
|
48
|
+
@series[series_name.to_sym] != nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def has_bucket_in_series?(series_name, bucket_name)
|
52
|
+
has_series?(series_name) && series_with_name(series_name).has_bucket?(bucket_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def has_group_in_bucket_in_series?(series_name, bucket_name, group_name)
|
56
|
+
has_bucket_in_series?(series_name, bucket_name) &&
|
57
|
+
series_with_name(series_name).bucket_with_name(bucket_name).has_group?(group_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def key_for_series(bucketable, series_name, bucket_name)
|
61
|
+
"bucket_maker:#{bucketable.class.to_s.underscore}_#{bucketable.id}:#{series_name.to_s}:#{bucket_name.to_s}" if bucketable && bucketable.id
|
62
|
+
end
|
63
|
+
|
64
|
+
def bucketize(series_name, bucket_name)
|
65
|
+
series_with_name(series_name).bucket_with_name(bucket_name).random_group
|
66
|
+
end
|
67
|
+
|
68
|
+
def bucketable?(bucketable, series_name, bucket_name, group_name)
|
69
|
+
if bucketable.has_attribute?(:created_at) &&
|
70
|
+
has_group_in_bucket_in_series?(series_name, bucket_name, group_name)
|
71
|
+
|
72
|
+
series_with_name(series_name).bucket_with_name(bucket_name).is_bucketable?(bucketable) ||
|
73
|
+
series_with_name(series_name).is_bucketable?(bucketable)
|
74
|
+
|
75
|
+
else
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Generators
|
5
|
+
class BucketMakerGenerator < ActiveRecord::Generators::Base
|
6
|
+
|
7
|
+
ACTIVE_RECORDABLE = 'bucket'
|
8
|
+
|
9
|
+
source_root File.expand_path("../../templates", __FILE__)
|
10
|
+
argument :store_in, type: :string, default: 'redis'
|
11
|
+
|
12
|
+
def generate_model
|
13
|
+
invoke "active_record:model", [name], :migration => false unless model_exists? && behavior == :invoke
|
14
|
+
if store_in == 'active_record'
|
15
|
+
invoke "active_record:model", [ACTIVE_RECORDABLE], :migration => false unless active_recordable_exists? && behavior == :invoke
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def inject_bucket_maker_content
|
20
|
+
contents = <<-CONTENT.strip_heredoc
|
21
|
+
# Module Inclusion for BucketMaker
|
22
|
+
# The options are
|
23
|
+
# - BucketMaker::Models::Redisable
|
24
|
+
#
|
25
|
+
include #{module_name_for_include}
|
26
|
+
|
27
|
+
CONTENT
|
28
|
+
|
29
|
+
klass_path = if namespaced?
|
30
|
+
class_name.to_s.split("::")
|
31
|
+
else
|
32
|
+
[class_name]
|
33
|
+
end
|
34
|
+
|
35
|
+
indent_depth = klass_path.size
|
36
|
+
contents = contents.split("\n").map { |line| " " * indent_depth + line } .join("\n") << "\n\n"
|
37
|
+
|
38
|
+
inject_into_class(model_path, klass_path.last, contents) if model_exists?
|
39
|
+
|
40
|
+
inject_for_active_recordable if store_in == 'active_record' && active_recordable_exists?
|
41
|
+
end
|
42
|
+
|
43
|
+
def inject_for_active_recordable
|
44
|
+
ar_contents = <<-CONTENT.strip_heredoc
|
45
|
+
# Association with the bucket
|
46
|
+
belongs_to :bucketable, polymorphic: true
|
47
|
+
CONTENT
|
48
|
+
|
49
|
+
ar_contents = ar_contents.split("\n").map { |line| " " + line } .join("\n") << "\n\n"
|
50
|
+
|
51
|
+
inject_into_class(active_recordable_path, ACTIVE_RECORDABLE.camelize, ar_contents)
|
52
|
+
end
|
53
|
+
|
54
|
+
def copy_bucket_maker_migration
|
55
|
+
if behavior == :invoke && store_in == 'active_record' && active_recordable_exists?
|
56
|
+
migration_template "active_recordable_migration.rb", "db/migrate/create_#{ACTIVE_RECORDABLE.pluralize}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def model_exists?
|
63
|
+
File.exists?(File.join(destination_root, model_path))
|
64
|
+
end
|
65
|
+
|
66
|
+
def active_recordable_exists?
|
67
|
+
File.exists?(File.join(destination_root, active_recordable_path))
|
68
|
+
end
|
69
|
+
|
70
|
+
def active_recordable_path
|
71
|
+
@ar_path ||= File.join("app", "models", "#{ACTIVE_RECORDABLE}.rb")
|
72
|
+
end
|
73
|
+
|
74
|
+
def model_path
|
75
|
+
@model_path ||= File.join("app", "models", "#{file_path}.rb")
|
76
|
+
end
|
77
|
+
|
78
|
+
def module_name_for_include
|
79
|
+
case store_in
|
80
|
+
when 'redis'
|
81
|
+
'BucketMaker::Models::Redisable'
|
82
|
+
when 'active_record'
|
83
|
+
'BucketMaker::Models::ActiveRecordable'
|
84
|
+
else
|
85
|
+
'BucketMaker::Models::Redisable'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rails/generators/named_base'
|
2
|
+
|
3
|
+
module BucketMaker
|
4
|
+
module Generators
|
5
|
+
class BucketMakerGenerator < Rails::Generators::NamedBase
|
6
|
+
|
7
|
+
namespace 'bucket_maker'
|
8
|
+
source_root File.expand_path("../../templates", __FILE__)
|
9
|
+
|
10
|
+
desc "Generates the model with a given NAME (if it doesn't exist) and adds the hook for method calls"
|
11
|
+
|
12
|
+
hook_for :orm
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|