planify 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/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ .ruby-gemset
2
+ .ruby-version
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ .rspec
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ services:
2
+ - mongodb
3
+ language: ruby
4
+ rvm:
5
+ - 2.0.0
6
+ - 1.9.3
7
+ - jruby-19mode
8
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ gem "rake"
5
+
6
+ group :test do
7
+ gem "rspec", "~> 2.14"
8
+ gem "pry"
9
+ gem "simplecov", require: false
10
+
11
+ if ENV["CI"]
12
+ gem "coveralls", require: false
13
+ end
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Kyle Dayton
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,158 @@
1
+ Planify [![Build Status](https://secure.travis-ci.org/maildropr/planify.png?branch=master)](http://travis-ci.org/maildropr/planify) [![Code Climate](https://codeclimate.com/github/maildropr/planify.png)](https://codeclimate.com/github/maildropr/planify) [![Coverage Status](https://coveralls.io/repos/maildropr/planify/badge.png)](https://coveralls.io/r/maildropr/planify)
2
+ ========
3
+
4
+ Make subscription plans and enforce their limits with Planify.
5
+
6
+ ## Requirements
7
+
8
+ Ruby:
9
+ * 1.9.3
10
+ * 2.0.0
11
+ * JRuby (1.9 mode)
12
+ * Rubinius (1.9 mode)
13
+
14
+ Mongoid 3 (Support for other ORMs will be a future improvement)
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ gem 'planify'
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install planify
29
+
30
+ ## Setup
31
+
32
+ ### Limitables
33
+ Limitables are classes which can be limited based on plan settings. To create a limitable, include the `Planify::Limitable` mixin in your class.
34
+
35
+ ```ruby
36
+ # app/models/widget.rb
37
+ class Widget
38
+ include Mongoid::Document
39
+ include Planify::Limitable
40
+ ...
41
+ end
42
+ ```
43
+
44
+ ### Plans
45
+ Plans hold information about how many instances of a `Limitable` can be created, as well as which features are available to users subscribed to this plan:
46
+
47
+ ```ruby
48
+ # config/initializers/plans.rb
49
+ Planify::Plans.define :starter do
50
+ max Widget, 100 # Can only create up to 100 widgets before needing to upgrade
51
+
52
+ feature :ajax_search
53
+ end
54
+ ```
55
+
56
+ ### Users
57
+ Add the `Planify::User` mixin to your user class. This will keep track of how many limitables the user has created, as well as their plan and plan overrides:
58
+
59
+ ```ruby
60
+ class User
61
+ include Mongoid::Document
62
+ include Planify::User
63
+ ...
64
+ end
65
+ ```
66
+
67
+ Then assign the user a plan:
68
+
69
+ ```ruby
70
+ @user = User.create
71
+ @user.has_plan :starter
72
+ ```
73
+
74
+ You can also assign user-specific overrides to plan limits and features:
75
+
76
+ ```ruby
77
+ # This user has half the widgets and no ajax-search
78
+ @user.has_plan :starter do
79
+ max Widget, 50
80
+ feature :ajax_search, false
81
+ end
82
+ ```
83
+
84
+ ## Usage
85
+
86
+ After creating your Limitables, Plans, and User, you are ready to start enforcing limits.
87
+
88
+ ```ruby
89
+ # widgets_controller.rb
90
+
91
+ def create
92
+ @user = current_user
93
+ @widget = Widget.create(params[:widget])
94
+
95
+ if @user.can_create? @widget # User has not hit their Widget cap
96
+ @widget.save
97
+ @user.created :widget
98
+ end
99
+ end
100
+
101
+ def destroy
102
+ @user = current_user
103
+ @widget = Widget.find(params[:id])
104
+
105
+ @widget.destroy
106
+ @user.destroyed @widget
107
+ end
108
+ ```
109
+
110
+ You can also test for features:
111
+
112
+ ```ruby
113
+ # _nav.haml
114
+
115
+ -if current_user.has_feature? :ajax_search
116
+ =ajax_search_form
117
+ ```
118
+
119
+ ## Rails Integration
120
+
121
+ When used inside a Rails project, Planify automatically adds two methods to your controllers: `enforce_limit!` and `limit_exceeded!`. `enforce_limit!` will call `limit_exceeded!` if the user is over their limit.
122
+
123
+ ```ruby
124
+ # app/controllers/widget_controller.rb
125
+ class WidgetController < ApplicationController
126
+ before_filter :enforce_widget_limit, only: [:new, :create]
127
+
128
+ ...
129
+
130
+ private
131
+
132
+ def enforce_widget_limit
133
+ # If the user's Widget limit is exceeded, limit_exceeded! will be called
134
+ enforce_limit! current_user, Widget
135
+ end
136
+ end
137
+ ```
138
+
139
+ The default behavior of `limit_exceeded!` is to raise an Exception. You can change this behavior by creating your own `limit_exceeded!` method in your `ApplicationController`.
140
+
141
+ ```ruby
142
+ # app/controllers/application_controller.rb
143
+ class ApplicationController < ActionController::Base
144
+ def limit_exceeded!
145
+ redirect_to upgrade_plan_url, notice: "You must upgrade your account!"
146
+ end
147
+ end
148
+ ```
149
+
150
+
151
+ ## Contributing
152
+
153
+ 1. Fork it
154
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
155
+ 3. Add some specs (so I don't accidentally break your feature in the future)
156
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
157
+ 5. Push to the branch (`git push origin my-new-feature`)
158
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/lib/planify.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "mongoid"
2
+ require "active_support"
3
+
4
+ require "planify/limitations"
5
+ require "planify/plan"
6
+ require "planify/plans"
7
+ require "planify/user"
8
+
9
+ require "planify/railtie" if defined?(Rails)
10
+
11
+ module Planify
12
+ module Limitable; end
13
+ end
@@ -0,0 +1,26 @@
1
+ module Planify
2
+ module Integrations
3
+
4
+ module Rails
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ extend Helpers
9
+ include Helpers
10
+ helper Helpers
11
+ end
12
+ end
13
+
14
+ module Helpers
15
+ def limit_exceeded!
16
+ raise "Limit exceeded"
17
+ end
18
+
19
+ def enforce_limit!(user, limitable)
20
+ limit_exceeded! unless user.can_create? limitable
21
+ end
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ require "planify/util/class_helper"
2
+
3
+ module Planify
4
+
5
+ # +Planify::Limitations+ is a simple container for Class constants and their associated limits
6
+ class Limitations
7
+ include ActiveSupport::Inflector
8
+ include Planify::ClassHelper
9
+
10
+ def initialize(limits = {})
11
+ @limits = limits
12
+ end
13
+
14
+ # Sets the limit for the given class constant
15
+ # @raise [ArgumentError] if +klass+ does not include module +Planify::Limitable+
16
+ # @param [String,Symbol,Class,Object] klass The class to set the limit for
17
+ # @param [Integer] limit The number of instances of +klass+ which can be created
18
+ # @return [Integer] The value of +limit+
19
+ def set(klass, limit)
20
+ key = normalize_class(klass)
21
+ raise ArgumentError unless constantize(key).include?(Planify::Limitable)
22
+ @limits[key] = limit
23
+ end
24
+
25
+ # Fetches the limit for the given class constant
26
+ # @param [String,Symbol,Class,Object] klass The class to find the limit for
27
+ # @param [Integer] default_limit The default value to return, if one is not defined for +klass+
28
+ # @return [Integer] The limit value for +klass+, or +default_limit+ if none exists.
29
+ def get(klass, default_limit = Float::INFINITY)
30
+ key = normalize_class(klass)
31
+
32
+ begin
33
+ @limits.fetch(key)
34
+ rescue
35
+ return default_limit
36
+ end
37
+ end
38
+
39
+ # Returns all limits defined in this instance
40
+ # @return [Hash(String,Integer)] A hash mapping class constant name to it's limit
41
+ def all
42
+ @limits
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,70 @@
1
+ module Planify
2
+
3
+ # The +Planify::Plan+ class provides functionality for storing class creation limits and available features. It also embodies a simple DSL for defining these attributes.
4
+ class Plan
5
+
6
+ attr_reader :features, :limits
7
+
8
+ def initialize(limits = {}, features = {})
9
+ @limits = Limitations.new(limits)
10
+ @features = features
11
+ end
12
+
13
+ # Sets the maximum number of a +Planify::Limitable+ that a user can create on this plan
14
+ # @param [String,Symbol,Class,Object] limitable The class to set the limit of
15
+ # @param [Integer] limit The maxiumum number of +limitable+ that can be created
16
+ def max(limitable, limit)
17
+ @limits.set(limitable, limit)
18
+ end
19
+
20
+ # Gets the plan's limit for a given class constant
21
+ # @param [String,Symbol,Class,Object] limitable The class to get the limit for
22
+ # @return [Integer, Float::INFINITY] The plan limit for +limitable+, if one exists. Otherwise +Float::INFINITY+
23
+ def limit(limitable)
24
+ @limits.get(limitable)
25
+ end
26
+
27
+ def feature(feature_name, enabled = true)
28
+ @features[feature_name.to_sym] = enabled
29
+ end
30
+
31
+ # Boolean method for determining if a certain feature is enabled in this plan
32
+ # @param [String,Symbol] feature the feature to check
33
+ # @return [Boolean] +true+ if +feature+ is enabled, +false+ if +feature+ is disabled or undefined.
34
+ def feature_enabled?(feature)
35
+ @features[feature.to_sym] || false
36
+ end
37
+
38
+ # Boolean method for determining if a certain feature is disabled in this plan
39
+ # @param [String,Symbol] feature the feature to check
40
+ # @return [Boolean] +true+ if +feature+ is disabled or undefined, +false+ if +feature+ is enabled.
41
+ def feature_disabled?(feature)
42
+ !feature_enabled?(feature)
43
+ end
44
+
45
+ # Returns a duplicate instance of this plan
46
+ # @return [Planify::Plan] an exact copy of this plan
47
+ def dup
48
+ duplicate = Plan.new
49
+ duplicate.merge! self
50
+
51
+ duplicate
52
+ end
53
+
54
+ # Merges limits and features from +other_plan+ into self.
55
+ # @param [Planify::Plan] other_plan the plan to merge with
56
+ # @return [nil]
57
+ def merge!(other_plan)
58
+ other_plan.features.each do |f, enabled|
59
+ feature f, enabled
60
+ end
61
+
62
+ other_plan.limits.all.each do |klass, lim|
63
+ max klass, lim
64
+ end
65
+
66
+ nil
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ module Planify
2
+ module Plans
3
+
4
+ @plans = ActiveSupport::HashWithIndifferentAccess.new
5
+
6
+ def self.define(name, &block)
7
+ plan = Plan.new
8
+ plan.instance_eval(&block) if block_given?
9
+
10
+ @plans[name] = plan
11
+ end
12
+
13
+ def self.get(name)
14
+ begin
15
+ @plans.fetch(name)
16
+ rescue
17
+ raise ArgumentError, "A plan named '#{name}' is not defined"
18
+ end
19
+ end
20
+
21
+ def self.all
22
+ @plans
23
+ end
24
+
25
+ def self.clear
26
+ @plans.clear
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ require "planify/integrations/rails"
2
+
3
+ module Planify
4
+ class PlanifyRailtie < Rails::Railtie
5
+ initializer "planify.hooks" do
6
+ ActionController::Base.send(:include, Planify::Integrations::Rails)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,82 @@
1
+ require "planify/user/limitable_counts"
2
+ require "planify/user/plan_info"
3
+
4
+ require "planify/util/class_helper"
5
+
6
+ module Planify
7
+ module User
8
+ include Planify::ClassHelper
9
+
10
+ def self.included(base)
11
+ base.class_eval do
12
+ embeds_one :planify_plan_info, as: :planify_user, class_name: "Planify::User::PlanInfo", validate: false
13
+ embeds_one :planify_limitable_counts, as: :planify_user, class_name: "Planify::User::LimitableCounts", validate: false
14
+ end
15
+ end
16
+
17
+ def has_plan(plan_name, &block)
18
+ plan = Planify::Plans.get(plan_name)
19
+ plan_info = PlanInfo.new(name: plan_name)
20
+
21
+ if block_given?
22
+ plan = plan.dup
23
+ configuration = Planify::Plan.new
24
+ configuration.instance_eval &block
25
+
26
+ plan.merge! configuration
27
+
28
+ plan_info.limit_overrides = configuration.limits.all
29
+ plan_info.feature_overrides = configuration.features
30
+ end
31
+
32
+ self.planify_plan_info = plan_info
33
+ nil
34
+ end
35
+
36
+ def plan
37
+ @plan ||= load_plan_from_info(self.planify_plan_info)
38
+ end
39
+
40
+ def limitable_counts
41
+ self.planify_limitable_counts ||= LimitableCounts.new
42
+ end
43
+
44
+ def creation_count(limitable)
45
+ key = normalize_class(limitable)
46
+ limitable_counts.fetch(key)
47
+ end
48
+
49
+ def created(limitable)
50
+ key = normalize_class(limitable)
51
+ limitable_counts.increment(key)
52
+ end
53
+
54
+ def destroyed(limitable)
55
+ key = normalize_class(limitable)
56
+ limitable_counts.decrement(key)
57
+ end
58
+
59
+ def can_create?(limitable)
60
+ key = normalize_class(limitable)
61
+ limitable_counts.fetch(key) < plan.limit(key)
62
+ end
63
+
64
+ def has_feature?(feature)
65
+ plan.feature_enabled?(feature)
66
+ end
67
+
68
+ private
69
+
70
+ def load_plan_from_info(plan_info)
71
+ return nil unless plan_info
72
+ plan = Planify::Plans.get(plan_info.name).dup
73
+
74
+ if plan_info.has_overrides?
75
+ plan.merge! plan_info.overrides_as_plan
76
+ end
77
+
78
+ return plan
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,23 @@
1
+ module Planify
2
+ module User
3
+
4
+ class LimitableCounts
5
+ include Mongoid::Document
6
+
7
+ embedded_in :planify_user, polymorphic: true
8
+
9
+ def increment(limitable)
10
+ self.inc(limitable, 1)
11
+ end
12
+
13
+ def decrement(limitable)
14
+ self.inc(limitable, -1)
15
+ end
16
+
17
+ def fetch(limitable, default = 0)
18
+ self.attributes[limitable] || default
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ module Planify
2
+ module User
3
+
4
+ class PlanInfo
5
+ include Mongoid::Document
6
+
7
+ embedded_in :planify_user, polymorphic: true
8
+
9
+ field :name, type: String, default: nil
10
+ field :limit_overrides, type: Hash, default: nil
11
+ field :feature_overrides, type: Hash, default: nil
12
+
13
+ def has_overrides?
14
+ limit_overrides.present? || feature_overrides.present?
15
+ end
16
+
17
+ def overrides_as_plan
18
+ Plan.new.tap do |p|
19
+ limit_overrides.try(:each) do |klass, limit|
20
+ p.max klass, limit
21
+ end
22
+
23
+ feature_overrides.try(:each) do |f, enabled|
24
+ p.feature f, enabled
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ module Planify
2
+ module ClassHelper
3
+ include ActiveSupport::Inflector
4
+
5
+ def normalize_class(klass)
6
+ return klass.name if klass.is_a? Module
7
+ return klass.name if klass.respond_to? :new # Class constant
8
+
9
+ if klass.is_a?(String) || klass.is_a?(Symbol)
10
+ computed_class = constantize camelize(klass.to_s)
11
+ computed_class.to_s
12
+ else
13
+ klass.class.name
14
+ end
15
+ end # normalize_class
16
+
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Planify
2
+ VERSION = [1, 0, 0].join(".")
3
+ end
data/planify.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "planify/version"
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "planify"
8
+ gem.version = Planify::VERSION
9
+ gem.authors = ["Kyle Dayton"]
10
+ gem.email = ["kyle@graphicflash.com"]
11
+ gem.homepage = "http://github.com/kdayton-/planify"
12
+
13
+ gem.description = %q{A Mongoid plugin for managing subscription plans and features}
14
+ gem.summary = gem.description
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "mongoid", ">= 3.0.0"
22
+ gem.add_dependency "activesupport"
23
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ sessions:
3
+ default:
4
+ database: planify
5
+ hosts:
6
+ - localhost:27017
@@ -0,0 +1,4 @@
1
+ class Post
2
+ include Mongoid::Document
3
+ include Planify::Limitable
4
+ end
@@ -0,0 +1,6 @@
1
+ class User
2
+ include Mongoid::Document
3
+ include Planify::User
4
+
5
+ field :name, default: "John"
6
+ end
@@ -0,0 +1,85 @@
1
+ require "spec_helper"
2
+
3
+ describe Planify::Limitations do
4
+ describe ".set" do
5
+
6
+ context "when specfied class does not include the Limitable module" do
7
+ it "raises ArgumentError" do
8
+ expect { subject.set(Object, 100) }.to raise_exception(ArgumentError)
9
+ end
10
+ end
11
+
12
+ context "with a class constant" do
13
+ it "sets the limitation for the given class" do
14
+ subject.set(Post, 100)
15
+ subject.get(Post).should eq 100
16
+ end
17
+ end
18
+
19
+ context "with a class instance" do
20
+ it "sets the limitation for the instance class" do
21
+ @post = Post.new
22
+
23
+ subject.set(@post, 100)
24
+ subject.get(Post).should eq 100
25
+ end
26
+ end
27
+
28
+ context "with a module" do
29
+ it "sets the limitation for the module" do
30
+ module Foo; end
31
+ Foo.send(:include, Planify::Limitable)
32
+
33
+ subject.set(Foo, 200)
34
+ subject.get(Foo).should eq 200
35
+ end
36
+ end
37
+
38
+ context "with a string" do
39
+ context "string represents a constant name" do
40
+ it "sets the limitation for the constant" do
41
+ subject.set("Post", 100)
42
+ subject.get(Post).should eq 100
43
+ end
44
+ end
45
+
46
+ context "string does not represent a constant name" do
47
+ it "raises NameError" do
48
+ expect { subject.set("Not a class", 100) }.to raise_exception(NameError)
49
+ end
50
+ end
51
+ end
52
+
53
+ context "with a symbol" do
54
+ context "symbol represents a constant name" do
55
+ it "sets the limitation for the constant" do
56
+ subject.set(:post, 100)
57
+ subject.get(Post).should eq 100
58
+ end
59
+ end
60
+
61
+ context "symbol does not represent a constant name" do
62
+ it "raises NameError" do
63
+ expect { subject.set(:non_class, 100) }.to raise_exception(NameError)
64
+ end
65
+ end
66
+ end
67
+ end # .set
68
+
69
+ describe ".get" do
70
+
71
+ context "when class limit is defined" do
72
+ before { subject.set(:post, 100) }
73
+ it "returns the limit" do
74
+ subject.get(Post).should == 100
75
+ end
76
+ end
77
+
78
+ context "when class limit is undefined" do
79
+ it "returns the default value" do
80
+ subject.get(User, 1).should == 1
81
+ end
82
+ end
83
+
84
+ end # .get
85
+ end
File without changes
@@ -0,0 +1,102 @@
1
+ require "spec_helper"
2
+
3
+ describe Planify::Plan do
4
+ subject { Planify::Plans.get(:starter) }
5
+
6
+ describe ".limit" do
7
+ before { subject.max(Post, 100) }
8
+
9
+ it "returns the max limit for a Limitable class" do
10
+ subject.limit(Post).should eq 100
11
+ end
12
+ end
13
+
14
+ describe ".max" do
15
+ before { subject.max(Post, 5) }
16
+
17
+ it "sets the maximum number of Class allowed" do
18
+ subject.limit(Post).should eq 5
19
+ end
20
+ end
21
+
22
+ describe ".feature" do
23
+ before do
24
+ subject.feature :ajax_search
25
+ subject.feature :live_reload, false
26
+ end
27
+
28
+ it "sets availability of the feature" do
29
+ subject.features.keys.should include :ajax_search, :live_reload
30
+ end
31
+
32
+ it "defaults features to enabled" do
33
+ subject.features[:ajax_search].should be_true
34
+ end
35
+ end
36
+
37
+ describe ".feature_enabled?" do
38
+ before { subject.feature(:ajax_search) }
39
+
40
+ it "returns true if the feature is enabled for this plan" do
41
+ subject.feature_enabled?(:ajax_search).should be_true
42
+ end
43
+
44
+ it "returns false if the feature if explicitly disabled" do
45
+ subject.feature(:ajax_search, false)
46
+ subject.feature_enabled?(:ajax_search).should be_false
47
+ end
48
+
49
+ it "returns false if the feature does not exist" do
50
+ subject.feature_enabled?(:dne_feature).should be_false
51
+ end
52
+ end
53
+
54
+ describe ".feature_disabled?" do
55
+ before { subject.feature(:ajax_search) }
56
+
57
+ it "returns true if the feature is not enabled for this plan" do
58
+ subject.feature_disabled?(:ajax_search).should be_false
59
+ end
60
+
61
+ it "returns true if the feature if explicitly disabled" do
62
+ subject.feature(:ajax_search, false)
63
+ subject.feature_disabled?(:ajax_search).should be_true
64
+ end
65
+
66
+ it "returns true if the feature does not exist" do
67
+ subject.feature_disabled?(:dne_feature).should be_true
68
+ end
69
+
70
+ it "returns false if the feature is enabled" do
71
+ subject.feature_disabled?(:ajax_search).should be_false
72
+ end
73
+ end
74
+
75
+ describe ".dup" do
76
+ it "returns a duplicate plan with the same features and limits" do
77
+ duplicate = subject.dup
78
+
79
+ duplicate.should_not be subject
80
+ duplicate.limits.all.should == subject.limits.all
81
+ duplicate.features.should == subject.features
82
+ end
83
+ end
84
+
85
+ describe ".merge!" do
86
+
87
+ let(:pro_plan) do
88
+ Planify::Plans.define :pro do
89
+ max Post, 1000
90
+ feature :ajax_search
91
+ end
92
+ end
93
+
94
+ it "merges settings in other plan into this plan" do
95
+ subject.merge! pro_plan
96
+
97
+ subject.features.keys.should include :ajax_search
98
+ subject.limit(Post).should == 1000
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+
3
+ describe Planify::Plans do
4
+
5
+ before(:each) { Planify::Plans.clear }
6
+
7
+ describe ".define" do
8
+ it "creates a plan with the given name" do
9
+ plan = Planify::Plans.define :starter
10
+ Planify::Plans.get(:starter).should == plan
11
+ end
12
+
13
+ it "given block should be evaluated on plan" do
14
+ plan = Planify::Plans.define :starter do
15
+ max Post, 10
16
+ end
17
+
18
+ Planify::Plans.get(:starter).limit(Post).should == 10
19
+ end
20
+ end
21
+
22
+ describe ".get" do
23
+ context "with a string" do
24
+ it "will match the plan" do
25
+ Planify::Plans.define :starter
26
+ Planify::Plans.get("starter").should be_an_instance_of Planify::Plan
27
+ end
28
+ end
29
+
30
+ context "when plan is defined" do
31
+ before do
32
+ Planify::Plans.define :starter do
33
+ max Post, 10
34
+ end
35
+ end
36
+
37
+ it "returns the plan" do
38
+ Planify::Plans.get(:starter).limit(Post).should == 10
39
+ end
40
+ end
41
+
42
+ context "when plan is undefined" do
43
+ it "raises an ArgumentError" do
44
+ expect { Planify::Plans.get(:non_existant) }.to raise_exception(ArgumentError)
45
+ end
46
+ end
47
+ end
48
+
49
+ describe ".clear" do
50
+ before { Planify::Plans.define :starter }
51
+
52
+ it "destroys all plans" do
53
+ Planify::Plans.all.should_not be_empty
54
+ Planify::Plans.clear
55
+ Planify::Plans.all.should be_empty
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+
3
+ describe Planify::User::PlanInfo do
4
+ describe ".overrides_as_plan" do
5
+ it "returns an instance of Planify::Plan" do
6
+ subject.overrides_as_plan.should be_an_instance_of Planify::Plan
7
+ end
8
+ end
9
+
10
+ describe ".has_overrides?" do
11
+ it "returns true if limit overrides exist" do
12
+ subject.limit_overrides = {post: 100}
13
+ subject.should have_overrides
14
+ end
15
+
16
+ it "returns true if feature overrides exist" do
17
+ subject.feature_overrides = {ajax_search: false}
18
+ subject.should have_overrides
19
+ end
20
+
21
+ it "returns true if both limit and feature overrides exist" do
22
+ subject.limit_overrides = {post: 100}
23
+ subject.feature_overrides = {ajax_search: false}
24
+ subject.should have_overrides
25
+ end
26
+
27
+ it "returns false if neither limit nor feature overrides exist" do
28
+ subject.should_not have_overrides
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,115 @@
1
+ require "spec_helper"
2
+
3
+ describe Planify::User, focus: false do
4
+ subject { User.new }
5
+ before(:each) do
6
+ Planify::Plans.define :starter do
7
+ max Post, 100
8
+ feature :ajax_search
9
+ end
10
+
11
+ subject.has_plan :starter
12
+ end
13
+
14
+ describe ".has_plan" do
15
+ it "should assign the plan" do
16
+ subject.plan.limit(Post).should == 100
17
+ end
18
+
19
+ it "should persist the plan name" do
20
+ subject.save!
21
+ user = User.find(subject.id)
22
+
23
+ user.plan.limit(Post).should == 100
24
+ end
25
+
26
+ context "with a configuration block" do
27
+ before do
28
+ subject.has_plan :starter do
29
+ max Post, 5
30
+ feature :ajax_search, false
31
+ end
32
+ end
33
+
34
+ it "the configuration should override the plan defaults" do
35
+ subject.plan.limit(Post).should == 5
36
+ end
37
+
38
+ it "should store the configuration change" do
39
+ subject.save!
40
+ user = User.find(subject.id)
41
+
42
+ user.plan.limit(Post).should == 5
43
+ end
44
+
45
+ it "should not change the base plan" do
46
+ Planify::Plans.get(:starter).limit(Post).should == 100
47
+ end
48
+ end
49
+ end
50
+
51
+ describe ".can_create?" do
52
+ before { subject.plan.max(Post, 1) }
53
+
54
+ it "should return true if limit for class is not reached" do
55
+ subject.can_create?(:post).should be_true
56
+ end
57
+
58
+ it "should return false if limit for class is exceeded" do
59
+ subject.created Post
60
+ subject.can_create?(Post).should be_false
61
+ end
62
+ end
63
+
64
+ describe ".has_feature?" do
65
+ it "returns true if the users plan has the given feature enabled" do
66
+ subject.has_feature?(:ajax_search).should be_true
67
+ end
68
+
69
+ it "returns false if the users plan does not have the feature" do
70
+ subject.has_feature?(:dne_feature).should be_false
71
+ end
72
+
73
+ it "returns false if the feature is disabled for the users plan" do
74
+ subject.plan.feature(:ajax_search, false)
75
+ subject.has_feature?(:ajax_search).should be_false
76
+ end
77
+ end
78
+
79
+ describe ".created" do
80
+ it "should increase the creation count for the given limitable" do
81
+ expect { subject.created Post }.to change{ subject.creation_count(Post) }.by(1)
82
+ end
83
+
84
+ it "should persist the change" do
85
+ subject.save!
86
+ subject.created Post
87
+
88
+ user = User.find(subject.id)
89
+ user.creation_count(Post).should == 1
90
+ end
91
+ end
92
+
93
+ describe ".destroyed" do
94
+ before { subject.created Post }
95
+ it "should decrease the creation count for the given limitable" do
96
+ expect { subject.destroyed Post }.to change{ subject.creation_count(Post) }.by(-1)
97
+ end
98
+
99
+ it "should persist the change" do
100
+ subject.save!
101
+ subject.destroyed Post
102
+
103
+ user = User.find(subject.id)
104
+ user.creation_count(Post).should == 0
105
+ end
106
+ end
107
+
108
+ describe ".creation_count" do
109
+ before { subject.created(Post) }
110
+
111
+ it "returns the number of limitable created" do
112
+ subject.creation_count(Post).should == 1
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,39 @@
1
+ $: << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require "simplecov"
4
+
5
+ if ENV["CI"]
6
+ require "coveralls"
7
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
8
+ Coveralls.wear!
9
+ end
10
+
11
+ SimpleCov.start do
12
+ add_filter "spec"
13
+ end
14
+
15
+ require "mongoid"
16
+ require "rspec"
17
+ require "pry"
18
+
19
+ require "planify"
20
+
21
+ Mongoid.load!("spec/config/mongoid.yml", :test)
22
+
23
+ MODELS = File.join(File.dirname(__FILE__), "models")
24
+ Dir[ File.join(MODELS, "*.rb") ].sort.each { |f| require f }
25
+
26
+ RSpec.configure do |config|
27
+ config.treat_symbols_as_metadata_keys_with_true_values = true
28
+ config.run_all_when_everything_filtered = true
29
+ config.filter_run :focus
30
+
31
+ config.order = 'random'
32
+
33
+ config.before :all do
34
+ Planify::Plans.define :starter do
35
+ max Post, 100
36
+ end
37
+ end
38
+
39
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: planify
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kyle Dayton
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongoid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A Mongoid plugin for managing subscription plans and features
47
+ email:
48
+ - kyle@graphicflash.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .coveralls.yml
54
+ - .gitignore
55
+ - .travis.yml
56
+ - Gemfile
57
+ - LICENSE.txt
58
+ - README.md
59
+ - Rakefile
60
+ - lib/planify.rb
61
+ - lib/planify/integrations/rails.rb
62
+ - lib/planify/limitations.rb
63
+ - lib/planify/plan.rb
64
+ - lib/planify/plans.rb
65
+ - lib/planify/railtie.rb
66
+ - lib/planify/user.rb
67
+ - lib/planify/user/limitable_counts.rb
68
+ - lib/planify/user/plan_info.rb
69
+ - lib/planify/util/class_helper.rb
70
+ - lib/planify/version.rb
71
+ - planify.gemspec
72
+ - spec/config/mongoid.yml
73
+ - spec/models/post.rb
74
+ - spec/models/user.rb
75
+ - spec/planify/limitations_spec.rb
76
+ - spec/planify/plan_overrides_spec.rb
77
+ - spec/planify/plan_spec.rb
78
+ - spec/planify/plans_spec.rb
79
+ - spec/planify/user/plan_info_spec.rb
80
+ - spec/planify/user_spec.rb
81
+ - spec/spec_helper.rb
82
+ homepage: http://github.com/kdayton-/planify
83
+ licenses: []
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ segments:
95
+ - 0
96
+ hash: 654973981249883969
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ segments:
104
+ - 0
105
+ hash: 654973981249883969
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 1.8.25
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: A Mongoid plugin for managing subscription plans and features
112
+ test_files:
113
+ - spec/config/mongoid.yml
114
+ - spec/models/post.rb
115
+ - spec/models/user.rb
116
+ - spec/planify/limitations_spec.rb
117
+ - spec/planify/plan_overrides_spec.rb
118
+ - spec/planify/plan_spec.rb
119
+ - spec/planify/plans_spec.rb
120
+ - spec/planify/user/plan_info_spec.rb
121
+ - spec/planify/user_spec.rb
122
+ - spec/spec_helper.rb