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   345 use Carp;
  59         113  
  59         397  
13 59     59   6607 use strict;
  59         110  
  59         2818  
14 59     59   301 use warnings;
  59         103  
  59         1213  
15 59     59   259  
  59         100  
  59         1666  
16             use Config::Model::Exception;
17 59     59   327  
  59         125  
  59         100860  
18             my $logger = get_logger("Model::Searcher");
19              
20             my $type = shift;
21             my %args = @_;
22 8     8 0 12  
23 8         22 my $self = {};
24             foreach my $p (qw/model node/) {
25 8         14 $self->{$p} = delete $args{$p}
26 8         16 or croak "Searcher->new: Missing $p parameter";
27 16 50       59 }
28              
29             bless $self, $type;
30              
31 8         14 my $root_class = $self->{node}->config_class_name;
32              
33 8         22 $self->{data} = $self->_sniff_class( $root_class, {} );
34              
35 8         23 return $self;
36             }
37 8         46  
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   175 my $model = $self->{model};
45             my $c_model = $model->get_model_clone($class);
46 103         112  
47 103         145 $logger->trace("sniffing config class $class");
48 103         232  
49             croak "Recursive config class $class detected, aborting..."
50 103         4845 if defined $found_ref->{$class};
51              
52             $found_ref->{$class} = 1;
53 103 50       775  
54             my @elements = $model->get_element_name(
55 103         157 class => $class,
56             );
57 103         235  
58             foreach my $element (@elements) {
59             my $element_model = $c_model->{element}{$element};
60             my $element_type = $element_model->{type};
61 103         166 my $cargo = $element_model->{cargo};
62 529         699 my $c_type = defined $cargo ? $cargo->{type} : '';
63 529         1050 my $cfg_class_name =
64 529         559 defined $cargo
65 529 100       721 ? $cargo->{config_class_name}
66             : $element_model->{config_class_name};
67             my %local_found = %$found_ref;
68              
69 529 100       701 if ( $element_type =~ /(warped_)?node/
70 529         973 or $c_type =~ /(warped_)?node/ ) {
71             my $tmp =
72 529 100 100     1616 $element_type eq 'node' || $c_type eq 'node'
73             ? $self->_sniff_class( $cfg_class_name, \%local_found )
74 76 100 100     287 : $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         187 $h{$element}{next_step}{$element} = '';
  466         865  
81             }
82             }
83 453         1169 $logger->trace("done sniffing config class $class");
84             return \%h;
85             }
86 103         295  
87 103         989 my ( $self, $element_model, $found_ref ) = @_;
88              
89             my %warp_tmp;
90             my $ref = $element_model->{warp}{rules};
91 19     19   33 my @rules = ref $ref eq 'HASH' ? %$ref : @$ref;
92              
93 19         21 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       54 my $sub_class = ref $res ? $res->[0] : $res;
96              
97 19         45 # sniff all classes mentionned in warped node rules
98 38         63 my %local_found = %$found_ref;
99 38 50       63 my $tmp = $self->_sniff_class( $sub_class, \%local_found );
100              
101             # merge all tmp in %warp_tmp
102 38         78 for ( keys %$tmp ) { $warp_tmp{$_}{next_class}{$sub_class} = $tmp->{$_}; }
103 38         82 }
104              
105             return \%warp_tmp;
106 38         113 }
  220         466  
107              
108             my $self = shift;
109 19         41 sort keys %{ $self->{data} };
110             }
111              
112             my $self = shift;
113 1     1 1 2 my %args = @_;
114 1         2  
  1         16  
115             foreach my $p (qw/element/) {
116             $self->{$p} = delete $args{$p}
117             or croak "Searcher->prepare: Missing $p parameter";
118 7     7 1 13 }
119 7         32  
120             $self->reset; # initialize the search engine
121 7         14  
122 7 50       24 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         18 message => "Searcher cannot find element '$searched' "
127             . "from $root_class. Found only "
128 7 50       16 . 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         124 $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 19 }
142              
143 10         16 return shift->{element};
144 10         19 }
145 10         21  
146 10         20 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 33 my $next_step = $self->{search_tree}{next_step};
156              
157 23         38 @result =
158             ref $next_step ? sort keys %$next_step
159 23         27 : defined $next_step ? die "next_step error"
160 23 100       56 : ();
161 1         4 }
162              
163             #my $name = $self->{current}{element_name} ;
164 22         33 #print "From $name, next_step is @result\n";
165             return wantarray ? @result : \@result;
166 22 50       85 }
    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       57 return $result if scalar @$result != 1;
175              
176             $self->choose(@$result);
177             }
178 3     3 1 376  
179 3         4 }
180              
181 3         4 my $self = shift;
182 7         13 my $choice = shift;
183 7         36  
184 7 100       59 #print "choose $choice from node\n";
185             my $obj = $self->{current}{object};
186 4         8 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 1356 }
193 17         19  
194             my $self = shift;
195             my $choice = shift;
196 17         23  
197 17 100       36 #print "choose $choice from id\n";
198 1         5 my $id_obj = $self->{current}{object};
199             my $class = $id_obj->config_class_name;
200              
201 16         36 # 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         2 }
208              
209             my $self = shift;
210 1         2 my $choice = shift;
211 1         5  
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         5 if ( ref($next) and not defined $next->{$choice} ) {
218 1         3 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         20 # the following line may trigger an exception for warped out
224             # elements
225             my $next_node = $node->fetch_element($choice);
226 16         20  
227 16         22 # $next is a scalar for leaf element of a ref for node element
228 16         34 if ( $next->{$choice} ) {
229             my $data = $next->{$choice};
230 16 50 33     88  
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         44 . $next_node->warp_error );
239             }
240             }
241 16 100       46  
242 9         15 $self->{search_tree} = $data;
243             }
244             else {
245 9 100       19 $self->{search_tree} = { next_step => undef };
246 4         12 $next_node = $node->fetch_element($choice);
247 4         13 }
248 4 50       9  
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         18 my $self = shift;
256             return $self->{current}{object};
257             }
258 7         23  
259 7         42 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         27  
263 16         35 my $object = $self->{current}{object};
264 16         28 while (1) {
265 16         30 my $next_step = $self->next_step;
266             if ( scalar @$next_step == 0 ) {
267              
268             # found target
269 2     2 1 1148 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 24  
275 5   33     12 $self->_auto_choose_elt( $next_choice, $id_cb );
276 5   33     11 }
277             }
278 5         7  
279 5         7 my $self = shift;
280 14         29 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       22 my $object = $self->{current}{object};
288             my @choice = $object->fetch_all_indexes();
289              
290 9         1407 my $id =
291             @choice == 1
292             ? $choice[0]
293             : $id_cb->( $object, @choice );
294              
295 9     9   15 $self->{current}{object} = $object->fetch_with_id($id);
296 9         9 }
297 9         11 }
298              
299 9         22 1;
300              
301 9         16 # ABSTRACT: Search an element in a configuration model
302 9 100       43  
303 2         3  
304 2         11 =pod
305              
306 2 100       7 =encoding UTF-8
307              
308             =head1 NAME
309              
310             Config::Model::SearchElement - Search an element in a configuration model
311 2         11  
312             =head1 VERSION
313              
314             version 2.152
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