File Coverage

blib/lib/Config/Model/SearchElement.pm
Criterion Covered Total %
statement 144 151 95.3
branch 38 48 79.1
condition 9 15 60.0
subroutine 19 20 95.0
pod 9 12 75.0
total 219 246 89.0


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::SearchElement 2.153; # TRIAL
11              
12 59     59   453 use Log::Log4perl qw(get_logger :levels);
  59         131  
  59         448  
13 59     59   7950 use Carp;
  59         157  
  59         3115  
14 59     59   377 use strict;
  59         151  
  59         1201  
15 59     59   287 use warnings;
  59         128  
  59         1965  
16              
17 59     59   395 use Config::Model::Exception;
  59         174  
  59         126785  
18              
19             my $logger = get_logger("Model::Searcher");
20              
21             sub new {
22 8     8 0 17 my $type = shift;
23 8         26 my %args = @_;
24              
25 8         17 my $self = {};
26 8         18 foreach my $p (qw/model node/) {
27 16 50       62 $self->{$p} = delete $args{$p}
28             or croak "Searcher->new: Missing $p parameter";
29             }
30              
31 8         24 bless $self, $type;
32              
33 8         26 my $root_class = $self->{node}->config_class_name;
34              
35 8         32 $self->{data} = $self->_sniff_class( $root_class, {} );
36              
37 8         56 return $self;
38             }
39              
40             # to verify the data structure returned by search_element, you can used
41             # either Data::Dumper or Tk::ObjScanner (both are available on CPAN)
42              
43             sub _sniff_class {
44 103     103   219 my ( $self, $class, $found_ref ) = @_;
45              
46 103         143 my %h;
47 103         186 my $model = $self->{model};
48 103         284 my $c_model = $model->get_model_clone($class);
49              
50 103         5904 $logger->trace("sniffing config class $class");
51              
52             croak "Recursive config class $class detected, aborting..."
53 103 50       920 if defined $found_ref->{$class};
54              
55 103         199 $found_ref->{$class} = 1;
56              
57 103         278 my @elements = $model->get_element_name(
58             class => $class,
59             );
60              
61 103         202 foreach my $element (@elements) {
62 529         937 my $element_model = $c_model->{element}{$element};
63 529         768 my $element_type = $element_model->{type};
64 529         695 my $cargo = $element_model->{cargo};
65 529 100       899 my $c_type = defined $cargo ? $cargo->{type} : '';
66             my $cfg_class_name =
67             defined $cargo
68             ? $cargo->{config_class_name}
69 529 100       859 : $element_model->{config_class_name};
70 529         1225 my %local_found = %$found_ref;
71              
72 529 100 100     1985 if ( $element_type =~ /(warped_)?node/
73             or $c_type =~ /(warped_)?node/ ) {
74 76 100 100     380 my $tmp =
75             $element_type eq 'node' || $c_type eq 'node'
76             ? $self->_sniff_class( $cfg_class_name, \%local_found )
77             : $self->_sniff_warped_node( $element_model, \%local_found );
78              
79             # merge all tmp in %h
80 76         236 for (keys %$tmp) { $h{$_}{next_step}{$element} = $tmp->{$_}; }
  466         1060  
81             }
82             else {
83 453         1438 $h{$element}{next_step}{$element} = '';
84             }
85             }
86 103         387 $logger->trace("done sniffing config class $class");
87 103         1218 return \%h;
88             }
89              
90             sub _sniff_warped_node {
91 19     19   44 my ( $self, $element_model, $found_ref ) = @_;
92              
93 19         27 my %warp_tmp;
94 19         35 my $ref = $element_model->{warp}{rules};
95 19 50       64 my @rules = ref $ref eq 'HASH' ? %$ref : @$ref;
96              
97 19         56 for ( my $r_idx = 0 ; $r_idx < $#rules ; $r_idx += 2 ) {
98 38         85 my $res = $rules[ $r_idx + 1 ]{config_class_name};
99 38 50       72 my $sub_class = ref $res ? $res->[0] : $res;
100              
101             # sniff all classes mentionned in warped node rules
102 38         86 my %local_found = %$found_ref;
103 38         98 my $tmp = $self->_sniff_class( $sub_class, \%local_found );
104              
105             # merge all tmp in %warp_tmp
106 38         129 for ( keys %$tmp ) { $warp_tmp{$_}{next_class}{$sub_class} = $tmp->{$_}; }
  220         588  
107             }
108              
109 19         48 return \%warp_tmp;
110             }
111              
112             sub get_searchable_elements {
113 1     1 1 4 my $self = shift;
114 1         2 sort keys %{ $self->{data} };
  1         17  
115             }
116              
117             sub prepare {
118 7     7 1 13 my $self = shift;
119 7         21 my %args = @_;
120              
121 7         13 foreach my $p (qw/element/) {
122 7 50       26 $self->{$p} = delete $args{$p}
123             or croak "Searcher->prepare: Missing $p parameter";
124             }
125              
126 7         22 $self->reset; # initialize the search engine
127              
128 7 50       16 unless ( defined $self->{search_tree} ) {
129 0         0 my $searched = $self->{element};
130 0         0 my $root_class = $self->{node}->config_class_name;
131             Config::Model::Exception::User->throw(
132             message => "Searcher cannot find element '$searched' "
133             . "from $root_class. Found only "
134 0         0 . join( ' ', sort keys %{ $self->{data} } ) );
  0         0  
135             }
136              
137 7         138 return $self;
138             }
139              
140             sub reset {
141 10     10 1 19 my $self = shift;
142              
143 10         18 my $searched = $self->{element};
144 10         23 $self->{search_tree} = $self->{data}{$searched};
145 10         27 $self->{current}{object} = $self->{node};
146 10         20 $self->{current}{element_name} = 'Root';
147 10         21 $self->{current}{element_type} = 'node';
148             }
149              
150             sub searched {
151 0     0 1 0 return shift->{element};
152             }
153              
154             sub next_step {
155 23     23 1 42 my $self = shift;
156              
157 23         40 my $current_obj = $self->{current}{object};
158              
159 23         35 my @result;
160 23 100       67 if ( $current_obj->get_type =~ /list|hash/ ) {
161 1         5 @result = $current_obj->fetch_all_indexes;
162             }
163             else {
164 22         46 my $next_step = $self->{search_tree}{next_step};
165              
166 22 50       102 @result =
    100          
167             ref $next_step ? sort keys %$next_step
168             : defined $next_step ? die "next_step error"
169             : ();
170             }
171              
172             #my $name = $self->{current}{element_name} ;
173             #print "From $name, next_step is @result\n";
174 23 50       67 return wantarray ? @result : \@result;
175             }
176              
177             sub next_choice {
178 3     3 1 454 my $self = shift;
179 3         5 my $result;
180              
181 3         6 while (1) {
182 7         15 $result = $self->next_step;
183 7         41 $logger->trace("next_choice: result is @$result");
184 7 100       68 return $result if scalar @$result != 1;
185              
186 4         10 $self->choose(@$result);
187             }
188              
189             }
190              
191             sub choose {
192 17     17 1 1606 my $self = shift;
193 17         34 my $choice = shift;
194              
195             #print "choose $choice from node\n";
196 17         25 my $obj = $self->{current}{object};
197 17 100       42 if ( $obj->get_type =~ /hash|list/ ) {
198 1         13 $self->choose_from_id_element($choice);
199             }
200             else {
201 16         44 $self->choose_from_node($choice);
202             }
203             }
204              
205             sub choose_from_id_element {
206 1     1 0 5 my $self = shift;
207 1         4 my $choice = shift;
208              
209             #print "choose $choice from id\n";
210 1         2 my $id_obj = $self->{current}{object};
211 1         8 my $class = $id_obj->config_class_name;
212              
213             # the following line may trigger an exception for warped out
214             # elements
215 1         4 my $next_node = $id_obj->fetch_with_id($choice);
216              
217 1         8 $self->{current}{object} = $next_node;
218 1         4 return $next_node;
219             }
220              
221             sub choose_from_node {
222 16     16 0 28 my $self = shift;
223 16         25 my $choice = shift;
224              
225             #print "choose $choice from node\n";
226 16         30 my $next = $self->{search_tree}{next_step};
227 16         27 my $node = $self->{current}{object};
228 16         45 my $node_class = $node->config_class_name;
229              
230 16 50 33     73 if ( ref($next) and not defined $next->{$choice} ) {
231 0         0 Config::Model::Exception::User->throw( message => "Searcher: wrong choice '$choice' "
232             . "from $node_class. expected "
233             . join( ' ', sort keys %$next ) );
234             }
235              
236             # the following line may trigger an exception for warped out
237             # elements
238 16         46 my $next_node = $node->fetch_element($choice);
239              
240             # $next is a scalar for leaf element of a ref for node element
241 16 100       46 if ( $next->{$choice} ) {
242 9         18 my $data = $next->{$choice};
243              
244             # gobble next_class for warped_node element
245 9 100       25 if ( defined $data->{next_class} ) {
246 4         13 my $chosen_class = $next_node->config_class_name;
247 4         10 $data = $data->{next_class}{$chosen_class};
248 4 50       11 unless ( defined $data ) {
249 0         0 Config::Model::Exception::User->throw( message => "Searcher: choice '$choice' "
250             . "from $node_class leads to a warped out node: "
251             . $next_node->warp_error );
252             }
253             }
254              
255 9         16 $self->{search_tree} = $data;
256             }
257             else {
258 7         32 $self->{search_tree} = { next_step => undef };
259 7         19 $next_node = $node->fetch_element($choice);
260             }
261              
262 16         31 $self->{current}{object} = $next_node;
263 16         49 $self->{current}{element_type} = $node->element_type($choice);
264 16         30 $self->{current}{element_name} = $choice;
265 16         44 return $next_node;
266             }
267              
268             sub current_object {
269 2     2 1 1362 my $self = shift;
270 2         10 return $self->{current}{object};
271             }
272              
273             sub auto_choose {
274 5     5 1 36 my $self = shift;
275 5   33     13 my $elt_cb = shift || croak "auto_choose: missing element call back";
276 5   33     13 my $id_cb = shift || croak "auto_choose: missing id call back";
277              
278 5         10 my $object = $self->{current}{object};
279 5         10 while (1) {
280 14         33 my $next_step = $self->next_step;
281 14 100       41 if ( scalar @$next_step == 0 ) {
282              
283             # found target
284 5         36 return $self->{current}{object};
285             }
286              
287 9 100       29 my $next_choice =
288             ( scalar @$next_step == 1 ) ? $next_step->[0] : $elt_cb->( $object, @$next_step );
289              
290 9         1603 $self->_auto_choose_elt( $next_choice, $id_cb );
291             }
292             }
293              
294             sub _auto_choose_elt {
295 9     9   14 my $self = shift;
296 9         13 my $next_choice = shift;
297 9         11 my $id_cb = shift;
298              
299 9         39 $self->choose($next_choice);
300              
301 9         25 my $elt_type = $self->{current}{element_type};
302 9 100       55 if ( $elt_type =~ /list|hash/ ) {
303 2         7 my $object = $self->{current}{object};
304 2         9 my @choice = $object->fetch_all_indexes();
305              
306 2 100       13 my $id =
307             @choice == 1
308             ? $choice[0]
309             : $id_cb->( $object, @choice );
310              
311 2         13 $self->{current}{object} = $object->fetch_with_id($id);
312             }
313             }
314              
315             1;
316              
317             # ABSTRACT: Search an element in a configuration model
318              
319             __END__
320              
321             =pod
322              
323             =encoding UTF-8
324              
325             =head1 NAME
326              
327             Config::Model::SearchElement - Search an element in a configuration model
328              
329             =head1 VERSION
330              
331             version 2.153
332              
333             =head1 SYNOPSIS
334              
335             use Config::Model;
336              
337             # define configuration tree object
338             my $model = Config::Model->new;
339             $model->create_config_class(
340             name => "Foo",
341             element => [
342             [qw/foo bar/] => {
343             type => 'leaf',
344             value_type => 'string'
345             },
346             ]
347             );
348             $model ->create_config_class (
349             name => "MyClass",
350              
351             element => [
352              
353             [qw/foo bar/] => {
354             type => 'leaf',
355             value_type => 'string'
356             },
357             hash_of_nodes => {
358             type => 'hash', # hash id
359             index_type => 'string',
360             cargo => {
361             type => 'node',
362             config_class_name => 'Foo'
363             },
364             },
365             ],
366             ) ;
367              
368             my $inst = $model->instance(root_class_name => 'MyClass' );
369              
370             my $root = $inst->config_root ;
371              
372             # put data
373             my $step = 'foo=FOO hash_of_nodes:fr foo=bonjour -
374             hash_of_nodes:en foo=hello ';
375             $root->load( step => $step );
376              
377             # create searcher for manual search
378             my $searcher = $root->searcher();
379              
380             # looking for foo element in the tree
381             $searcher -> prepare (element => 'foo') ;
382             my @next = $searcher->next_step() ;
383              
384             print "next possible steps: @next\n";
385             # next possible steps: foo hash_of_nodes
386              
387             # Looking for foo below hash_of_nodes
388             $searcher->choose('hash_of_nodes') ;
389             @next = $searcher->next_step() ;
390              
391             print "next possible steps: @next\n";
392             # next possible steps: en fr
393              
394             # Looking for foo below fr
395             $searcher->choose('fr') ;
396             @next = $searcher->next_step() ;
397              
398             print "next possible steps: @next\n";
399             # next possible steps: foo
400              
401             # last step
402             $searcher->choose('foo') ;
403             my $target = $searcher->current_object;
404              
405             print "Found '",$target->location,"'\n";
406             # Found 'hash_of_nodes:fr foo'
407              
408             # automatic search setup
409             my $element_call_back = sub { return 'hash_of_nodes' ;} ;
410             my $id_call_back = sub { return 'en' ;} ;
411              
412             $searcher->reset ;
413             $target = $searcher->auto_choose($element_call_back, $id_call_back) ;
414             print "Automatic search found '",$target->location,"'\n";
415             # Automatic search found 'hash_of_nodes:en foo'
416              
417             =head1 DESCRIPTION
418              
419             This modules provides a way to search for a configuration element in a
420             configuration tree by exploring the configuration model.
421              
422             For instance, suppose that you have a xorg.conf model and you know
423             that you need to tune the C<MergedXinerama> parameter, but you don't
424             remember where is this parameter in the configuration tree. This module
425             guides you through the tree to the(s) node(s) that contain this
426             parameter.
427              
428             This class should be invaluable to construct interactive user interfaces.
429              
430             This module provides 2 search modes:
431              
432             =over
433              
434             =item *
435              
436             A manual search where you are guided step by step to the element
437             you're looking for. At each step, the module returns you the
438             possible paths to choose from. The user has to choose the
439             correct path from the available paths. Most of the time, only one
440             possibility is returned, so the user choice should be
441             straightforward. In other case (more that one choice), the user
442             has to decide the next step.
443              
444             =item *
445              
446             An automatic search where you provide call-back that resolves the
447             ambiguities in case of multiple paths.
448              
449             =back
450              
451             =head1 CONSTRUCTOR
452              
453             The constructor should be used only by L<Config::Model::Node>.
454              
455             =head1 Methods
456              
457             =head2 get_searchable_elements
458              
459             Return the list of elements found in model that can be searched in the
460             configuration tree.
461              
462             =head2 prepare
463              
464             Parameters: C<< (element => ...) >>
465              
466             Prepare the searcher to look for the element passed in the argument.
467             Returns the searcher object (i.e. $self).
468              
469             =head2 reset
470              
471             Re-initialize the search engine to redo the search from start
472              
473             =head2 searched
474              
475             Returns the searched element name.
476              
477             =head1 Manual search
478              
479             =head2 next_step
480              
481             Returns an array (or a ref depending on context)
482             containing the next possible step to find the
483             element you're looking for. The array ref can contain 1 or more
484             elements.
485              
486             If the array ref is empty, you can get the target element with
487             L</"current_object">.
488              
489             =head2 next_choice
490              
491             Returns an array ref containing the next non-obvious choice to find
492             the element you're looking for.
493              
494             If the array ref is empty, you can get the target element with
495             L</"current_object">.
496              
497             =head2 choose
498              
499             Parameters: C<< ( <chosen_element_name> ) >>
500              
501             Tell the search engine your choice. The chosen element name must be
502             one of the possibilities given by L</"next_step">.
503              
504             =head2 current_object
505              
506             Returns the object where the search engine is. It can be
507             a L<node|Config::Model::Node>,
508             a L<list|Config::Model::ListId>,
509             a L<hash|Config::Model::HashId>, or
510             a L<leaf element|Config::Model::Value>.
511              
512             =head1 Automatic search
513              
514             =head2 auto_choose
515              
516             Parameters: C<< ( element_callback, id_call_back) >>
517              
518             Finds the searched element with minimal user interaction.
519              
520             C<element_callback> is called when the search engine finds a node
521             where more than one element can lead to the searched item.
522              
523             C<id_call_back> is called when the search engine finds a hash
524             element or a list element which contain B<no> or B<more than 1>
525             elements. In this case the call-back returns an id that is
526             used by the search engine to get the target element.
527              
528             Both call-back arguments are:
529              
530             =over
531              
532             =item *
533              
534             The current object (as returned by L</"current_object">)
535              
536             =item *
537              
538             A list of possible choices
539              
540             =back
541              
542             For instance, your callback can be :
543              
544             my $id_cb = sub {
545             my ($object,@choices) = @_ ;
546             ....
547             return $choice[1] ;
548             }
549              
550             Both call-back are expected to return a scalar value that is either:
551              
552             =over
553              
554             =item *
555              
556             An element name
557              
558             =item *
559              
560             An id valid for the list or hash element returned by L</"current_object">.
561              
562             =back
563              
564             =head1 AUTHOR
565              
566             Dominique Dumont, (ddumont at cpan dot org)
567              
568             =head1 SEE ALSO
569              
570             L<Config::Model>,
571             L<Config::Model::Node>,
572             L<Config::Model::AnyId>,
573             L<Config::Model::ListId>,
574             L<Config::Model::HashId>,
575             L<Config::Model::Value>,
576              
577             =head1 AUTHOR
578              
579             Dominique Dumont
580              
581             =head1 COPYRIGHT AND LICENSE
582              
583             This software is Copyright (c) 2005-2022 by Dominique Dumont.
584              
585             This is free software, licensed under:
586              
587             The GNU Lesser General Public License, Version 2.1, February 1999
588              
589             =cut