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 +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 [](http://travis-ci.org/maildropr/planify) [](https://codeclimate.com/github/maildropr/planify) [](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
|