planify 1.0.0

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