File Coverage

blib/lib/MooX/Tag/TO_HASH.pm
Criterion Covered Total %
statement 47 47 100.0
branch 9 10 90.0
condition 14 20 70.0
subroutine 11 11 100.0
pod 1 1 100.0
total 82 89 92.1


line stmt bran cond sub pod time code
1              
2             # ABSTRACT: Controlled translation of Moo objects into Hashes
3              
4             use v5.10;
5 7     7   1481106  
  7         58  
6             use strict;
7 7     7   33 use warnings;
  7         12  
  7         120  
8 7     7   26  
  7         13  
  7         243  
9             our $VERSION = '0.01';
10              
11             use Safe::Isa;
12 7     7   2668 use Sub::Util qw( set_subname );
  7         2850  
  7         804  
13 7     7   42  
  7         13  
  7         471  
14             use constant ( {
15             map { uc( $_ ) => $_ } 'omit_if_empty', 'if_exists',
16 7         14 'if_defined', 'no_recurse',
  42         852  
17             'alt_name', 'predicate'
18             } );
19 7     7   37 use constant ( { FTO_HASH => 'to_hash' } );
  7         12  
20 7     7   37  
  7         17  
  7         3192  
21             our %ALLOWED
22             = ( map { $_ => undef } OMIT_IF_EMPTY, IF_EXISTS, IF_DEFINED, NO_RECURSE );
23              
24             require Carp;
25             goto \&Carp::croak;
26             }
27              
28              
29             set_subname __PACKAGE__ . '::tag_handler' => sub {
30             my ( $orig, $attrs, %opt ) = @_;
31             my $spec = $opt{ +FTO_HASH };
32              
33             # if no to_hash, or to_hash has been processed (e.g. it's now a hash ref)
34             # pass on to original routine.
35             return $orig->( $attrs, %opt )
36             if !defined $spec || ref( $spec );
37              
38             my %spec;
39             if ( $spec ne '1' ) {
40             my ( $alt_name, @stuff ) = split( ',', $spec );
41             defined $_ and _croak( "unknown option: $_ " )
42             for grep { !exists $ALLOWED{$_} } @stuff;
43             $spec{ +ALT_NAME } = $alt_name if length( $alt_name );
44             $spec{$_} = 1 for @stuff;
45              
46             # consistency checks if more than one attribute is passed to has.
47             if ( ref $attrs && @{$attrs} > 1 ) {
48             _croak(
49             "can't specify alternate name if more than one attribute is defined"
50             ) if exists $spec{ +ALT_NAME };
51             _croak(
52             "can't specify predicate name if more than one attribute is defined"
53             ) if defined $opt{ +PREDICATE } && $opt{ +PREDICATE } ne '1';
54             }
55              
56             $spec{ +IF_EXISTS } = delete $spec{ +OMIT_IF_EMPTY }
57             if exists $spec{ +OMIT_IF_EMPTY };
58              
59             $opt{ +PREDICATE } //= '1'
60             if $spec{ +IF_EXISTS };
61             }
62              
63             my %to_hash;
64             for my $attr ( ref $attrs ? @{$attrs} : $attrs ) {
65             $to_hash{$attr} = {%spec};
66             if ( $spec{ +IF_EXISTS } ) {
67             $opt{ +PREDICATE } //= 1;
68             $to_hash{$attr}{ +PREDICATE }
69             = $opt{ +PREDICATE } eq '1'
70             ? 'has_' . $attr
71             : $opt{ +PREDICATE };
72             }
73             }
74             $opt{ +FTO_HASH } = \%to_hash;
75             return $orig->( $attrs, %opt );
76             };
77             }
78              
79             use Moo::Role;
80             use MooX::TaggedAttributes -propagate,
81             -tags => 'to_hash',
82 7     7   49 -handler => \&make_tag_handler;
  7         15  
  7         50  
83 7         55  
84             use namespace::clean -except => [ '_tags', '_tag_list' ];
85 7     7   7210  
  7         100492  
86              
87 7     7   106313  
  7         15  
  7         32  
88              
89              
90              
91              
92              
93              
94             my $self = shift;
95              
96             my $to_hash = $self->_tags->tag_attr_hash->{to_hash} // {};
97              
98 12     12 1 74920 # the structure of %to_hash is complicated because has() may take
99             # multiple attributes. For example,
100 12   50     63  
101             # has ['foo','bar'] => ( is => 'ro', to_hash => '1' );
102              
103             # results in %to_hash looking like this:
104              
105             # bar => {
106             # bar => { omit_if_empty => 0, predicate => "has_bar" },
107             # foo => { omit_if_empty => 0, predicate => "has_foo" },
108             # },
109             # foo => {
110             # bar => { omit_if_empty => 0, predicate => "has_bar" },
111             # foo => { omit_if_empty => 0, predicate => "has_foo" },
112             # },
113              
114             my %hash;
115             for my $attr ( keys %{$to_hash} ) {
116             my $opt = $to_hash->{$attr}{$attr};
117             # hashes returned by the _tags method are readonly, so need to
118 12         31417 # check if key exists before querying it to avoid an exception
119 12         25 next
  12         42  
120 63         102 if exists $opt->{ +IF_EXISTS }
121             && $opt->{ +IF_EXISTS }
122             && !$self->${ \$opt->{ +PREDICATE } };
123              
124             next
125             if exists $opt->{ +IF_DEFINED }
126 63 100 66     216 && $opt->{ +IF_DEFINED }
  28   100     137  
127             && !defined $self->${ \$attr };
128              
129             my $alt_name
130             = exists $opt->{ +ALT_NAME }
131 51 100 66     146 ? $opt->{ +ALT_NAME } // $attr
  7   100     50  
132             : $attr;
133             my $value = $self->$attr;
134             if ( exists $opt->{ +NO_RECURSE } && $opt->{ +NO_RECURSE } ) {
135 45 100 33     110 $hash{$alt_name} = $value;
136             }
137 45         159 else {
138 45 100 66     123 # turtles all the way down...
139 1         4 my $mth = $value->$_can( FTO_HASH );
140             $hash{$alt_name} = defined $mth ? $value->$mth : $value;
141             }
142             }
143 44         111 return \%hash;
144 44 50       394 }
145              
146              
147 12         108  
148              
149             1;
150              
151             #
152             # This file is part of MooX-Tag-TO_HASH
153             #
154             # This software is Copyright (c) 2022 by Smithsonian Astrophysical Observatory.
155             #
156             # This is free software, licensed under:
157             #
158             # The GNU General Public License, Version 3, June 2007
159             #
160              
161              
162             =pod
163              
164             =for :stopwords Diab Jerius Smithsonian Astrophysical Observatory
165              
166             =head1 NAME
167              
168             MooX::Tag::TO_HASH - Controlled translation of Moo objects into Hashes
169              
170             =head1 VERSION
171              
172             version 0.01
173              
174             =head1 SYNOPSIS
175              
176             package My::Farm;
177            
178             use Moo;
179             with 'MooX::Tag::TO_HASH';
180            
181             has cow => ( is => 'ro', to_hash => 1 );
182             has duck => ( is => 'ro', to_hash => 'goose,if_exists', );
183             has horse => ( is => 'ro', to_hash => ',if_defined', );
184             has hen => ( is => 'ro', to_hash => 1, );
185             has secret_admirer => ( is => 'ro', );
186            
187             # and somewhere else...
188            
189             use Data::Dumper;
190             my $farm = My::Farm->new(
191             cow => 'Daisy',
192             duck => 'Frank',
193             secret_admirer => 'Fluffy',
194             );
195            
196             print Dumper $farm->TO_HASH;
197              
198             # resulting in
199              
200             $VAR1 = {
201             'hen' => undef,
202             'cow' => 'Daisy',
203             'goose' => 'Frank'
204             };
205              
206             =head1 DESCRIPTION
207              
208             C<MooX::Tag::TO_HASH> is a L<Moo::Role> which provides a controlled method of converting your
209             L<Moo> based object into a hash.
210              
211             Simply mark each field that should be output with the special option
212             C<to_hash> when declaring it:
213              
214             has field => ( is => 'ro', to_hash => 1 );
215              
216             and call the L</TO_HASH> method on your instantiated object.
217              
218             my %hash = $obj->TO_HASH;
219              
220             Fields inherited from superclasses or consumed from roles which use
221             C<MooX::Tag::TO_HASH> are automatically handled.
222              
223             If a field's value is another object, L</TO_HASH> will automatically
224             turn that into a hash if it has its own C<TO_HASH> method (you can
225             also prevent that).
226              
227             By applying a method modifier to the L<TO_HASH> method, you can modify
228             its output after the conversion.
229              
230             =head2 Usage
231              
232             Add the C<to_hash> option to each field which should be
233             included in the hash. C<to_hash> can either take a value of C<1>,
234             e.g.
235              
236             has field => ( is => 'ro', to_hash => 1 );
237              
238             or a string which looks like one of these:
239              
240             alternate_name
241             alternate_name,option_flag,option_flag,...
242             ,option_flag,option_flag,...
243              
244             If C<alternate_name> is specified, that'll be the key used in the
245             output hash.
246              
247             C<option_flag> may be one of the following:
248              
249             =over
250              
251             =item C<if_exists>
252              
253             Only output the field if it was set. This uses L</Moo>'s attribute
254             predicate (one will be added to the field if it not already
255             specified).
256              
257             It I<will> be output if the field is set to C<undef>.
258              
259             A synonym for this is C<omit_if_empty>, for compatibility with
260             L<MooX::TO_JSON>.
261              
262             =item C<if_defined>
263              
264             Only output the field if it was set and its value is defined.
265              
266             =item C<no_recurse>
267              
268             If a field is an object, don't try and turn it into a hash via its
269             C<TO_HASH> method.
270              
271             (Yes, this name is backwards, but eventually a separate C<recurse>
272             option may become available which limits the recursion depth).
273              
274             =back
275              
276             =head1 METHODS
277              
278             =head2 TO_HASH
279              
280             %hash = $obj->TO_HASH
281              
282             This method is added to the consuming class or role.
283              
284             =head1 EXAMPLES
285              
286             =head2 Modifying the generated hash
287              
288             package My::Test::C4;
289            
290             use Moo;
291             with 'MooX::Tag::TO_HASH';
292            
293             has cow => ( is => 'ro', to_hash => 1 );
294             has duck => ( is => 'ro', to_hash => 'goose,if_exists', );
295             has horse => ( is => 'ro', to_hash => ',if_defined', );
296             has hen => ( is => 'ro', to_hash => 1, );
297             has secret_admirer => ( is => 'ro', );
298            
299             # upper case the hash keys
300             around TO_HASH => sub {
301             my ( $orig, $obj ) = @_;
302             my $hash = $obj->$orig;
303             $hash->{ uc $_ } = delete $hash->{$_} for keys %$hash;
304             return $hash;
305             };
306            
307             # and elsewhere:
308             use Data::Dumper;
309            
310             print Dumper(
311             My::Test::C4->new(
312             cow => 'Daisy',
313             hen => 'Ruby',
314             duck => 'Donald',
315             horse => 'Ed',
316             secret_admirer => 'Nemo'
317             )->TO_HASH
318             );
319              
320             # resulting in
321              
322             $VAR1 = {
323             'COW' => 'Daisy',
324             'HEN' => 'Ruby',
325             'GOOSE' => 'Donald',
326             'HORSE' => 'Ed'
327             };
328              
329             =head1 SUPPORT
330              
331             =head2 Bugs
332              
333             Please report any bugs or feature requests to bug-moox-tag-to_hash@rt.cpan.org or through the web interface at: https://rt.cpan.org/Public/Dist/Display.html?Name=MooX-Tag-TO_HASH
334              
335             =head2 Source
336              
337             Source is available at
338              
339             https://gitlab.com/djerius/moox-tag-to_hash
340              
341             and may be cloned from
342              
343             https://gitlab.com/djerius/moox-tag-to_hash.git
344              
345             =head1 SEE ALSO
346              
347             Please see those modules/websites for more information related to this module.
348              
349             =over 4
350              
351             =item *
352              
353             L<MooX::TO_JSON - this is similar, but doesn't handle fields inherited from super classes or consumed from roles.|MooX::TO_JSON - this is similar, but doesn't handle fields inherited from super classes or consumed from roles.>
354              
355             =back
356              
357             =head1 AUTHOR
358              
359             Diab Jerius <djerius@cpan.org>
360              
361             =head1 COPYRIGHT AND LICENSE
362              
363             This software is Copyright (c) 2022 by Smithsonian Astrophysical Observatory.
364              
365             This is free software, licensed under:
366              
367             The GNU General Public License, Version 3, June 2007
368              
369             =cut