File Coverage

blib/lib/MooX/TaggedAttributes.pm
Criterion Covered Total %
statement 105 117 89.7
branch 31 44 70.4
condition 15 21 71.4
subroutine 20 23 86.9
pod 4 4 100.0
total 175 209 83.7


line stmt bran cond sub pod time code
1              
2             # ABSTRACT: Add a tag with an arbitrary value to a an attribute
3              
4             use v5.10.1;
5 5     5   137342  
  5         16  
6             use strict;
7 5     5   27 use warnings;
  5         6  
  5         101  
8 5     5   28  
  5         11  
  5         228  
9             our $VERSION = '0.17'; # TRIAL
10              
11             use MRO::Compat;
12 5     5   2083  
  5         7599  
  5         167  
13             use Sub::Name ();
14 5     5   1439 use Moo::Role ();
  5         1712  
  5         94  
15 5     5   30 use Role::Hooks;
  5         11  
  5         65  
16 5     5   2033  
  5         24481  
  5         126  
17             use Moo::_Utils ();
18 5     5   28 use MooX::TaggedAttributes::Cache;
  5         13  
  5         73  
19 5     5   1947  
  5         17  
  5         1578  
20             our %TAGSTORE;
21             our %TAGCACHE;
22             our %TAGHANDLER;
23              
24             # when using -propagate, make sure we don't duplicate role application.
25             # can't use simply role checks because of inheritance
26             our %APPLIED_ROLE;
27              
28             my %ARGS = ( -tags => 1, -handler => 1, -propagate => undef );
29              
30             require Carp;
31             goto \&Carp::croak;
32 0     0   0 }
33 0         0  
34             my ( $target ) = @_;
35              
36             return
37 65     65   121 if Moo::Role::does_role( $target, 'MooX::TaggedAttributes::Propagate' );
38              
39             Moo::Role->apply_roles_to_package( $target,
40 65 100       192 'MooX::TaggedAttributes::Role' );
41             Moo::Role->apply_roles_to_package( $target,
42 8         164 'MooX::TaggedAttributes::Propagate' );
43              
44 8         2880 Role::Hooks->after_apply(
45             $target,
46             sub {
47             my ( $role, $ltarget ) = @_;
48              
49             # Multiple instances of this hook may get installed
50 62     62   156206 # through application of multiple tag roles to a class
51             # but, we don't want to repeat installation of tags or
52             # another instance of this hook. (the latter
53             # this is guarded against in _install_on_application
54             # but this'll take care of it as a sideeffect)
55              
56             return if $APPLIED_ROLE{$ltarget}{$role}++;
57              
58             # this is guarded in _install_on_application so that
59 62 100       235 # the modifier is done only once for the target
60             _install_on_application( $ltarget );
61              
62              
63 57         133 role_import( $role, $ltarget );
64             } );
65              
66 57         827 }
67 8         3316  
68              
69             my ( $class, @args ) = @_;
70             my $target = caller;
71              
72             my %args;
73 18     18   111282  
74 18         43 while ( @args ) {
75             my $arg = shift @args;
76 18         28 _croak( "unknown argument to ", __PACKAGE__, ": $arg" )
77             unless exists $ARGS{$arg};
78 18         57 $args{$arg} = defined $ARGS{$arg} ? shift @args : 1;
79 30         49 }
80              
81 30 50       71 Moo::Role->apply_roles_to_package( $target, 'MooX::TaggedAttributes::Role' )
82 30 100       104 unless Moo::Role::does_role( $target, 'MooX::TaggedAttributes::Role' );
83              
84             return unless %args;
85 18 50       52  
86             if ( defined $args{-tags} ) {
87             $args{-tags} = [ $args{-tags} ]
88 18 50       7854 unless 'ARRAY' eq ref $args{-tags};
89              
90 18 50       60 $args{-class} = $class;
91             install_tags( $target, %args )
92 18 100       49 if @{ $args{-tags} };
93             }
94 18         35  
95             if ( defined $args{-propagate} && $args{-propagate} ) {
96 18 50       23 _install_on_application( $target );
  18         87  
97             }
98              
99 18 100 66     76 no strict 'refs'; ## no critic
100 8         24 *${ \"${target}::import" } = \&role_import;
101             }
102              
103 5     5   31  
  5         9  
  5         1191  
104 18         2559  
  18         801  
105              
106              
107              
108              
109              
110              
111              
112              
113              
114              
115              
116              
117              
118              
119              
120              
121              
122              
123              
124              
125              
126              
127             my $role = shift;
128             return unless Moo::Role->is_role( $role );
129             my $target = shift // caller;
130              
131             unless ( Moo::Role::does_role( $target, $role ) ) {
132 97     97 1 49414  
133 97 50       254 if ( Moo::Role->is_role( $target ) ) {
134 97   66     2030 Moo::Role->apply_roles_to_package( $target, $role );
135             }
136 97 100       188 else {
137             # Prevent installation of the import routine from a tagged role
138 40 100       757 # into the consumer. Roles won't overwrite an existing method,
139 13         226 # so create one which goes away when this block exits.
140              
141             # localized globs don't seem work on 5.10.1, result in an error
142             # Attempt to free unreferenced scalar: SV 0x564fc668eb60
143             # at [...]MooX/TaggedAttributes.pm line 147.
144              
145             if ( $^V lt v5.14 ) {
146             require Package::Stash;
147             my $pkg = Package::Stash->new( $target );
148             if ( $pkg->has_symbol( '&import' ) ) {
149             Moo::Role->apply_roles_to_package( $target, $role );
150 27 50       1052 }
151 0         0 else {
152 0         0 $pkg->add_symbol( '&import', sub { } );
153 0 0       0 eval {
154 0         0 Moo::Role->apply_roles_to_package( $target, $role );
155             };
156             my $e = $@;
157 0     0   0 $pkg->remove_symbol( '&import' );
158 0         0 die $e if $e ne '';
159 0         0 }
160             }
161 0         0 else {
162 0         0 no strict 'refs'; ## no critic
163 0 0       0 my $glob = *${ \"${target}::import" };
164             !defined *{$glob}{CODE}
165             and local *{$glob} = sub { };
166             Moo::Role->apply_roles_to_package( $target, $role );
167 5     5   32 }
  5         9  
  5         3189  
168 27         38 }
  27         159  
169 27         162 }
170 27 50   0   46 install_tags( $target, -class => $role );
  27         68  
171 27         159 }
172              
173              
174              
175 97         69907  
176              
177              
178              
179              
180              
181              
182              
183              
184              
185              
186              
187              
188              
189             my ( $target, %opt ) = @_;
190              
191             my $tags = $opt{-tags}
192             // ( defined( $opt{-class} ) && $TAGSTORE{ $opt{-class} } )
193             || do {
194             my $class = $opt{-class};
195 115     115 1 290 _croak( "-tags or -class not specified" ) if !defined $class;
196             _croak( "Class '$class' has not yet been registered" );
197             };
198              
199 115   33     595 # first time importing a tag role, install our tag handler
200             install_tag_handler( $target, \&make_tag_handler )
201             if !exists $TAGSTORE{$target};
202              
203             # add the tags.
204             push @{ $TAGSTORE{$target} //= [] }, @$tags;
205              
206             # if an extra handler has been specified, or the tag role class
207 115 100       343 # $opt{-class} has one install that as well.
208             if ( my $handler = $opt{-handler}
209             // ( defined $opt{-class} && $TAGHANDLER{ $opt{-class} } ) )
210 115   100     28680 {
  115         586  
211             my @handlers = 'ARRAY' eq ref $handler ? @$handler : $handler;
212             install_tag_handler( $target, $_ ) for @handlers;
213             push @{ $TAGHANDLER{$target} //= [] }, @handlers;
214 115 100 66     1869 }
      100        
215             }
216              
217 22 100       68  
218 22         44  
219 22   100     4800  
  22         554  
220              
221              
222              
223              
224              
225              
226              
227             my ( $target, $handler ) = @_;
228             Moo::_Utils::_install_modifier( $target,
229             around => has => $handler->( $target ) );
230             }
231              
232              
233              
234 116     116 1 1494  
235 116         241  
236              
237              
238              
239              
240             my $target = shift;
241             push @{ $Moo::Role::INFO{$target}{modifiers} ||= [] }, [@_];
242             Moo::Role->_maybe_reset_handlemoose( $target );
243             }
244              
245              
246              
247              
248 51     51   76  
249 51   50     60  
  51         161  
250 51         110  
251              
252              
253              
254              
255              
256             # we need to
257             # 1) use the target package's around() function, and
258             # 2) call it in that package's context.
259              
260             # create a closure which knows about the target's around
261             # so that if namespace::clean is called on the target class
262             # we don't lose access to it.
263              
264             my $target = shift;
265             my $is_role = Moo::Role->is_role( $target );
266              
267             return Sub::Name::subname "${target}::tag_handler" => sub {
268              
269             my ( $orig, $attrs, %opt ) = @_;
270             $orig->( $attrs, %opt );
271              
272             $attrs = ref $attrs ? $attrs : [$attrs];
273 88     88 1 118 my @tags = @{ $TAGSTORE{$target} };
274 88         185  
275             my @args = (
276             $target,
277             around => "_tag_list" => sub {
278 99     99   20562 my $orig = shift;
279 99         413 ## no critic (ProhibitAccessOfPrivateData)
280             my @ret = (
281 99 50       92039 @{&$orig},
282 99         165 map { [ $_, $attrs, $opt{$_} ] }
  99         251  
283             grep { exists $opt{$_} } @tags,
284             );
285             return \@ret;
286             } );
287 338         3288  
288             $is_role
289             ? _install_role_modifier( @args )
290 338         4627 : Moo::_Utils::_install_modifier( @args );
291 296         662 }
292 338         387 }
  682         1191  
293              
294 338         1409  
295 99         466 1;
296              
297 99 100       282 #
298             # This file is part of MooX-TaggedAttributes
299             #
300             # This software is Copyright (c) 2018 by Smithsonian Astrophysical Observatory.
301 88         2266 #
302             # This is free software, licensed under:
303             #
304             # The GNU General Public License, Version 3, June 2007
305             #
306              
307              
308             =pod
309              
310             =for :stopwords Diab Jerius Smithsonian Astrophysical Observatory instantiation use'ing
311              
312             =head1 NAME
313              
314             MooX::TaggedAttributes - Add a tag with an arbitrary value to a an attribute
315              
316             =head1 VERSION
317              
318             version 0.17
319              
320             =head1 SYNOPSIS
321              
322             # define a Tag Role
323             package T1;
324             use Moo::Role;
325            
326             use MooX::TaggedAttributes -tags => [qw( t1 t2 )];
327             1;
328              
329             # Apply a tag role directly to a class
330             package C1;
331             use Moo;
332             use T1;
333            
334             has c1 => ( is => 'ro', t1 => 1 );
335             1;
336              
337             # use a tag role in another Role
338             package R1;
339            
340             use Moo::Role;
341             use T1;
342            
343             has r1 => ( is => 'ro', t2 => 2 );
344             1;
345              
346             # Use a tag role which consumes a tag role in a class
347             package C2;
348             use Moo;
349             use R1;
350            
351             has c2 => ( is => 'ro', t2 => sub { } );
352             1;
353              
354             # Use our tags
355             use C1;
356             use C2;
357            
358             use 5.01001;
359            
360             # get the value of the tag t1, applied to attribute a1
361             say C1->new->_tags->{t1}{a1};
362            
363             # get the value of the tag t2, applied to attribute c2
364             say C2->new->_tags->{t2}{c2};
365              
366             =head1 DESCRIPTION
367              
368             This module attaches a tag-value pair to an attribute in a B<Moo>
369             class or role, and provides a interface to query which attributes have
370             which tags, and what the values are. It keeps track of tags for
371             attributes through role composition as well as class inheritance.
372              
373             =head2 Tagging Attributes
374              
375             To define a set of tags, create a special I<tag role>:
376              
377             package T1;
378             use Moo::Role;
379             use MooX::TaggedAttributes -tags => [ 't1' ];
380            
381             has a1 => ( is => 'ro', t1 => 'foo' );
382            
383             1;
384              
385             If there's only one tag, it can be passed directly without being
386             wrapped in an array:
387              
388             package T2;
389             use Moo::Role;
390             use MooX::TaggedAttributes -tags => 't2';
391            
392             has a2 => ( is => 'ro', t2 => 'bar' );
393            
394             1;
395              
396             A tag role is a standard B<Moo::Role> with added machinery to track
397             attribute tags. As shown, attributes may be tagged in the tag role
398             as well as in modules which consume it.
399              
400             Tag roles may be consumed just as ordinary roles, but in order for
401             role consumers to have the ability to assign tags to attributes, they
402             need to be consumed with the Perl B<use> statement, not with the B<with> statement.
403              
404             Consuming with the B<with> statement I<will> propagate attributes with
405             existing tags, but won't provide the ability to tag new attributes.
406              
407             This is correct:
408              
409             package R2;
410             use Moo::Role;
411             use T1;
412            
413             has r2 => ( is => 'ro', t1 => 'foo' );
414             1;
415              
416             package R3;
417             use Moo::Role;
418             use R3;
419            
420             has r3 => ( is => 'ro', t1 => 'foo' );
421             1;
422              
423             The same goes for classes:
424              
425             package C1;
426             use Moo;
427             use T1;
428            
429             has c1 => ( is => 'ro', t1 => 'foo' );
430             1;
431              
432             Combining tag roles is as simple as B<use>'ing them in the new role:
433              
434             package T12;
435            
436             use Moo::Role;
437             use T1;
438             use T2;
439            
440             1;
441              
442             package C2;
443             use Moo;
444             use T12;
445            
446             has c2 => ( is => 'ro', t1 => 'foo', t2 => 'bar' );
447             1;
448              
449             =head2 Accessing tags
450              
451             Classes and objects are provided a B<_tags> method which returns a
452             L<MooX::TaggedAttributes::Cache> object. For backwards compatibility,
453             it can be dereferenced as a hash, providing a hash of hashes keyed
454             off of the tags and attribute names. For example, for the following
455             code:
456              
457             package T;
458             use Moo::Role;
459             use MooX::TaggedAttributes -tags => [qw( t1 t2 )];
460             1;
461              
462             package C;
463             use Moo;
464             use T;
465            
466             has a => ( is => 'ro', t1 => 2 );
467             has b => ( is => 'ro', t2 => 'foo' );
468             1;
469              
470             The tag structure returned by C<< C->_tags >>
471              
472             bless({ t1 => { a => 2 }, t2 => { b => "foo" } }, "MooX::TaggedAttributes::Cache")
473              
474             and C<< C->new->_tags >>
475              
476             bless({ t1 => { a => 2 }, t2 => { b => "foo" } }, "MooX::TaggedAttributes::Cache")
477              
478             are identical.
479              
480             =head1 ADVANCED USE
481              
482             =head2 Experimental!
483              
484             =head3 Additional tag handlers
485              
486             C<MooX::TaggedAttributes> works in part by wrapping L<Moo/has> in
487             logic which handles the association of tags with attributes. This
488             wrapping is automatically applied when a module uses a tag role, and
489             its mechanism may be used to apply an additional wrapper by passing
490             the C<-handler> option to L<MooX::TaggedAttributes>:
491              
492             use MooX::TaggedAttributes -handler => $handler, -tags => ...;
493              
494             C<$handler> is a subroutine reference which will be called as
495              
496             $coderef = $handler->($class);
497              
498             Its return value must be a coderef suitable for passing as an 'around'
499             modifier for 'has' to L<Moo::_Utils::_install_modifier> to wrap
500             C<has>, e.g.
501              
502             Moo::_Utils::_install_modifier( $target, around has => $coderef );
503              
504             =head3 Automatically propagating tagging abilities
505              
506             As mentioned previously, a package load a tag role using the C<use>
507             statement (not the C<with> statement) to be able tag attributes.
508              
509             An (experimental) alternative is to pass the C<-propagate> option when
510             defining a tag role, e.g.
511              
512             # define a Tag Role
513             package T1;
514             use Moo::Role;
515            
516             use MooX::TaggedAttributes -tags => [qw( t1 t2 )], -propagate;
517             1;
518              
519             Classes or roles consuming this role via C<with> will be able to tag
520             attributes, and will pass that capability on to classes which consume
521             them.
522              
523             This results in different behavior than the previous (soon to be
524             deprecated) mode. There, consuming a role using C<with> does not
525             convey tagging abilities to the consumer. That is done with the C<use>
526             command.
527              
528             =head1 BUGS, LIMITATIONS, TRAPS FOR THE UNWARY
529              
530             =head2 Changes to an object after instantiation are not tracked.
531              
532             If a role with tagged attributes is applied to an object, the
533             tags for those attributes are not visible.
534              
535             =head2 An B<import> routine is installed into the tag role's namespace
536              
537             When a tag role imports C<MooX::TaggedAttributes> via
538              
539             package My::Role;
540             use MooX::TaggedAttributes;
541              
542             two things happen to it:
543              
544             =over
545              
546             =item 1
547              
548             a role is applied to it which adds the methods C<_tags> and C<_tag_list>.
549              
550             =item 2
551              
552             An C<import()> method is installed (e.g. in the above example, that
553             becomes C<My::Role::import>). This may cause conflicts if C<My::Role>
554             has an import method. (It's exceedingly rare that a role would have an
555             C<import> method.) This import method is used when the tag role is
556             itself imported, e.g. in the above example,
557              
558             package My::Module;
559             use My::Role; # <---- My::Role's import routine is called here
560              
561             This C<import> does two things. In the above example, it
562              
563             =over
564              
565             =item 1
566              
567             applies the role C<My::Role> to C<My::Module>;
568              
569             =item 2
570              
571             modifies the L<Moo> C<has> attribute creator so that calls to C<has>
572             in C<My::Module> track attributes with tags.
573              
574             =back
575              
576             =back
577              
578             =head1 SUPPORT
579              
580             =head2 Bugs
581              
582             Please report any bugs or feature requests to bug-moox-taggedattributes@rt.cpan.org or through the web interface at: https://rt.cpan.org/Public/Dist/Display.html?Name=MooX-TaggedAttributes
583              
584             =head2 Source
585              
586             Source is available at
587              
588             https://gitlab.com/djerius/moox-taggedattributes
589              
590             and may be cloned from
591              
592             https://gitlab.com/djerius/moox-taggedattributes.git
593              
594             =head1 INTERNAL ROUTINES
595              
596             These routines are B<not> meant for public consumption, but are
597             documented here for posterity.
598              
599             =head2 role_import
600              
601             This import method is installed into tag roles (i.e. roles which
602             import L<MooX::TaggedAttributes>). The result is that when a tag role
603             is imported, via e.g.
604              
605             package My::Module
606             use My::TagRole;
607              
608             =over
609              
610             =item *
611              
612             The role will be applied to the importing module (e.g., C<My::Module>), providing the C<_tags> and
613             C<_tag_list> methods.
614              
615             =item *
616              
617             The Moo C<has> routine in C<My::Module> will be modified to track attributes with tags.
618              
619             =back
620              
621             =head2 install_tags
622              
623             install_tags( $target, %opt );
624              
625             This subroutine associates a list of tags with a class. The first time this is called
626             on a class it also calls L</install_tag_handler>. For subsequent calls it appends
627             the tags to the class' list of tags.
628              
629             C<%opt> may contain C<tag_handler> which is a coderef for a tag handler.
630              
631             C<%opt> must contain either C<tags>, an arrayref of tags, or C<class>, the name of a class
632             which as already been registered with L<MooX::TaggedAttributes>.
633              
634             =head2 install_tag_handler
635              
636             install_tag_handler( $class, $factory );
637              
638             This installs a wrapper around the C<has> routine in C<$class>. C<$factory>
639             is called as C<< $factory->($class) >> and should return a wrapper compatible
640             with L<Class::Method::Modifiers/around>.
641              
642             =head2 _install_role_modifier
643              
644             Our own purloined version of code to register modifiers for roles. See
645             L<Role::Tiny>'s C<_gen_subs> or L<Moo::Role>'s similarly named routine.
646             Unfortunately, there's no way of easily calling that code
647              
648             =head2 make_tag_handler
649              
650             $coderef = make_tag_handler( $target_class );
651              
652             A tag handler factory returning a coderef which wraps the
653             C<$target_class::_tag_list> method to add the tags in
654             C<$TAGSTORE{$target}> to its return value.
655              
656             =head1 AUTHOR
657              
658             Diab Jerius <djerius@cpan.org>
659              
660             =head1 COPYRIGHT AND LICENSE
661              
662             This software is Copyright (c) 2018 by Smithsonian Astrophysical Observatory.
663              
664             This is free software, licensed under:
665              
666             The GNU General Public License, Version 3, June 2007
667              
668             =cut