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.
Files changed (71) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +8 -0
  4. data/Appraisals +11 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +115 -0
  8. data/Rakefile +17 -0
  9. data/app/controllers/bucket_maker_controller.rb +32 -0
  10. data/app/controllers/concerns/bucket_maker_concern.rb +23 -0
  11. data/bucket_maker.gemspec +37 -0
  12. data/config/routes.rb +14 -0
  13. data/lib/bucket_maker.rb +8 -0
  14. data/lib/bucket_maker/bucket.rb +65 -0
  15. data/lib/bucket_maker/configuration.rb +66 -0
  16. data/lib/bucket_maker/engine.rb +13 -0
  17. data/lib/bucket_maker/models/active_recordable.rb +35 -0
  18. data/lib/bucket_maker/models/bucketable.rb +92 -0
  19. data/lib/bucket_maker/models/redisable.rb +59 -0
  20. data/lib/bucket_maker/series.rb +48 -0
  21. data/lib/bucket_maker/series_maker.rb +80 -0
  22. data/lib/bucket_maker/version.rb +3 -0
  23. data/lib/generators/active_record/bucket_maker_generator.rb +90 -0
  24. data/lib/generators/bucket_maker/bucket_maker_generator.rb +15 -0
  25. data/lib/generators/bucket_maker/install_generator.rb +16 -0
  26. data/lib/generators/templates/active_recordable_migration.rb +12 -0
  27. data/lib/generators/templates/bucket_maker.rb +41 -0
  28. data/spec/controllers/bucket_maker_controller_spec.rb +112 -0
  29. data/spec/dummy/Rakefile +7 -0
  30. data/spec/dummy/app/assets/images/.keep +0 -0
  31. data/spec/dummy/app/assets/javascripts/application.js +16 -0
  32. data/spec/dummy/app/assets/javascripts/welcome.js.coffee +3 -0
  33. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  34. data/spec/dummy/app/assets/stylesheets/welcome.css.scss +3 -0
  35. data/spec/dummy/app/controllers/application_controller.rb +12 -0
  36. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  37. data/spec/dummy/app/controllers/welcome_controller.rb +4 -0
  38. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  39. data/spec/dummy/app/helpers/welcome_helper.rb +2 -0
  40. data/spec/dummy/app/models/.keep +0 -0
  41. data/spec/dummy/app/models/concerns/.keep +0 -0
  42. data/spec/dummy/app/models/user.rb +12 -0
  43. data/spec/dummy/app/views/layouts/application.html.erb +16 -0
  44. data/spec/dummy/app/views/welcome/index.html.erb +2 -0
  45. data/spec/dummy/config.ru +4 -0
  46. data/spec/dummy/config/application.rb +16 -0
  47. data/spec/dummy/config/boot.rb +9 -0
  48. data/spec/dummy/config/buckets.yml +35 -0
  49. data/spec/dummy/config/database.yml +6 -0
  50. data/spec/dummy/config/environment.rb +5 -0
  51. data/spec/dummy/config/environments/test.rb +31 -0
  52. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  53. data/spec/dummy/config/initializers/bucket_maker.rb +41 -0
  54. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  55. data/spec/dummy/config/initializers/inflections.rb +16 -0
  56. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  57. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  58. data/spec/dummy/config/initializers/session_store.rb +3 -0
  59. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  60. data/spec/dummy/config/locales/en.yml +23 -0
  61. data/spec/dummy/config/routes.rb +2 -0
  62. data/spec/dummy/db/migrate/20131223144333_create_users.rb +9 -0
  63. data/spec/dummy/db/test.sqlite3 +0 -0
  64. data/spec/factories/factories.rb +8 -0
  65. data/spec/routing/routes_spec.rb +81 -0
  66. data/spec/spec_helper.rb +24 -0
  67. data/spec/unit/bucket_spec.rb +57 -0
  68. data/spec/unit/bucketable_spec.rb +100 -0
  69. data/spec/unit/series_maker_spec.rb +91 -0
  70. data/spec/unit/series_spec.rb +64 -0
  71. 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,3 @@
1
+ module BucketMaker
2
+ VERSION = "0.0.1"
3
+ 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