File Coverage

blib/lib/MooseX/AttributeCloner.pm
Criterion Covered Total %
statement 151 156 96.7
branch 47 56 83.9
condition 50 63 79.3
subroutine 15 15 100.0
pod 5 5 100.0
total 268 295 90.8


line stmt bran cond sub pod time code
1             #############
2             # Created By: setitesuk@gmail.com
3             # Created On: 2009-11-03
4             # Last Updated: 2009-11-09
5              
6             package MooseX::AttributeCloner;
7 1     1   3401982 use Moose::Role;
  1         3  
  1         9  
8 1     1   4743 use Carp qw{carp cluck croak confess};
  1         2  
  1         80  
9 1     1   6 use English qw{-no_match_vars};
  1         2  
  1         9  
10 1     1   1248 use Readonly;
  1         2924  
  1         43  
11              
12 1     1   6 use JSON;
  1         1  
  1         8  
13              
14             our $VERSION = 0.27;
15              
16             Readonly::Scalar our $ATTRIBUTE_METACLASS_TO_SKIP => q[MooseX::Getopt::Meta::Attribute::NoGetopt];
17              
18             =head1 NAME
19              
20             MooseX::AttributeCloner
21              
22             =head1 VERSION
23              
24             0.27
25              
26             =head1 SYNOPSIS
27              
28             package My::Class;
29             use Moose;
30             with qw{MooseX::AttributeCloner};
31              
32             my $NewClassObject = $self->new_with_cloned_attributes(q{New::Class}, {});
33             1;
34              
35             =head1 DESCRIPTION
36              
37             The purpose of this Role is to take all the attributes which have values in the current class,
38             and populate them directly into a new class object. The purpose of which is that if you have data
39             inputted on the command line that needs to propagate through to later class objects, you shouldn't
40             need to do the following
41              
42             my $oNewClass = New::Class->new({
43             attr1 => $self->attr1,
44             attr2 => $self->attr2,
45             ...
46             });
47              
48             Which is going to get, quite frankly, tedious in the extreme. Particularly when you have more 2 class
49             objects in your chain.
50              
51             =head1 SUBROUTINES/METHODS
52              
53             =head2 new_with_cloned_attributes
54              
55             This takes a package name as the first argument, plus an optional additional $arg_refs hash. It will
56             return a class object of the package populated with any matching attribute data from the current object,
57             plus anything in the $arg_refs hash.
58              
59             =cut
60              
61             sub new_with_cloned_attributes {
62 4     4 1 3847 my ($self, $package, $arg_refs) = @_;
63 4   100     368 $arg_refs ||= {};
64              
65 4 100 66     25 if (!ref$self && ref$package) {
66 1         2 my $temp = $self;
67 1         2 $self = $package;
68 1         2 $package = $temp;
69             }
70              
71             eval {
72 4         12 my $package_file_name = $package;
73 4         10 $package_file_name =~ s{::}{/}gxms;
74 4 50       18 if ($package_file_name !~ /[.]pm\z/xms) {
75 4         9 $package_file_name .= q{.pm};
76             }
77 4         42 require $package_file_name;
78 4 50       8 } or do {
79 0         0 confess $EVAL_ERROR;
80             };
81 4         21 $self->_hash_of_attribute_values($arg_refs);
82 4         126 return $package->new($arg_refs);
83             }
84              
85             =head2 attributes_as_command_options
86              
87             returns all the built attributes that are not objects as a string of command_line options
88             only the first level of references will be passed through, multi-dimensional data structures
89             should use the json serialisation option and deserialise it on object construction or script
90             running
91              
92             my $command_line_string = $class->attributes_as_command_options();
93             --attr1 val1 --attr2 val2
94              
95             By default, it returns the options with a double dash, space separated, and not quoted (as above). These can be switched by submitting a hash_ref as follows
96              
97             my $command_line_string = $class->attributes_as_command_options({
98             equal => 1,
99             quotes => 1,
100             single_dash => 1,
101             });
102              
103             Although, if you are passing a hash_ref, this will always be space separated attr val.
104              
105             You may exclude some values if you wish. To do this, use the example below
106              
107             my $command_line_string = $class->attributes_as_command_options({
108             excluded_attributes => [ qw( init_arg1 init_arg2 init_arg3 ) ],
109             });
110              
111             Note here you are using the init_arg, rather than any reader/accessor method names to exclude the option, as it is the init_arg which will be used in the command_line string generated
112              
113             Sometimes you may have floating attributes for argv and ARGV (we have discovered this with MooseX::Getopt). As such, these are being treated as 'special', and these will be excluded by default. You can request them to be included as follows.
114              
115             my $command_line_string = $class->attributes_as_command_options({
116             included_argv_attributes => [ qw( argv ARGV ) ],
117             });
118              
119             No additional command_line params can be pushed into this, it only deals with the attributes already set in the current object
120              
121             Note, it is your responsibility to know where you may need any of these to be on or off, unless they have no init_arg (init_arg => undef)
122              
123             From v0.25, any attributes with a metaclass of NoGetopt will not be translated to a command line as they would cause a failure to any new_with_options with MooseX::Getopt. You can override this by passing an additional argument 'include_no_getopt'
124              
125             my $command_line_string = $class->attributes_as_command_options({
126             included_argv_attributes => [ qw( argv ARGV ) ],
127             include_no_getopt => 1,
128             });
129              
130             =cut
131              
132             sub attributes_as_command_options {
133 11     11 1 7950 my ($self,$arg_refs) = @_;
134 11   100     47 $arg_refs ||= {};
135              
136 11         49 my $attributes = $self->_hash_of_attribute_values({command_options => 1});
137              
138             # exclude any specified init_args
139 11         111 $self->_exclude_args($attributes, $arg_refs);
140              
141             # remove any objects from the hash
142 7         26 $self->_traverse_hash($attributes);
143              
144 7         9 my @command_line_options;
145              
146             # version 0.21 - force this to be in a sorted order, so that results can be consistent should operating systems return keys in a different order
147 7         12 foreach my $key (sort keys %{$attributes}) {
  7         43  
148              
149 31 100 66     124 if (! ref $attributes->{$key}
      66        
150             &&
151             ( (ref( $self->meta()->get_attribute($key) ) ne $ATTRIBUTE_METACLASS_TO_SKIP ) || $arg_refs->{include_no_getopt} ) ) {
152 18         563 my $string = $self->_create_string($key, $attributes->{$key}, $arg_refs);
153 18         28 push @command_line_options, $string;
154 18         41 next;
155             }
156              
157 13 100       74 if (ref $attributes->{$key} eq q{HASH}) {
158              
159 5         8 foreach my $h_key (sort {$a cmp $b} keys %{$attributes->{$key}}) {
  5         14  
  5         28  
160              
161 10 50 33     81 if (defined $attributes->{$key}->{$h_key} && ! ref $attributes->{$key}->{$h_key}) { # don't pass through empty strings or references
162 10         39 my $string = $self->_create_string($key, qq{$h_key=$attributes->{$key}->{$h_key}}, $arg_refs, 1);
163 10         24 push @command_line_options, $string;
164             }
165              
166             }
167              
168             }
169              
170 13 100       44 if (ref $attributes->{$key} eq q{ARRAY}) {
171              
172 7         9 foreach my $value (@{$attributes->{$key}}) {
  7         19  
173              
174 30 100 100     126 if (defined $value && ! ref $value) { # don't pass through empty strings or references
175 18         43 my $string = $self->_create_string($key, $value, $arg_refs);
176 18         65 push @command_line_options, $string;
177             }
178              
179             }
180              
181             }
182              
183             }
184              
185 7         13 my $clo_string;
186 7 100       17 if ($arg_refs->{single_dash}) {
187 2         8 $clo_string = join q{ -}, @command_line_options;
188 2         7 $clo_string = q{-} . $clo_string;
189             } else {
190 5         16 $clo_string = join q{ --}, @command_line_options;
191 5         12 $clo_string = q{--} . $clo_string;
192             }
193 7         59 return $clo_string;
194             }
195              
196             =head2 attributes_as_json
197              
198             returns all the built attributes that are not objects as a JSON string
199              
200             my $sAttributesAsJSON = $class->attributes_as_json();
201              
202             =head2 attributes_as_escaped_json
203              
204             as attributes_as_json, except it is an escaped JSON string, so that this could be used on a command line
205              
206             my $sAttributesAsEscapedJSON = $class->attributes_as_escaped_json();
207              
208             This uses JSON to generate the string, removing any objects before stringifying, and then parses it through a regex to generate a string with escaped characters
209             Note, because objects are removed, arrays will remain the correct length, but have null in them
210             =cut
211              
212             sub attributes_as_escaped_json {
213 1     1 1 1482 my ($self) = @_;
214 1         5 my $json = $self->attributes_as_json();
215 1         90 $json =~ s{([^\w\d-])}{\\$1}gmxs;
216 1         6 return $json;
217             }
218              
219             sub attributes_as_json {
220 2     2 1 3734 my ($self) = @_;
221              
222 2         10 my $attributes = $self->_hash_of_attribute_values();
223             # remove any objects from the hash
224 2         9 $self->_traverse_hash($attributes);
225 2         11 my $json = to_json($attributes);
226 2         84 return $json;
227             }
228              
229             =head2 attributes_as_hashref
230              
231             Returns a hashref of the attributes this object has built,
232             optionally excluding any specified attributes.
233             Includes objects which may have been built.
234              
235             my $hAttributesAsHashref = $class->attributes_as_hashref({
236             excluded_attributes => [ qw( init_arg1 init_arg2 init_arg3 ) ],
237             });
238              
239             Note here you are using the init_arg, rather than any reader/accessor method names to exclude the option
240              
241             =cut
242              
243             sub attributes_as_hashref {
244 1     1 1 2 my ( $self, $arg_refs ) = @_;
245 1   50     4 $arg_refs ||= {};
246 1         4 my $attributes = $self->_hash_of_attribute_values();
247              
248             # exclude any specified init_args
249 1         3 $self->_exclude_args($attributes, $arg_refs);
250 1         3 return $attributes;
251             }
252              
253             ###############
254             # private methods
255              
256              
257             # a hash_ref of attribute values from $self, where built
258             # either acts on a provided hash_ref, or will return a new one
259             sub _hash_of_attribute_values {
260 18     18   32 my ($self, $arg_refs) = @_;
261 18   100     52 $arg_refs ||= {};
262              
263 18         31 my $command_options = $arg_refs->{command_options};
264 18         29 delete$arg_refs->{command_options};
265              
266 18         78 my @attributes = $self->meta->get_all_attributes();
267 18         1177 foreach my $attr (@attributes) {
268 276   66     9777 my $reader = $attr->reader() || $attr->accessor();
269 276         833 my $init_arg = $attr->init_arg();
270              
271             # if there is no reader/accessor method, then we can't read the attribute value, so skip
272 276 50       539 next if (!$reader);
273              
274             # if the reader/accessor are private, then we don't want to pass it around
275 276 50       557 next if ($reader =~ /\A_/xms);
276              
277             # if lazy_build, then will only propagate data if it is built, saving any expensive build routines.
278             # obviously, this has the effect that you may need to do it twice, or force a build before the cloning of data
279             # NOTE: this doesn't account for those which are lazy, and have a builder, but no predicate (generated by lazy_build flag only)
280 276 100       660 if ($attr->{predicate}) {
281 18         28 my $pred = $attr->{predicate};
282 18 100       556 next if !$self->$pred();
283             }
284              
285 274 100 33     9286 if ($init_arg && !exists$arg_refs->{$init_arg} && defined $self->$reader()) {
      66        
286 100 50 100     3947 next if ( $attr->type_constraint() eq q{Bool} && $command_options && ! $self->$reader );
      66        
287 100 100 100     10385 $arg_refs->{$init_arg} = $attr->type_constraint() eq q{Bool} && $command_options ? q{} : $self->$reader();
288             }
289             }
290              
291 18         337 return $arg_refs;
292             }
293              
294             # remove any objects from a hash
295             sub _traverse_hash {
296 24     24   36 my ($self, $hash) = @_;
297 24         27 my @keys_to_delete;
298 24         27 foreach my $key (keys %{$hash}) {
  24         72  
299 78 100       193 next if (!ref $hash->{$key});
300 25 100       66 if (ref$hash->{$key} eq q{HASH}) {
301 7         27 $self->_traverse_hash($hash->{$key});
302 7         10 next;
303             }
304 18 100       51 if (ref$hash->{$key} eq q{ARRAY}) {
305 9         30 $hash->{$key} = $self->_traverse_array($hash->{$key});
306 9         16 next;
307             }
308 9         51 push @keys_to_delete, $key;
309             }
310 24         54 foreach my $key (@keys_to_delete) {
311 9         22 delete $hash->{$key};
312             }
313 24         41 return $hash;
314             }
315              
316             # remove any objects from an array
317             sub _traverse_array {
318 9     9   15 my ($self, $array) = @_;
319 9         15 my @wanted_items;
320 9         14 foreach my $item (@{$array}) {
  9         20  
321 40 100       81 if (!ref $item) {
322 24         33 push @wanted_items, $item;
323 24         30 next;
324             }
325 16 100       37 if (ref$item eq q{HASH}) {
326 8         20 $self->_traverse_hash($item);
327 8         13 push @wanted_items, $item;
328 8         9 next;
329             }
330 8 50       19 if (ref$item eq q{ARRAY}) {
331 0         0 $item = $self->_traverse_array($item);
332 0         0 push @wanted_items, $item;
333 0         0 next;
334             }
335 8         27 push @wanted_items, undef;
336             }
337 9         25 return \@wanted_items;
338             }
339              
340             ############
341             # remove any unwanted options by the init_arg they would have
342              
343             sub _exclude_args {
344 12     12   22 my ($self, $attributes, $arg_refs) = @_;
345 12   100     68 my $excluded_attributes = $arg_refs->{excluded_attributes} || [];
346 12         21 delete $arg_refs->{excluded_attributes};
347 12   100     53 my $included_argv_attributes = $arg_refs->{included_argv_attributes} || [];
348 12         20 delete $arg_refs->{included_argv_attributes};
349 12 0 33     32 if (!$excluded_attributes && !$included_argv_attributes) {
350 0         0 return 1;
351             }
352              
353 12 100 100     74 if ( ! ref$excluded_attributes || ref$excluded_attributes ne q{ARRAY} ) {
354 2         39 croak qq{Your excluded_attributes are not in an arrayref - $excluded_attributes};
355             }
356              
357 10 100 100     64 if ( ! ref$included_argv_attributes || ref$included_argv_attributes ne q{ARRAY} ) {
358 2         24 croak qq{Your included_argv_attributes are not in an arrayref - $included_argv_attributes};
359             }
360              
361 8         15 foreach my $exclusion (@{$excluded_attributes}) {
  8         40  
362 3         6 delete $attributes->{$exclusion};
363             }
364              
365 8         18 my $wanted_argv = {};
366 8         11 foreach my $inclusion (@{$included_argv_attributes}) {
  8         17  
367 2         7 $wanted_argv->{$inclusion}++;
368             }
369              
370 8         15 foreach my $argv ( qw{ argv ARGV }) {
371 16 100       44 if (!$wanted_argv->{$argv}) {
372 14         28 delete $attributes->{$argv};
373             }
374             }
375              
376 8         24 return 1;
377             }
378              
379             # create a command line string
380              
381             sub _create_string {
382 46     46   66 my ($self, $attr, $value, $arg_refs, $hash) = @_;
383 46         54 my $string = $attr;
384              
385 46 100 100     330 if ($value ne q{} && !$hash && $arg_refs->{equal}) {
      100        
386 10         19 $string .= q{=};
387             } else {
388 36         59 $string .= q{ }; # default attr value separator
389             }
390              
391 46 100 100     172 if ($value ne q{} && $arg_refs->{quotes}) {
392 14         24 $string .= qq{"$value"};
393             } else {
394 32         39 $string .= qq{$value}; # default no quote of value
395             }
396 46         94 return $string;
397             }
398              
399             1;
400             __END__
401              
402             =head1 DIAGNOSTICS
403              
404             =head1 CONFIGURATION AND ENVIRONMENT
405              
406             =head1 DEPENDENCIES
407              
408             =over
409              
410             =item Moose::Role
411              
412             =item Carp
413              
414             =item English -no_match_vars
415              
416             =item Readonly
417              
418             =item JSON
419              
420             =back
421              
422             =head1 INCOMPATIBILITIES
423              
424             =head1 BUGS AND LIMITATIONS
425              
426             This is more than likely to have bugs in it. Please contact me with any you find (or submit to RT)
427             and any patches.
428              
429             =head1 AUTHOR
430              
431             setitesuk
432              
433             =head1 LICENSE AND COPYRIGHT
434              
435             Copyright (C) 2011 Andy Brown (setitesuk@gmail.com)
436              
437             This program is free software: you can redistribute it and/or modify
438             it under the terms of the GNU General Public License as published by
439             the Free Software Foundation, either version 3 of the License, or
440             (at your option) any later version.
441              
442             This program is distributed in the hope that it will be useful,
443             but WITHOUT ANY WARRANTY; without even the implied warranty of
444             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
445             GNU General Public License for more details.
446              
447             You should have received a copy of the GNU General Public License
448             along with this program. If not, see <http://www.gnu.org/licenses/>.