File Coverage

blib/lib/MooseX/RelatedClasses.pm
Criterion Covered Total %
statement 65 68 95.5
branch 8 8 100.0
condition n/a
subroutine 30 33 90.9
pod 3 3 100.0
total 106 112 94.6


line stmt bran cond sub pod time code
1             #
2             # This file is part of MooseX-RelatedClasses
3             #
4             # This software is Copyright (c) 2012 by Chris Weyl.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU Lesser General Public License, Version 2.1, February 1999
9             #
10             package MooseX::RelatedClasses;
11             our $AUTHORITY = 'cpan:RSRCHBOY';
12             # git description: 0.009-2-gfc98462
13             $MooseX::RelatedClasses::VERSION = '0.010';
14              
15             # ABSTRACT: Parameterized role for related class attributes
16              
17 7     19   892144 use MooseX::Role::Parameterized;
  7         431789  
  7         45  
18 7     19   204142 use namespace::autoclean;
  7         15  
  7         45  
19 7     7   3681 use autobox::Core;
  7         58612  
  7         63  
20 7     7   6401 use autobox::Camelize;
  7         2323  
  7         32  
21 7     7   5693 use MooseX::AttributeShortcuts 0.020;
  7         1124552  
  7         51  
22 7     7   194657 use MooseX::Types::Common::String ':all';
  7         12  
  7         73  
23 7     7   27088 use MooseX::Types::LoadableClass ':all';
  7         242922  
  7         51  
24 7     7   15145 use MooseX::Types::Perl ':all';
  7         165240  
  7         80  
25 7     7   16875 use MooseX::Types::Moose ':all';
  7         11  
  7         60  
26 7     7   43275 use MooseX::Util 'with_traits', 'find_meta';
  7         168228  
  7         80  
27              
28 7     7   5558 use Module::Find 'findallmod';
  7         6386  
  7         360  
29              
30 7     7   41 use Class::Load 'load_class';
  7         7  
  7         241  
31 7     7   2622 use String::RewritePrefix;
  7         4919  
  7         33  
32              
33 7     7   982 use Moose::Exporter;
  7         12  
  7         67  
34             Moose::Exporter->setup_import_methods(
35             with_meta => [ qw{
36             related_classes related_class
37             related_namespace
38             } ],
39             );
40              
41              
42 6     6 1 6973 sub related_class { goto \&related_classes }
43              
44             sub related_classes {
45 6     6 1 14 my $meta = shift;
46              
47 6 100       25 if (@_ % 2 == 1) {
48 4 100       20 unshift @_, ref $_[0] ? 'names' : 'name';
49             }
50              
51 6         22 find_meta('MooseX::RelatedClasses')->apply($meta, @_);
52             }
53              
54              
55             sub related_namespace {
56 0     0 1 0 my ($meta, $namespace) = (shift, shift);
57              
58 0         0 my %args = (
59             all_in_namespace => 1,
60             namespace => $namespace,
61             name => $namespace,
62             @_,
63             );
64              
65             ### %args
66 0         0 find_meta('MooseX::RelatedClasses')->apply($meta, %args);
67             }
68              
69              
70             parameter name => (
71             traits => [Shortcuts],
72             is => 'ro',
73             isa => PackageName,
74             predicate => 1,
75             );
76              
77             parameter names => (
78             traits => [Shortcuts],
79             is => 'rwp',
80             predicate => 1,
81             lazy => 1,
82              
83             isa => HashRef[Identifier],
84             constraint => sub { do { is_PackageName($_) or die 'keys must be PackageName' } for $_->keys; 1 },
85             coerce => [
86             ArrayRef => sub { +{ map { $_ => $_->decamelize } @$_ } },
87             PackageName()->name() => sub { +{ $_ => $_->decamelize } },
88             ],
89              
90             default => sub { confess 'name parameter required!' unless $_[0]->has_name; $_[0]->name },
91             );
92              
93             parameter namespace => (
94             traits => [Shortcuts],
95             is => 'rwp',
96             isa => Maybe[PackageName],
97             predicate => 1,
98             );
99              
100             parameter all_in_namespace => (isa => Bool, default => 0);
101             parameter load_all => (isa => Bool, default => 0);
102             parameter private => (isa => Bool, default => 0);
103              
104             # TODO use rewrite prefix to look for traits in namespace
105              
106             role {
107             my ($p, %opts) = @_;
108              
109             confess 'Cannot specify both the "name" and "names" parameters!'
110             if $p->has_name && $p->has_names;
111              
112             # check namespace
113             if (not $p->has_namespace) {
114              
115             die 'Either a namespace or a consuming metaclass must be supplied!'
116             unless $opts{consumer};
117              
118             $p->_set_namespace($opts{consumer}->name);
119             }
120              
121             if ($p->all_in_namespace) {
122              
123             confess 'Cannot use an empty namespace and all_in_namespace!'
124             unless $p->has_namespace;
125              
126             my $ns = $p->namespace;
127              
128             ### finding for namespace: $ns
129             my %mod =
130             map { s/^${ns}:://; $_ => $_->decamelize }
131             map { load_class($_) if $p->load_all; $_ }
132             Module::Find::findallmod $ns
133             ;
134              
135             ### %mod
136             $p->_set_names(\%mod);
137             }
138              
139             $p->names->each(sub { _generate_one_attribute_set($p, @_, %opts) });
140             #_generate_one_attribute_set($p, $_, %opts)
141             #for $p->names->flatten;
142              
143             return;
144             };
145              
146             sub _generate_one_attribute_set {
147 17     17   44 my ($p, $name => $identifier, %opts) = @_;
148              
149 17 100       69 my $full_name
150             = $p->namespace
151             ? $p->namespace . '::' . $name
152             : $name
153             ;
154              
155 17 100       181 my $pvt = $p->private ? '_' : q{};
156              
157             # SomeThing::More -> some_thing__more
158 17         106 my $local_name = "${identifier}_class";
159 17         28 my $original_local_name = "original_$local_name";
160 17         49 my $original_reader = "$pvt$original_local_name";
161 17         29 my $traitsfor_local_name = $local_name . '_traits';
162 17         33 my $traitsfor_reader = "$pvt$traitsfor_local_name";
163              
164             ### $full_name
165             has "$pvt$original_local_name" => (
166             traits => [Shortcuts],
167             is => 'lazy',
168             isa => LoadableClass,
169 76     76   263022 constraint => sub { $_->isa($full_name) },
170             coerce => 1,
171             init_arg => "$pvt$local_name",
172 22     22   650 builder => sub { $full_name },
        0      
        0      
        58      
        36      
173 17         80 );
174              
175             has "$pvt$local_name" => (
176             traits => [Shortcuts],
177             is => 'lazy',
178             isa => LoadableClass,
179 60     60   46693 constraint => sub { $_->isa($full_name) },
180             coerce => 1,
181             init_arg => undef,
182             builder => sub {
183 30     30   66 my $self = shift @_;
        30      
        44      
        14      
        12      
184              
185 30         1011 return with_traits( $self->$original_reader() =>
186             $self->$traitsfor_reader()->flatten,
187             );
188             },
189 17         4174 );
190              
191             # XXX do the same original/local init_arg swizzle here too?
192             has "$pvt$traitsfor_local_name" => (
193             traits => [Shortcuts, 'Array'],
194             is => 'lazy',
195             isa => ArrayRef[LoadableRole],
196 30     68   1000 builder => sub { [ ] },
        48      
        46      
197 17         3053 handles => {
198             "${pvt}has_$traitsfor_local_name" => 'count',
199             },
200             );
201              
202 17         55877 return;
203             }
204              
205             !!42;
206              
207             __END__
208              
209             =pod
210              
211             =encoding UTF-8
212              
213             =for :stopwords Chris Weyl Kulag Parameterized Namespacing findable
214              
215             =for :stopwords Wishlist flattr flattr'ed gittip gittip'ed
216              
217             =head1 NAME
218              
219             MooseX::RelatedClasses - Parameterized role for related class attributes
220              
221             =head1 VERSION
222              
223             This document describes version 0.010 of MooseX::RelatedClasses - released April 10, 2015 as part of MooseX-RelatedClasses.
224              
225             =head1 SYNOPSIS
226              
227             # with this:
228             with 'MooseX::RelatedClasses' => {
229             name => 'Thinger', namespace => undef,
230             };
231              
232             # this:
233             use MooseX::RelatedClasses;
234             related_class name => 'Thinger', namespace => undef;
235              
236             # ...or this (preferred):
237             use MooseX::RelatedClasses;
238             related_class 'Thinger', namespace => undef;
239              
240             # ...we get three attributes:
241             #
242             # thinger_class
243             # thinger_class_traits
244             # original_thinger_class
245             #
246             # ...and they look like this:
247              
248             has thinger_class => (
249             traits => [ Shortcuts ], # MooseX::AttributeShortcuts
250             is => 'lazy', # MX::AttributeShortcuts
251             isa => LoadableClass, # MooseX::Types::LoadableClass
252             init_arg => undef,
253             constraint => sub { $_->isa('Thinger') }, # MX::AttributeShortcuts
254             builder => sub { ... compose original class and traits ... },
255             );
256              
257             has thinger_class_traits => (
258             traits => [ Shortcuts ],
259             is => 'lazy',
260             isa => ArrayRef[LoadableRole],
261             builder => sub { [ ] },
262             );
263              
264             has original_thinger_class => (
265             traits => [ Shortcuts ],
266             is => 'lazy',
267             isa => LoadableClass,
268             constraint => sub { $_->isa('Thinger') },
269             coerce => 1,
270             init_arg => 'thinger_class',
271             builder => sub { 'My::Framework::Thinger' },
272             );
273              
274             =head1 DESCRIPTION
275              
276             Have you ever built out a framework, or interface API of some sort, to
277             discover either that you were hardcoding your related class names (not very
278             extension-friendly) or writing the same code for the same type of attributes
279             to specify what related classes you're using?
280              
281             Alternatively, have you ever been using a framework, and wanted to tweak one
282             tiny bit of behaviour in a subclass, only to realize it was written in such a
283             way to make that difficult-to-impossible without a significant effort?
284              
285             This package aims to end that, by providing an easy, flexible way of defining
286             "related classes", their base class, and allowing traits to be specified.
287              
288             =head2 This is early code!
289              
290             This package is very new, and is still being vetted "in use", as it were. The
291             documentation (or tests) may not be 100%, but it's in active use. Pull
292             requests are happily received :)
293              
294             =head2 Documentation
295              
296             See the SYNOPSIS for information; the tests are also useful here as well.
297              
298             I _did_ warn you this is a very early release, right?
299              
300             =head1 ROLE PARAMETERS
301              
302             Parameterized roles accept parameters that influence their construction. This role accepts the following parameters.
303              
304             =head2 name
305              
306             The name of a class, without the prefix, to consider related. e.g. if My::Foo
307             is our namespace and My::Foo::Bar is the related class:
308              
309             name => 'Bar'
310              
311             ...is the correct specification.
312              
313             This parameter is optional, so long as either the names or all_in_namespace
314             parameters are given.
315              
316             =head2 names [ ... ]
317              
318             One or more names that would be legal for the name parameter.
319              
320             =head2 all_in_namespace (Bool)
321              
322             True if all findable packages under the namespace should be used as related
323             classes. Defaults to false.
324              
325             =head2 namespace
326              
327             The namespace our related classes live in. If this is not given explicitly,
328             the name of the consuming class will be used as the namespace. If the
329             consuming class' metaclass is not available (e.g. the role is being
330             constructed by something other than a consumer), then this parameter is
331             mandatory.
332              
333             This parameter will also accept an explicit 'undef'. If this is the case,
334             then related classes must be specified by their full name and it is an error
335             to attempt to enable the all_in_namespace option.
336              
337             e.g.:
338              
339             with 'MooseX::RelatedClasses' => {
340             namespace => undef,
341             name => 'LWP::UserAgent',
342             };
343              
344             ...will provide the C<lwp__user_agent_class>, C<lwp__user_agent_traits> and
345             C<original_lwp__user_agent_class> attributes.
346              
347             =head2 load_all (Bool)
348              
349             If set to true, all related classes are loaded as we find them. Defaults to
350             false.
351              
352             =head2 private (Bool)
353              
354             If true, attributes, accessors and builders will all be named according to the
355             same rules L<MooseX::AttributeShortcuts> uses. (That is, in general prefixed
356             with an "_".)
357              
358             =head1 FUNCTIONS
359              
360             =head2 related_class()
361              
362             Synonym for L</related_classes()>.
363              
364             =head2 related_classes()
365              
366             Takes the same options that the role takes as parameters. That means that this:
367              
368             related_classes name => 'LWP::UserAgent', namespace => undef;
369              
370             ...is effectively the same as:
371              
372             with 'MooseX::RelatedClasses' => {
373             name => 'LWP::UserAgent',
374             namespace => undef,
375             };
376              
377             =head2 related_namespace()
378              
379             Given a namespace, declares that everything under that namespace is related.
380             That is,
381              
382             related_namespace 'Net::Amazon::EC2';
383              
384             ...is the same as:
385              
386             with 'MooseX::RelatedClasses' => {
387             namespace => 'Net::Amazon::EC2',
388             name => 'Net::Amazon::EC2',
389             all_in_namespace => 1,
390             };
391              
392             =head1 EXAMPLES
393              
394             =head2 Multiple Related Classes at Once
395              
396             Use the L</names> option with an array reference of classes, and attribute
397             sets will be built for all of them.
398              
399             related_classes [ qw{ Thinger Dinger Finger } ];
400              
401             # or longhand:
402             related_classes names => [ qw{ Thinger Dinger Finger } ];
403              
404             =head2 Namespaces / Namespacing
405              
406             Normally, related classes tend to be under the namespace of the class they
407             are related to. For example, let's say we have a class named C<TimeLords>.
408             Related to this class are C<TimeLords::SoftwareWritten::Git>,
409             C<TimeLords::Gallifrey> and C<TimeLords::Enemies::Daleks>.
410              
411             The C<TimeLords> package can start off like this, to include the proper
412             related classes:
413              
414             package TimeLords;
415              
416             use Moose;
417             use timeandspace::autoclean;
418             use MooseX::RelatedClasses;
419              
420             related_classes [ qw{ Gallifrey Enemies::Daleks SoftwareWritten::Git } ];
421              
422             And that will generate the expected related class attributes:
423              
424             # TimeLords::Gallifrey
425             gallifrey_class
426             gallifrey_class_traits
427             original_gallifrey_class
428             # TimeLords::Enemies::Daleks
429             enemies__daleks_class
430             enemies__daleks_class_traits
431             original_enemies__daleks_class
432             # TimeLords::SoftwareWritten::Git
433             software_written__git_class
434             software_written__git_class_traits
435             original_software_written__git_class
436              
437             =head2 Related classes outside the namespace
438              
439             Occasionally you'll want to use something like L<LWP::UserAgent>, which has
440             nothing to do with your class except that you use it, and would like to be
441             able to easily tweak it on the fly. This can be done with the C<undef>
442             namespace:
443              
444             related_class 'LWP::UserAgent', namespace => undef;
445              
446             This will cause the following related class attributes to be generated:
447              
448             lwp__user_agent_class
449             lwp__user_agent_class_traits
450             original_lwp__user_agent_class
451              
452             =head1 INSPIRATION / MADNESS
453              
454             The L<Class::MOP> / L<Moose> MOP show the beginnings of this: with attributes
455             or methods named a certain way (e.g. *_metaclass()) the class to be used for a
456             particular thing (e.g. attribute metaclass) is stored in a fashion such that a
457             subclass (or trait) may overwrite and provide a different class name to be
458             used.
459              
460             So too, here, we do this, but in a more flexible way: we track the original
461             related class, any additional traits that should be applied, and the new
462             (anonymous, typically) class name of the related class.
463              
464             Another example is the (very useful and usable) L<Net::Amazon::EC2>. It uses
465             L<Moose>, is nicely broken out into discrete classes, etc, but does not lend
466             itself to easy on-the-fly extension by developers with traits.
467              
468             =head1 ANONYMOUS CLASS NAMES
469              
470             Note that we use L<MooseX::Traitor> to compose anonymous classes, so the
471             "anonymous names" will look less like:
472              
473             Moose::Meta::Package::__ANON__::SERIAL::...
474              
475             And more like:
476              
477             My::Framework::Thinger::__ANON__::SERIAL::...
478              
479             Anonymous classes are only ever composed if traits for a related class are
480             supplied.
481              
482             =head1 SOURCE
483              
484             The development version is on github at L<http://https://github.com/RsrchBoy/moosex-relatedclasses>
485             and may be cloned from L<git://https://github.com/RsrchBoy/moosex-relatedclasses.git>
486              
487             =head1 BUGS
488              
489             Please report any bugs or feature requests on the bugtracker website
490             https://github.com/RsrchBoy/moosex-relatedclasses/issues
491              
492             When submitting a bug or request, please include a test-file or a
493             patch to an existing test-file that illustrates the bug or desired
494             feature.
495              
496             =head1 AUTHOR
497              
498             Chris Weyl <cweyl@alumni.drew.edu>
499              
500             =head2 I'm a material boy in a material world
501              
502             =begin html
503              
504             <a href="https://www.gittip.com/RsrchBoy/"><img src="https://raw.githubusercontent.com/gittip/www.gittip.com/master/www/assets/%25version/logo.png" /></a>
505             <a href="http://bit.ly/rsrchboys-wishlist"><img src="http://wps.io/wp-content/uploads/2014/05/amazon_wishlist.resized.png" /></a>
506             <a href="https://flattr.com/submit/auto?user_id=RsrchBoy&url=https%3A%2F%2Fgithub.com%2FRsrchBoy%2Fmoosex-relatedclasses&title=RsrchBoy's%20CPAN%20MooseX-RelatedClasses&tags=%22RsrchBoy's%20MooseX-RelatedClasses%20in%20the%20CPAN%22"><img src="http://api.flattr.com/button/flattr-badge-large.png" /></a>
507              
508             =end html
509              
510             Please note B<I do not expect to be gittip'ed or flattr'ed for this work>,
511             rather B<it is simply a very pleasant surprise>. I largely create and release
512             works like this because I need them or I find it enjoyable; however, don't let
513             that stop you if you feel like it ;)
514              
515             L<Flattr this|https://flattr.com/submit/auto?user_id=RsrchBoy&url=https%3A%2F%2Fgithub.com%2FRsrchBoy%2Fmoosex-relatedclasses&title=RsrchBoy's%20CPAN%20MooseX-RelatedClasses&tags=%22RsrchBoy's%20MooseX-RelatedClasses%20in%20the%20CPAN%22>,
516             L<gittip me|https://www.gittip.com/RsrchBoy/>, or indulge my
517             L<Amazon Wishlist|http://bit.ly/rsrchboys-wishlist>... If you so desire.
518              
519             =head1 CONTRIBUTOR
520              
521             =for stopwords Kulag
522              
523             Kulag <g.kulag@gmail.com>
524              
525             =head1 COPYRIGHT AND LICENSE
526              
527             This software is Copyright (c) 2012 by Chris Weyl.
528              
529             This is free software, licensed under:
530              
531             The GNU Lesser General Public License, Version 2.1, February 1999
532              
533             =cut