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