planify 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.coveralls.yml +1 -0
- data/.gitignore +20 -0
- data/.travis.yml +8 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +158 -0
- data/Rakefile +6 -0
- data/lib/planify.rb +13 -0
- data/lib/planify/integrations/rails.rb +26 -0
- data/lib/planify/limitations.rb +46 -0
- data/lib/planify/plan.rb +70 -0
- data/lib/planify/plans.rb +30 -0
- data/lib/planify/railtie.rb +9 -0
- data/lib/planify/user.rb +82 -0
- data/lib/planify/user/limitable_counts.rb +23 -0
- data/lib/planify/user/plan_info.rb +32 -0
- data/lib/planify/util/class_helper.rb +18 -0
- data/lib/planify/version.rb +3 -0
- data/planify.gemspec +23 -0
- data/spec/config/mongoid.yml +6 -0
- data/spec/models/post.rb +4 -0
- data/spec/models/user.rb +6 -0
- data/spec/planify/limitations_spec.rb +85 -0
- data/spec/planify/plan_overrides_spec.rb +0 -0
- data/spec/planify/plan_spec.rb +102 -0
- data/spec/planify/plans_spec.rb +58 -0
- data/spec/planify/user/plan_info_spec.rb +31 -0
- data/spec/planify/user_spec.rb +115 -0
- data/spec/spec_helper.rb +39 -0
- metadata +122 -0
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
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
|
data/lib/planify/plan.rb
ADDED
@@ -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
|
data/lib/planify/user.rb
ADDED
@@ -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
|
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
|
data/spec/models/post.rb
ADDED
data/spec/models/user.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|