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
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZWUxYWViNjAzYTQ0MmY5NWVmNTc3YTY2ODM3ZTNjZDQwOTNkNzQ1Yg==
5
+ data.tar.gz: !binary |-
6
+ YWUzZmMyNzkxYmI2OTc0ZjI2Mjg1MDFiZjE1NGRiMmYyODBhYjFkZg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ODMxZGE3ZDM3ZDllMmYwOGViYzZiOTIzZTQwMzk0NTQ5N2Y4YTk2YjJiMmRi
10
+ OWVlMTdiYTBjOGJkMGMyYWE3MjZmM2I1YTFkNDY3ZWQ2ZmIyNzZmZGFkYmY1
11
+ MjUyZjBhZTBkMzAxYjI0MGQwNmE2M2QzNWRmNjMxNWMxYmI1ODQ=
12
+ data.tar.gz: !binary |-
13
+ MGZhOGExYjc5NjgyNjY0YjU2ZGQyMjM0NjllY2EwOGVmN2RkZTNlMGU4NGIy
14
+ NDhjYmI2ZGUyZGVlNTQzYzcyOTI4ZDEwNjQ2OTM4NjBhZWIzYTgzYjM3MDQx
15
+ MzRlNDZiMzRhNWJmOGUxOTRhMWJkMTQzYTk2OTQzYmJiOGRlZmQ=
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .log
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ cache: bundler
2
+ language:
3
+ - ruby
4
+ install:
5
+ - 'bundle install'
6
+ rvm:
7
+ - 1.9.3
8
+ - 2.0.0
data/Appraisals ADDED
@@ -0,0 +1,11 @@
1
+ if RUBY_VERSION >= '2.0'
2
+ rails_versions = ['~> 3.2.13', '~> 4.0.0']
3
+ else
4
+ rails_versions = ['~> 3.1.12', '~> 3.2.13']
5
+ end
6
+
7
+ rails_versions.each do |rails_version|
8
+ appraise "rails#{rails_version.slice(/\d+\.\d+/)}" do
9
+ gem 'rails', rails_version
10
+ end
11
+ end
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bucket_maker.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Dinesh Vasudevan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # BucketMaker
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/bucket_maker.png)](http://rubygems.org/gems/bucket_maker)
4
+ [![Code Climate](https://codeclimate.com/github/dinks/bucket_maker.png)](https://codeclimate.com/github/dinks/bucket_maker)
5
+ [![Build Status](https://travis-ci.org/dinks/bucket_maker.png?branch=master)](https://travis-ci.org/dinks/bucket_maker)
6
+ [![Coverage Status](https://coveralls.io/repos/dinks/bucket_maker/badge.png)](https://coveralls.io/r/dinks/bucket_maker)
7
+
8
+ A Gem to categorize Objects into buckets. Typical use case is an A/B test for Users
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'bucket_maker'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install bucket_maker
23
+
24
+ Generate the `initializer` by running
25
+
26
+ rails g bucket_maker:install
27
+
28
+ This will create a file `config/bucket_maker.rb` with the configuration options
29
+
30
+ The `Series/Bucket/Group` is common for all the `Bucketable` objects
31
+
32
+ We define the buckets in a yml file and add this to the configuration file
33
+ Please look at `spec/dummy/config/buckets.yml`
34
+
35
+ To create a model (if does not exist) and populate the model with the bucket_maker module inclusion, run
36
+
37
+ rails g bucket_maker MODEL
38
+
39
+ Where `MODEL` could be say `User`
40
+
41
+ This will add the code to the *top* of the class
42
+
43
+ include BucketMaker::Models::Redisable
44
+
45
+ By default it uses redis to store the keys and groups
46
+
47
+ To make use of ActiveRecord to hold the key and group, use the generator this way
48
+
49
+ rails g bucket_maker user active_record
50
+
51
+ This will add the code to the *top* of the class
52
+
53
+ include BucketMaker::Models::ActiveRecordable
54
+
55
+ ## Usage
56
+
57
+ Every class which includes `BucketMaker::Models::` should implement 2 methods
58
+
59
+ group_for_key(series_key)
60
+
61
+ and
62
+
63
+ set_group_for_key(series_key, group_name)
64
+
65
+ These methods only define the way to retrieve and store data
66
+
67
+ Look at `BucketMaker::Models::Bucketable` for more info
68
+
69
+ ### Methods
70
+
71
+ in_bucket?(series_name, bucket_name, group_name)
72
+
73
+ To check if a `Bucketable` object is in the group `group_name` under a bucket `bucket_name`
74
+ under a series `series_name`
75
+
76
+ not_in_bucket?(series_name, bucket_name, group_name)
77
+
78
+ Inverse of `in_bucket?`
79
+
80
+ force_to_bucket!(series_name, bucket_name, group_name)
81
+
82
+ Force the `Bucketable` to take the `group_name` under `bucket_name` under `series_name`
83
+
84
+ bucketize!
85
+
86
+ Randomize the `Bucketable` object for all possible combinations
87
+
88
+ bucketize_for_series_and_bucket!(series_name, bucket_name)
89
+
90
+ Randomize the `Bucketable` object for `bucket_name` under `series_name`
91
+
92
+ ### Routes and Controller
93
+
94
+ There are 3 routes that the Gem tries to add. This is added *ONLY* if the configuration has the
95
+ `path_prefix` option set to something other than `nil`. By default the routes are loaded
96
+
97
+ All the results got are with respect to the `Bucketable` object `User`. The gem assumes that this
98
+ request is a part of a A/B Testing procedure and expects `@current_user` to be set accordingly
99
+
100
+ * GET to `/#{path_prefix}:series_name/:bucket_name/:group_name` is used to see if `@current_user` is
101
+ in the group `group_name` under `bucket_name` under `series_name`
102
+
103
+ * POST to `/#{path_prefix}:series_name/:bucket_name` is used group the `@current_user` into a group which
104
+ is under `bucket_name` under `series_name`
105
+
106
+ * POST to `/#{path_prefix}:series_name/:bucket_name/:group_name` is used force group the `@current_user`
107
+ into the group `group_name` under `bucket_name` under `series_name`
108
+
109
+ ## Contributing
110
+
111
+ 1. Fork it
112
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
113
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
114
+ 4. Push to the branch (`git push origin my-new-feature`)
115
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'bundler/gem_tasks'
4
+
5
+ require 'rake'
6
+ require 'rspec/core/rake_task'
7
+ require 'appraisal'
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ desc 'Default'
12
+ task :default => [:all]
13
+
14
+ desc 'Test the engine under all supported Rails versions'
15
+ task all: ['appraisal:install'] do |t|
16
+ exec 'rake appraisal spec'
17
+ end
@@ -0,0 +1,32 @@
1
+ class BucketMakerController < ApplicationController
2
+ include BucketMakerConcern
3
+
4
+ before_filter :ensure_valid_series_bucket_group, only: [:show, :switch]
5
+ before_filter :ensure_valid_series_bucket, only: [:randomize]
6
+
7
+ def show
8
+ render text: @current_user.in_bucket?(@series_name, @bucket_name, @group_name)
9
+ end
10
+
11
+ def switch
12
+ render text: @current_user.force_to_bucket!(@series_name, @bucket_name, @group_name)
13
+ end
14
+
15
+ def randomize
16
+ render text: @current_user.bucketize_for_series_and_bucket!(@series_name, @bucket_name)
17
+ end
18
+
19
+ private
20
+ def ensure_valid_series_bucket_group
21
+ not_found unless BucketMaker.buckets_configuration.has_group_in_bucket_in_series?(@series_name, @bucket_name, @group_name)
22
+ end
23
+
24
+ def ensure_valid_series_bucket
25
+ not_found unless BucketMaker.buckets_configuration.has_bucket_in_series?(@series_name, @bucket_name)
26
+ end
27
+
28
+ def not_found
29
+ head :not_found
30
+ false
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module BucketMakerConcern
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ layout false
6
+
7
+ before_filter :showable?, :extract_params
8
+
9
+ private
10
+ def showable?
11
+ unless @current_user && BucketMaker.configured?
12
+ head :unauthorized
13
+ false
14
+ end
15
+ end
16
+
17
+ def extract_params
18
+ @series_name = params[:series_name] || ''
19
+ @bucket_name = params[:bucket_name] || ''
20
+ @group_name = params[:group_name] || ''
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bucket_maker/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+
8
+ spec.name = "bucket_maker"
9
+ spec.version = BucketMaker::VERSION
10
+ spec.authors = ["Dinesh Vasudevan"]
11
+ spec.email = ["dinesh.vasudevan@gmail.com"]
12
+ spec.description = %q{ Create Simple A/B categories }
13
+ spec.summary = %q{ A Gem to categorize Objects into buckets. Typical use case is an A/B test for Users }
14
+ spec.homepage = "https://github.com/dinks/bucket_maker"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency "redis"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "activesupport", ">= 3.1.0" # For x.months
27
+ spec.add_development_dependency "activerecord", ">= 3.1.0" # For model test
28
+ spec.add_development_dependency "sqlite3"
29
+ spec.add_development_dependency "appraisal" # Check against different Rails Versions
30
+ spec.add_development_dependency "capybara", "= 2.0.3"
31
+ spec.add_development_dependency "pry-debugger"
32
+ spec.add_development_dependency "rspec-rails"
33
+ spec.add_development_dependency "factory_girl_rails"
34
+ spec.add_development_dependency "mock_redis"
35
+ spec.add_development_dependency "coveralls"
36
+
37
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,14 @@
1
+ Rails.application.routes.draw do
2
+ if BucketMaker.configured? && BucketMaker.load_routes?
3
+
4
+ # Show if the user is in group
5
+ get "/#{BucketMaker.configuration.path_prefix}:series_name/:bucket_name/:group_name", to: 'bucket_maker#show', as: :show_bucket, format: :json
6
+
7
+ # Randomize group
8
+ post "/#{BucketMaker.configuration.path_prefix}:series_name/:bucket_name", to: 'bucket_maker#randomize', as: :randomize_bucket, format: :json
9
+
10
+ # Force Switch group
11
+ post "/#{BucketMaker.configuration.path_prefix}:series_name/:bucket_name/:group_name", to: 'bucket_maker#switch', as: :switch_bucket, format: :json
12
+
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ require 'bucket_maker/version'
2
+ require 'bucket_maker/configuration'
3
+ require 'bucket_maker/models/redisable'
4
+ require 'bucket_maker/models/active_recordable'
5
+
6
+ module BucketMaker
7
+ require 'bucket_maker/engine' if defined?(Rails)
8
+ end
@@ -0,0 +1,65 @@
1
+ module BucketMaker
2
+ class Bucket
3
+ attr_accessor :name,
4
+ :summary,
5
+ :created_after,
6
+ :distributions
7
+
8
+ attr_reader :distributions_percent
9
+
10
+ BUCKET_DESCRIPTION = 'description'
11
+ BUCKET_USER_AFTER = 'created_after'
12
+ BUCKET_DISTRIBUTION = 'distributions'
13
+
14
+ def initialize(name, options={})
15
+ @name = name.to_sym
16
+ @summary = options[BUCKET_DESCRIPTION]
17
+
18
+ @created_after = if options[BUCKET_USER_AFTER]
19
+ DateTime.parse(options[BUCKET_USER_AFTER])
20
+ else
21
+ DateTime.parse("1st Jan 1900")
22
+ end
23
+
24
+ @distributions_percent = nil
25
+ @denominator = 1
26
+ @distributions = options[BUCKET_DISTRIBUTION].inject({}) do |result, (dist_name, dist_options)|
27
+ result[dist_name.to_sym] = dist_options
28
+ result
29
+ end if options[BUCKET_DISTRIBUTION]
30
+
31
+ end
32
+
33
+ def random_group
34
+ # Is set after first randomization
35
+ unless @distributions_percent
36
+ @denominator = @distributions.inject(0) do |result, (_, dist_value)|
37
+ result + dist_value
38
+ end
39
+ @distributions_percent = @distributions.inject({}) do |result, (dist_name, dist_value)|
40
+ result[dist_name.to_sym] = (dist_value * 100.0)/@denominator
41
+ result
42
+ end
43
+
44
+ @distributions_percent.inject(0) do |starter, (dist_name, percent_value)|
45
+ ender = starter + percent_value
46
+ @distributions_percent[dist_name.to_sym] = (starter .. ender)
47
+ ender
48
+ end
49
+ end
50
+
51
+ randomized = rand(@denominator * 100)
52
+ @distributions_percent.find do |_, percent_range|
53
+ percent_range.include?(randomized)
54
+ end.first
55
+ end
56
+
57
+ def is_bucketable?(bucketable)
58
+ bucketable.created_at >= @created_after
59
+ end
60
+
61
+ def has_group?(group_name)
62
+ @distributions[group_name.to_sym] != nil
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,66 @@
1
+ require 'forwardable'
2
+ require 'active_support/core_ext'
3
+ require 'bucket_maker/series_maker'
4
+
5
+ module BucketMaker
6
+ class Configuration
7
+ attr_accessor :redis_options,
8
+ :path_prefix,
9
+ :buckets_config_file,
10
+ :lazy_load,
11
+ :redis_expiration_time
12
+
13
+ attr_reader :buckets_configuration,
14
+ :connection
15
+
16
+ def initialize
17
+ @redis_options = {
18
+ host: 'localhost',
19
+ port: 6379,
20
+ db: 1
21
+ }
22
+
23
+ @redis_expiration_time = 12.months
24
+
25
+ @path_prefix = '2bOrNot2B/'
26
+
27
+ @buckets_config_file = nil
28
+ @buckets_configuration = nil
29
+
30
+ @lazy_load = true
31
+ end
32
+
33
+ def reconfigure!
34
+ if @buckets_config_file
35
+ @buckets_configuration = BucketMaker::SeriesMaker.instance
36
+ @buckets_configuration.make!(@buckets_config_file)
37
+ end
38
+ end
39
+
40
+ def configured?
41
+ @buckets_configuration && @buckets_configuration.configured?
42
+ end
43
+
44
+ def load_routes?
45
+ @path_prefix != nil
46
+ end
47
+
48
+ end
49
+
50
+ class << self
51
+ extend Forwardable
52
+
53
+ attr_accessor :configuration
54
+
55
+ def_delegators :@configuration, :configured?, :reconfigure!, :buckets_configuration, :load_routes?
56
+ end
57
+
58
+ def self.configure
59
+ if block_given?
60
+ self.configuration ||= Configuration.new
61
+ yield self.configuration
62
+ self.configuration.reconfigure!
63
+ end
64
+ end
65
+
66
+ end