File Coverage

blib/lib/MooX/TaggedAttributes.pm
Criterion Covered Total %
statement 93 122 76.2
branch 26 48 54.1
condition 13 21 61.9
subroutine 17 23 73.9
pod 5 5 100.0
total 154 219 70.3


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