File Coverage

blib/lib/Config/Model/WarpedNode.pm
Criterion Covered Total %
statement 112 117 95.7
branch 28 36 77.7
condition 10 19 52.6
subroutine 24 24 100.0
pod 5 13 38.4
total 179 209 85.6


line stmt bran cond sub pod time code
1             #
2             # This file is part of Config-Model
3             #
4             # This software is Copyright (c) 2005-2022 by Dominique Dumont.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU Lesser General Public License, Version 2.1, February 1999
9             #
10             package Config::Model::WarpedNode 2.153; # TRIAL
11              
12 22     22   808 use Mouse;
  22         57  
  22         219  
13              
14 22     22   11305 use Carp qw(cluck croak);
  22         70  
  22         1565  
15              
16 22     22   170 use Config::Model::Exception;
  22         50  
  22         539  
17 22     22   2016 use Config::Model::Warper;
  22         50  
  22         603  
18 22     22   189 use Data::Dumper ();
  22         65  
  22         500  
19 22     22   137 use Log::Log4perl qw(get_logger :levels);
  22         61  
  22         197  
20 22     22   3534 use Storable qw/dclone/;
  22         85  
  22         1461  
21 22     22   186 use Scalar::Util qw/weaken/;
  22         49  
  22         11230  
22              
23             extends qw/Config::Model::AnyThing/;
24              
25             with "Config::Model::Role::NodeLoader";
26             with "Config::Model::Role::Grab";
27              
28             my $logger = get_logger("Tree::Node::Warped");
29              
30             # don't authorize to warp 'morph' parameter as it may lead to
31             # difficult maintenance
32              
33             # status is not warpable either as an obsolete parameter must stay
34             # obsolete
35              
36             my @allowed_warp_params = qw/config_class_name level gist/;
37              
38             has 'backup' => ( is => 'rw', isa => 'HashRef', default => sub { {}; } );
39              
40             has 'warp' => ( is => 'rw', isa => 'HashRef', default => sub { {}; });
41             has 'morph' => ( is => 'ro', isa => 'Bool', default => 0 );
42              
43             has warper => ( is => 'rw', isa => 'Config::Model::Warper' );
44              
45             my @backup_list = @allowed_warp_params;
46              
47             around BUILDARGS => sub {
48             my $orig = shift;
49             my $class = shift;
50             my %args = @_;
51             my %h = map { ( $_ => $args{$_} ); } grep { defined $args{$_} } @backup_list;
52             return $class->$orig( backup => dclone( \%h ), @_ );
53             };
54              
55             sub BUILD {
56 129     129 1 472 my $self = shift;
57              
58             # WarpedNode registers this object in a Value object (the
59             # warper). When the warper gets a new value, it modifies the
60             # WarpedNode according to the data passed by the user.
61              
62 129         431 my $warp_info = $self->warp;
63 129   50     500 $warp_info->{follow} //= {};
64 129   50     343 $warp_info->{rules} //= [];
65 129         2227 my $w = Config::Model::Warper->new(
66             warped_object => $self,
67             %$warp_info,
68             allowed => \@allowed_warp_params
69             );
70              
71 129         1188 $self->warper($w);
72 129         1139 return $self;
73             }
74              
75             sub config_model {
76 340     340 0 691 my $self = shift;
77 340         2419 return $self->parent->config_model;
78             }
79              
80             # Forward selected methods (See man perltootc)
81             foreach my $method (
82             qw/fetch_element config_class_name copy_from get_element_name
83             get_info fetch_gist has_element is_element_available element_type load
84             fetch_element_value get_type get_cargo_type dump_tree needs_save
85             describe get_help get_help_as_text children get set accept_regexp/
86             ) {
87             # to register new methods in package
88 22     22   207 no strict "refs"; ## no critic TestingAndDebugging::ProhibitNoStrict
  22         56  
  22         27839  
89              
90             *$method = sub {
91 1423     1423   2436 my $self = shift;
        397      
92              
93 1423 100       2997 if ($self->check) {
94 1422         5157 return $self->{data}->$method(@_);
95             }
96              
97             # return undef if no class was warped in
98 1         4 return ;
99             };
100             }
101              
102             sub name {
103 2391     2391 1 3723 my $self = shift;
104 2391         12606 return $self->location;
105             }
106              
107             sub is_accessible {
108 2     2 1 5 my $self = shift;
109 2 50       23 return defined $self->{data} ? 1 : 0;
110             }
111              
112             sub get_actual_node {
113 261     261 1 467 my $self = shift;
114 261         702 $self->check;
115 261         578 return $self->{data}; # might be undef
116             }
117              
118             sub check {
119 1684     1684 0 2561 my $self = shift;
120 1684   50     4809 my $check = shift || 'yes ';
121              
122             # must croak if element is not available
123 1684 100       3948 if ( not defined $self->{data} ) {
124              
125             # a node can be retrieved either for a store operation or for
126             # a fetch.
127 1 50       3 if ( $check eq 'yes' ) {
128 0         0 Config::Model::Exception::User->throw(
129             object => $self,
130             message => "Object '$self->{element_name}' is not accessible.\n\t"
131             . $self->warp_error
132             );
133             }
134             else {
135 1         3 return 0;
136             }
137             }
138 1683         3764 return 1;
139             }
140              
141             sub set_properties {
142 174     174 0 360 my $self = shift;
143              
144 174         310 my %args = ( %{ $self->backup }, @_ );
  174         907  
145              
146             # mega cleanup
147 174         506 for (@allowed_warp_params) { delete $self->{$_} }
  522         991  
148              
149 174         671 $logger->trace( $self->name . " set_properties called with ",
150             Data::Dumper->Dump( [ \%args ], ['set_properties_args'] ) );
151              
152 174         9554 my $config_class_name = delete $args{config_class_name};
153 174   50     820 my $node_class = delete $args{class} || 'Config::Model::Node';
154              
155 174         763 my @prop_args = ( qw/property level element/, $self->element_name );
156              
157 174         506 my $original_level = $self->config_model->get_element_property(
158             class => $self->parent->config_class_name,
159             @prop_args,
160             );
161              
162             my $next_level =
163             defined $args{level} ? $args{level}
164 174 100       1005 : defined $config_class_name ? $original_level
    100          
165             : 'hidden';
166              
167 174 100       1322 $self->parent->set_element_property( @prop_args, value => $next_level )
168             unless defined $self->index_value;
169              
170 174 100       442 unless ( defined $config_class_name ) {
171 8         24 $self->clear;
172 8         30 return;
173             }
174              
175 166         280 my @args;
176 166 100       424 ( $config_class_name, @args ) = @$config_class_name
177             if ref $config_class_name;
178              
179             # check if some action is needed (ie. create or morph node)
180             return
181             if defined $self->{config_class_name}
182 166 50 33     537 and $self->{config_class_name} eq $config_class_name;
183              
184 166         2109 my $old_object = $self->{data};
185 166         332 my $old_config_class_name = $self->{config_class_name};
186              
187             # create a new object from scratch
188 166         455 my $new_object = $self->create_node( $config_class_name, @args );
189              
190 166         452 $self->{config_class_name} = $config_class_name;
191 166         368 $self->{data} = $new_object;
192              
193 166 100 100     584 if ( defined $old_object and $self->{morph} ) {
194              
195             # there an old object that we need to translate
196 24 50       82 $logger->debug( "WarpedNode: morphing ", $old_object->name, " to ", $new_object->name )
197             if $logger->is_debug;
198              
199 24         204 $new_object->copy_from( from => $old_object, check => 'skip' );
200             }
201              
202             # bringing a new object does not really modify the content of the config tree.
203             # only changes underneath changes the tree. And these changes below triggers
204             # their own change notif. So there's no need to call notify_change when transitioning
205             # from an undef object into a real object. On the other hand, warping out an object does
206             # NOT trigger notify_changes from below. So notify_change must be called
207 166 50 66     576 if ( defined $old_object and $old_config_class_name) {
208 0         0 my $from = $old_config_class_name ;
209 0   0     0 my $to = $config_class_name // '<undef>';
210 0         0 $self->notify_change( note => "warped node from $from to $to" );
211             }
212              
213             # need to call trigger on all registered objects only after all is setup
214 166         565 $self->trigger_warp;
215             }
216              
217             sub create_node {
218 166     166 0 286 my $self = shift;
219 166         264 my $config_class_name = shift;
220              
221             my @args = (
222             config_class_name => $config_class_name,
223             instance => $self->{instance},
224             element_name => $self->{element_name},
225 166         917 parent => $self->parent,
226             container => $self->container,
227             );
228              
229 166 100       584 push @args, index_value => $self->index_value if defined $self->index_value;
230              
231 166         658 return $self->load_node(@args);
232             }
233              
234             sub clear {
235 8     8 0 17 my $self = shift;
236 8         119 delete $self->{data};
237             }
238              
239             sub load_data {
240 4     4 1 17 my $self = shift;
241 4 50       19 my %args = @_ > 1 ? @_ : ( data => shift );
242 4         11 my $data = $args{data};
243 4         18 my $check = $self->_check_check( $args{check} );
244              
245 4 50       19 if ( ref($data) ne 'HASH' ) {
246 0         0 Config::Model::Exception::LoadData->throw(
247             object => $self,
248             message => "load_data called with non hash ref arg",
249             wrong_data => $data,
250             );
251             }
252              
253 4         17 $self->get_actual_node->load_data(%args);
254              
255             }
256              
257             sub is_auto_write_for_type {
258 2     2 0 5 my $self = shift;
259 2         7 $self->get_actual_node->is_auto_write_for_type(@_);
260             }
261              
262             # register warper that goes through this path when looking for warp master value
263             sub register {
264 124     124 0 301 my ( $self, $warped, $w_idx ) = @_;
265              
266 124         322 $logger->debug( "WarpedNode: " . $self->name, " registered " . $warped->name );
267              
268             # weaken only applies to the passed reference, and there's no way
269             # to duplicate a weak ref. Only a strong ref is created. See
270             # qw(weaken) module for weaken()
271 124         1051 my @tmp = ( $warped, $w_idx );
272 124         538 weaken( $tmp[0] );
273 124         186 push @{ $self->{warp_these_objects} }, \@tmp;
  124         577  
274             }
275              
276             sub trigger_warp {
277 166     166 0 383 my $self = shift;
278              
279             # warp_these_objects is modified by the calls below, so this copy
280             # must be done before the loop
281 166 100       287 my @list = @{ $self->{warp_these_objects} || [] };
  166         794  
282              
283 166         2068 foreach my $ref (@list) {
284 7         44 my ( $warped, $warp_index ) = @$ref;
285 7 50       15 next unless defined $warped; # $warped is a weak ref and may vanish
286              
287             # pure warp of object
288 7         15 $logger->debug( "node trigger_warp: from '",
289             $self->name, "' warping '", $warped->name, "'" );
290              
291             # FIXME: this does not trigger new registration (or removal thereof)...
292 7         68 $warped->refresh_affected_registrations( $self->location );
293              
294             #$warped->refresh_values_from_master ;
295 7         33 $warped->do_warp;
296 7         30 $logger->debug( "node trigger_warp: from '",
297             $self->name, "' warping '", $warped->name, "' done" );
298             }
299             }
300              
301             # FIXME: should we un-register ???
302              
303             1;
304              
305             # ABSTRACT: Node that change config class properties
306              
307             __END__
308              
309             =pod
310              
311             =encoding UTF-8
312              
313             =head1 NAME
314              
315             Config::Model::WarpedNode - Node that change config class properties
316              
317             =head1 VERSION
318              
319             version 2.153
320              
321             =head1 SYNOPSIS
322              
323             use Config::Model;
324              
325             my $model = Config::Model->new;
326             foreach (qw/X Y/) {
327             $model->create_config_class(
328             name => "Class$_",
329             element => [ foo => {qw/type leaf value_type string/} ]
330             );
331             }
332             $model->create_config_class(
333             name => "MyClass",
334              
335             element => [
336             master_switch => {
337             type => 'leaf',
338             value_type => 'enum',
339             choice => [qw/cX cY/]
340             },
341              
342             'a_warped_node' => {
343             type => 'warped_node',
344             warp => }
345             follow => { ms => '! master_switch' },
346             rules => [
347             '$ms eq "cX"' => { config_class_name => 'ClassX' },
348             '$ms eq "cY"' => { config_class_name => 'ClassY' },
349             ]
350             }
351             },
352             ],
353             );
354              
355             my $inst = $model->instance(root_class_name => 'MyClass' );
356             my $root = $inst->config_root ;
357              
358             print "Visible elements: ",join(' ',$root->get_element_name),"\n" ;
359             # Visible elements: master_switch
360              
361             $root->load( steps => 'master_switch=cX' );
362             print "Visible elements: ",join(' ',$root->get_element_name),"\n" ;
363             # Visible elements: master_switch a_warped_node
364              
365             my $node = $root->grab('a_warped_node') ;
366             print "a_warped_node class: ",$node->config_class_name,"\n" ;
367             # a_warped_node class: ClassX
368              
369             $root->load( steps => 'master_switch=cY' );
370             print "a_warped_node class: ",$node->config_class_name,"\n" ;
371             # a_warped_node class: ClassY
372              
373             =head1 DESCRIPTION
374              
375             This class provides a way to change dynamically the configuration
376             class (or some other properties) of a node. The changes are done
377             according to the model declaration.
378              
379             This declaration specifies one (or several) leaf in the
380             configuration tree that triggers the actual property change of the
381             warped node. This leaf is also referred as I<warp master>.
382              
383             When the warp master(s) value(s) changes, C<WarpedNode> creates an instance
384             of the new class required by the warp master.
385              
386             If the morph parameter is set, the values held by the old object are
387             (if possible) copied to the new instance of the object using
388             L<copy_from|Config::Model::Node/"copy_from ( another_node_object )">
389             method.
390              
391             Warped node can alter the following properties:
392              
393             config_class_name
394             level
395              
396             =head1 Constructor
397              
398             C<WarpedNode> should not be created directly.
399              
400             =head1 Warped node model declaration
401              
402             =head2 Parameter overview
403              
404             A warped node must be declared with the following parameters:
405              
406             =over
407              
408             =item type
409              
410             Always set to C<warped_node>.
411              
412             =item follow
413              
414             L<Grab string|Config::Model::Role::Grab/grab"> leading to the
415             C<Config::Model::Value> warp master.
416             See L<Config::Model::Warper/"Warp follow argument"> for details.
417              
418             =item morph
419              
420             boolean. If 1, C<WarpedNode> tries to recursively copy the value from
421             the old object to the new object using
422             L<copy_from method|Config::Model::Node/"copy_from ( another_node_object )">.
423             When a copy is not possible, undef values
424             are assigned to object elements.
425              
426             =item rules
427              
428             Hash or array ref that specify the property change rules according to the
429             warp master(s) value(s).
430             See L<Config::Model::Warper/"Warp rules argument"> for details
431             on how to specify the warp master values (or combination of values).
432              
433             =back
434              
435             =head2 Effect declaration
436              
437             For a warped node, the effects are declared with these parameters:
438              
439             =over 8
440              
441             =item B<config_class_name>
442              
443             When requested by the warp master,the C<WarpedNode> creates a new
444             object of the type specified by this parameter:
445              
446             XZ => { config_class_name => 'SlaveZ' }
447              
448             Instead of a string, you can an array ref which contains the class
449             name and constructor arguments :
450              
451             XY => { config_class_name => ['SlaveY', foo => 'bar' ], },
452              
453             =item B<class>
454              
455             Specify a Perl class to implement the above config class. This Perl Class B<must> inherit
456             L<Config::Model::Node>.
457              
458             =back
459              
460             =head1 Forwarded methods
461              
462             The following methods are forwarded to contained node:
463              
464             fetch_element config_class_name get_element_name has_element
465             is_element_available element_type load fetch_element_value get_type
466             get_cargo_type describe
467              
468             =head1 Methods
469              
470             =head2 name
471              
472             Return the name of the node (even if warped out).
473              
474             =head2 is_accessible
475              
476             Returns true if the node hidden behind this warped node is accessible,
477             i.e. the warp master have values so a node was warped in.
478              
479             =head2 get_actual_node
480              
481             Returns the node object hidden behind the warped node. Croaks if the
482             node is not accessible.
483              
484             =head2 load_data
485              
486             Parameters: C<< ( hash_ref ) >>
487              
488             Load configuration data with a hash ref. The hash ref key must match
489             the available elements of the node carried by the warped node.
490              
491             =head1 EXAMPLE
492              
493             $model ->create_config_class
494             (
495             element =>
496             [
497             tree_macro => { type => 'leaf',
498             value_type => 'enum',
499             choice => [qw/XX XY XZ ZZ/]
500             },
501             bar => {
502             type => 'warped_node',
503             follow => '! tree_macro',
504             morph => 1,
505             rules => [
506             XX => { config_class_name
507             => [ 'ClassX', 'foo' ,'bar' ]}
508             XY => { config_class_name => 'ClassY'},
509             XZ => { config_class_name => 'ClassZ'}
510             ]
511             }
512             ]
513             );
514              
515             In the example above we see that:
516              
517             =over
518              
519             =item *
520              
521             The 'bar' slot can refer to a C<ClassX>, C<ClassZ> or C<ClassY> object.
522              
523             =item *
524              
525             The warper object is the C<tree_macro> attribute of the root of the
526             object tree.
527              
528             =item *
529              
530             When C<tree_macro> is set to C<ZZ>, C<bar> is not available. Trying to
531             access C<bar> raises an exception.
532              
533             =item *
534              
535             When C<tree_macro> is changed from C<ZZ> to C<XX>,
536             C<bar> refers to a brand new C<ClassX>
537             object constructed with C<< ClassX->new(foo => 'bar') >>
538              
539             =item *
540              
541             Then, if C<tree_macro> is changed from C<XX> to C<XY>, C<bar>
542             refers to a brand new C<ClassY> object. But in this case, the object is
543             initialized with most if not all the attributes of C<ClassX>. This copy
544             is done whenever C<tree_macro> is changed.
545              
546             =back
547              
548             =head1 AUTHOR
549              
550             Dominique Dumont, (ddumont at cpan dot org)
551              
552             =head1 SEE ALSO
553              
554             L<Config::Model::Instance>,
555             L<Config::Model>,
556             L<Config::Model::HashId>,
557             L<Config::Model::ListId>,
558             L<Config::Model::AnyThing>,
559             L<Config::Model::Warper>,
560             L<Config::Model::WarpedNode>,
561             L<Config::Model::Value>
562              
563             =head1 AUTHOR
564              
565             Dominique Dumont
566              
567             =head1 COPYRIGHT AND LICENSE
568              
569             This software is Copyright (c) 2005-2022 by Dominique Dumont.
570              
571             This is free software, licensed under:
572              
573             The GNU Lesser General Public License, Version 2.1, February 1999
574              
575             =cut