File Coverage

blib/lib/Abilities/Features.pm
Criterion Covered Total %
statement 22 47 46.8
branch 6 24 25.0
condition 3 6 50.0
subroutine 6 8 75.0
pod 4 4 100.0
total 41 89 46.0


line stmt bran cond sub pod time code
1             package Abilities::Features;
2              
3             # ABSTRACT: Extends Abilities with plan management for subscription-based web services.
4              
5 2     2   81633 use Carp;
  2         5  
  2         168  
6 2     2   932 use Hash::Merge qw/merge/;
  2         3552  
  2         99  
7 2     2   14 use Moo::Role;
  2         4  
  2         19  
8 2     2   680 use namespace::autoclean;
  2         3  
  2         17  
9              
10             our $VERSION = "0.5";
11             $VERSION = eval $VERSION;
12              
13             =head1 NAME
14              
15             Abilities::Features - Extends Abilities with plan management for subscription-based web services.
16              
17             =head1 VERSION
18              
19             version 0.5
20              
21             =head1 SYNOPSIS
22              
23             package Customer;
24            
25             use Moose; # or Moo
26             with 'Abilities::Features';
27            
28             # ... define required methods ...
29            
30             # somewhere else in your code:
31              
32             # get a customer object that consumed the Abilities::Features role
33             my $customer = MyApp->get_customer('some_company');
34            
35             # check if the customer has a certain feature
36             if ($customer->has_feature('ssl_encryption')) {
37             &initiate_https_connection();
38             } else {
39             &initiate_http_connection();
40             }
41              
42             =head1 DESCRIPTION
43              
44             This L extends the ability-based authorization
45             system defined by the L module with customer and plan management
46             for subscription-based web services. This includes paid services, where
47             customers subscribe to a plan from a list of available plans, each plan
48             with a different set of features. Examples of such a service are GitHub
49             (a Git revision control hosting service, where customers purchase a plan
50             that provides them with different amounts of storage, SSH support, etc.)
51             and MailChimp (email marketing service where customers purchase plans
52             that provide them with different amounts of monthly emails to send and
53             other features).
54              
55             The L role defined three entities: users, roles and actions.
56             This role defines three more entities: customers, plans and features.
57             Customers are organizations, companies or individuals that subscribe to
58             your web service. They can subscribe to any number of plans, and thus be
59             provided with the features of these plans. The users from the Abilities
60             module will now be children of the customers. They still go on being members
61             of roles and performing actions they are granted with, but now possibly
62             only within the scope of their parent customer, and to the limits defined
63             in the customer's plan. Plans can inherit features from other plans, allowing
64             for defining plans faster and easier.
65              
66             Customer and plan objects are meant to consume the Abilities::Features
67             role. L is a reference implementation of both the L and
68             L roles. It is meant to be used as-is by web applications,
69             or just as an example of how a user management and authorization system
70             that consumes these roles might look like. L and
71             L are customer and plan classes that consume this role.
72              
73             Just like in L, features can be constrained. For more info,
74             see L.
75              
76             More information about how these roles work can be found in the L
77             documentation.
78              
79             =head1 REQUIRED METHODS
80              
81             Customer and plan classes that consume this role are required to provide
82             the following methods:
83              
84             =head2 plans()
85              
86             This method returns a list of all plan names that a customer has subscribed to,
87             or that a plan inherits from.
88              
89             Example return structure:
90              
91             ( 'starter', 'diamond' )
92              
93             NOTE: In previous versions, this method was required to return
94             an array of plan objects, not a list of plan names. This has been changed
95             in version 0.3.
96              
97             =cut
98              
99             requires 'plans';
100              
101             =head2 features()
102              
103             This method returns a list of all feature names that a customer has explicitely
104             been given, or that a plan has. If a certain feature is constrained, then
105             it should be added to the list as an array reference with two items, the first being
106             the name of the feature, the second being the name of the constraint.
107              
108             Example return structure:
109              
110             ( 'ssh_access', [ 'multiple_users', 5 ] )
111              
112             NOTE: In previous versions, this method was required to return
113             an array of feature objects, not a list of feature names. This has been changed
114             in version 0.3.
115              
116             =cut
117              
118             requires 'features';
119              
120             =head2 get_plan( $name )
121              
122             Returns the object of the plan named C<$plan>.
123              
124             =cut
125              
126             requires 'get_plan';
127              
128             =head1 METHODS
129              
130             Classes that consume this role will have the following methods provided
131             to them:
132              
133             =head2 has_feature( $feature_name, [ $constraint ] )
134              
135             Receives the name of a feature, and possibly a constraint, and returns a
136             true value if the customer/plan has that feature, false value otherwise.
137              
138             =cut
139              
140             sub has_feature {
141 0     0 1 0 my ($self, $feature, $constraint) = @_;
142              
143             # return false if customer/plan does not have that feature
144 0 0       0 return unless $self->available_features->{$feature};
145              
146             # customer/plan has feature, but is there a constraint?
147 0 0       0 if ($constraint) {
148             # return true if customer/plan's feature is not constrained
149 0 0       0 return 1 if !ref $self->available_features->{$feature};
150            
151             # it is constrained (or at least it should be, let's make
152             # sure we have an array-ref of constraints)
153 0 0       0 if (ref $self->available_features->{$feature} eq 'ARRAY') {
154 0         0 foreach (@{$self->available_features->{$feature}}) {
  0         0  
155 0 0       0 return 1 if $_ eq $constraint;
156             }
157 0         0 return; # constraint not met
158             } else {
159 0         0 carp "Expected an array-ref of constraints for feature $feature, received ".ref($self->available_features->{$feature}).", returning false.";
160 0         0 return;
161             }
162             } else {
163             # no constraint, make sure customer/plan's feature is indeed
164             # not constrained
165 0 0       0 return if ref $self->available_features->{$feature}; # implied: ref == 'ARRAY', thus constrained
166 0         0 return 1; # not constrained
167             }
168             }
169              
170             =head2 in_plan( $plan_name )
171              
172             Receives the name of plan and returns a true value if the user/customer
173             is a direct member of the provided plan(s). Only direct association is
174             checked, so the user/customer must be specifically assigned to that plan,
175             and not to a plan that inherits from that plan (see L
176             instead).
177              
178             =cut
179              
180             sub in_plan {
181 5     5 1 12947 my ($self, $plan) = @_;
182              
183 5 50       13 return unless $plan;
184              
185 5         151 foreach ($self->plans) {
186 4 100       157 return 1 if $_ eq $plan;
187             }
188              
189 3         44 return;
190             }
191              
192             =head2 inherits_plan( $plan_name )
193              
194             Returns a true value if the customer/plan inherits the features of
195             the provided plan(s). If a customer belongs to the 'premium' plan, and
196             the 'premium' plan inherits from the 'basic' plan, then C
197             will be true for that customer, while C will be false.
198              
199             =cut
200              
201             sub inherits_plan {
202 4     4 1 7 my ($self, $plan) = @_;
203              
204 4 50       13 return unless $plan;
205              
206 4         109 foreach (map([$_, $self->get_plan($_)], $self->plans)) {
207 3 100 100     157 return 1 if $_->[0] eq $plan || $_->[1]->inherits_plan($plan);
208             }
209              
210 2         40 return;
211             }
212              
213             =head2 available_features
214              
215             Returns a hash-ref of all features available to a customer/plan object, after
216             consolidating features from inherited plans (recursively) and directly granted.
217             Keys of this hash-ref will be the names of the features, values will either be
218             1 (for yes/no features), or a single-item array-ref with a name of a constraint
219             (for constrained features).
220              
221             =cut
222              
223             sub available_features {
224 0     0 1   my $self = shift;
225              
226 0           my $features = {};
227              
228             # load direct features granted to this customer/plan
229 0           foreach ($self->features) {
230             # is this features constrained?
231 0 0 0       unless (ref $_) {
    0          
232 0           $features->{$_} = 1;
233             } elsif (ref $_ eq 'ARRAY' && scalar @$_ == 2) {
234 0           $features->{$_->[0]} = [$_->[1]];
235             } else {
236 0           carp "Can't handle feature of reference ".ref($_);
237             }
238             }
239              
240             # load features from plans this customer/plan has
241 0           my @hashes = map { $self->get_plan($_)->available_features } $self->plans;
  0            
242              
243             # merge all features
244 0           while (scalar @hashes) {
245 0           $features = merge($features, shift @hashes);
246             }
247              
248 0           return $features;
249             }
250              
251             =head1 UPGRADING FROM v0.2
252              
253             Up to version 0.2, C required the C and C
254             attributes to return objects. While this made it easier to calculate
255             available features, it made this system a bit less flexible.
256              
257             In version 0.3, C changed the requirement such that both these
258             attributes need to return strings (the names of the plans/features). If your implementation
259             has granted plans and features stored in a database by names, this made life a bit easier
260             for you. On other implementations, however, this has the potential of
261             requiring you to write a bit more code. If that is the case, I apologize,
262             but keep in mind that you can still store granted plans and features
263             any way you want in a database (either by names or by references), just
264             as long as you correctly provide C and C.
265              
266             Unfortunately, in both versions 0.3 and 0.4, I made a bit of a mess
267             that rendered both versions unusable. While I documented the C
268             attribute as requiring plan names instead of plan objects, the actual
269             implementation still required plan objects. This has now been fixed,
270             but it also meant I had to add a new requirement: consuming classes
271             now have to provide a method called C that takes the name
272             of a plan and returns its object. This will probably means loading the
273             plan from a database and blessing it into your plan class that also consumes
274             this module.
275              
276             I apologize for any inconvenience this might have caused.
277              
278             =head1 AUTHOR
279              
280             Ido Perlmuter, C<< >>
281              
282             =head1 BUGS
283              
284             Please report any bugs or feature requests to C, or through
285             the web interface at L. I will be notified, and then you'll
286             automatically be notified of progress on your bug as I make changes.
287              
288             =head1 SUPPORT
289              
290             You can find documentation for this module with the perldoc command.
291              
292             perldoc Abilities::Features
293              
294             You can also look for information at:
295              
296             =over 4
297              
298             =item * RT: CPAN's request tracker
299              
300             L
301              
302             =item * AnnoCPAN: Annotated CPAN documentation
303              
304             L
305              
306             =item * CPAN Ratings
307              
308             L
309              
310             =item * Search CPAN
311              
312             L
313              
314             =back
315              
316             =head1 LICENSE AND COPYRIGHT
317              
318             Copyright 2010-2013 Ido Perlmuter.
319              
320             This program is free software; you can redistribute it and/or modify it
321             under the terms of either: the GNU General Public License as published
322             by the Free Software Foundation; or the Artistic License.
323              
324             See http://dev.perl.org/licenses/ for more information.
325              
326             =cut
327              
328             1;