File Coverage

blib/lib/Config/XrmDatabase.pm
Criterion Covered Total %
statement 224 235 95.3
branch 45 56 80.3
condition 11 16 68.7
subroutine 32 33 96.9
pod 10 11 90.9
total 322 351 91.7


line stmt bran cond sub pod time code
1             package Config::XrmDatabase;
2              
3             # ABSTRACT: Pure Perl X Resource Manager Database
4              
5 8     8   1928065 use v5.26;
  8         77  
6 8     8   46 use warnings;
  8         16  
  8         337  
7              
8             our $VERSION = '0.06';
9              
10 8     8   4191 use Feature::Compat::Try;
  8         2716  
  8         36  
11              
12 8     8   26604 use Config::XrmDatabase::Failure ':all';
  8         21  
  8         1365  
13 8     8   3851 use Config::XrmDatabase::Util ':all';
  8         25  
  8         1711  
14 8     8   3796 use Config::XrmDatabase::Types -all;
  8         390  
  8         115  
15 8     8   14691 use Types::Standard qw( Object Str Optional HashRef );
  8         23  
  8         57  
16 8     8   14760 use Type::Params qw( compile_named );
  8         86322  
  8         91  
17 8     8   6745 use Ref::Util;
  8         4688  
  8         372  
18              
19 8     8   5063 use Moo;
  8         60253  
  8         48  
20              
21 8     8   13345 use namespace::clean;
  8         23  
  8         81  
22              
23 8     8   8643 use MooX::StrictConstructor;
  8         117445  
  8         41  
24              
25 8     8   198795 use experimental qw( signatures postderef declared_refs refaliasing );
  8         26  
  8         84  
26              
27             has _db => (
28             is => 'rwp',
29             init_arg => undef,
30             default => sub { {} },
31             );
32              
33             has _query_return_value => (
34             is => 'ro',
35             isa => QueryReturnValue,
36             init_arg => 'query_return_value',
37             coerce => 1,
38             default => 'value',
39             );
40              
41             has _query_on_failure => (
42             is => 'ro',
43             isa => OnQueryFailure,
44             init_arg => 'query_on_failure',
45             coerce => 1,
46             default => 'undef',
47             );
48              
49             # fake attribute so we can use MooX::StrictConstructor
50             has _insert => (
51             is => 'ro',
52             isa => HashRef,
53             init_arg => 'insert',
54             predicate => 1,
55             clearer => 1,
56             );
57              
58              
59              
60              
61              
62              
63              
64              
65              
66              
67              
68              
69              
70              
71              
72              
73              
74              
75              
76              
77              
78              
79              
80              
81              
82              
83              
84              
85              
86              
87              
88              
89              
90              
91              
92              
93 23     23 0 3196 sub BUILD ( $self, $ ) {
  23         52  
  23         37  
94 23 100       207 if ( $self->_has_insert ) {
95 1         5 my $kv = $self->_insert;
96 1         12 $self->insert( $_, $kv->{$_} ) for keys %$kv;
97 1         25 $self->_clear_insert;
98             }
99             }
100              
101              
102              
103              
104              
105              
106              
107              
108              
109              
110              
111 1591     1591 1 30676 sub insert ( $self, $name, $value ) {
  1591         1994  
  1591         1896  
  1591         1928  
  1591         1919  
112              
113 1591         3131 $name = parse_resource_name( $name );
114 1591         4307 my $db = $self->_db;
115 1591   100     15401 $db = $db->{$_} //= {} for $name->@*;
116 1591         3282 $db->{ +VALUE } = $value;
117 1591         17976 $db->{ +MATCH_COUNT } = 0;
118             }
119              
120              
121              
122              
123              
124              
125              
126              
127              
128              
129              
130              
131              
132              
133              
134              
135              
136              
137              
138              
139              
140              
141              
142              
143              
144              
145              
146              
147              
148              
149              
150              
151              
152              
153              
154              
155              
156              
157              
158              
159              
160              
161              
162              
163              
164              
165              
166              
167              
168              
169              
170              
171              
172              
173              
174              
175              
176              
177              
178              
179              
180              
181              
182              
183              
184              
185              
186              
187              
188              
189              
190              
191              
192              
193              
194              
195              
196              
197              
198              
199              
200              
201              
202              
203              
204              
205              
206              
207              
208              
209 8     8   5235 no namespace::clean;
  8         22  
  8         70  
210             use constant {
211 8         840 QUERY_RETURN_VALUE => 'value',
212             QUERY_RETURN_REFERENCE => 'reference',
213             QUERY_RETURN_ALL => 'all',
214             QUERY_ON_FAILURE_THROW => 'throw',
215             QUERY_ON_FAILURE_UNDEF => 'undef',
216 8     8   3620 };
  8         25  
217 8     8   69 use namespace::clean;
  8         18  
  8         69  
218              
219 19     19 1 138 sub query ( $self, $class, $name, %iopt ) {
  19         35  
  19         26  
  19         37  
  19         32  
  19         31  
220              
221 19         97 state $check = compile_named(
222             { head => [ Str, Str ] },
223             return_value => Optional[QueryReturnValue],
224             on_failure => Optional[OnQueryFailure],
225             );
226              
227 19         11878 ( $class, $name, my \%opt ) = $check->( $class, $name, %iopt );
228              
229 19   66     642 $opt{on_failure} //= $self->_query_on_failure;
230 19   66     91 $opt{return_value} //= $self->_query_return_value;
231              
232 19         42 ( $class, $name ) = map { parse_fq_resource_name( $_ ) } $class, $name;
  38         111  
233              
234 19 50       58 components_failure->throw(
235             "class and name must have the same number of components" )
236             if @$class != @$name;
237              
238 19         43 my $return_all = $opt{return_value} eq QUERY_RETURN_ALL;
239              
240 19         35 my $match = [];
241 19         50 my @qargs = ( $class, $name, $return_all, $match );
242 19         65 my $retval = $self->_query( $self->_db, 0, \@qargs );
243              
244 19 100       55 if ( ! defined $retval ) {
245             return $opt{on_failure}->( $name, $class )
246 8 100       27 if Ref::Util::is_coderef( $opt{on_failure} );
247              
248             query_failure->throw(
249             "unable to match name: '$name'; class : '$class'" )
250 6 100       21 if $opt{on_failure} eq QUERY_ON_FAILURE_THROW;
251              
252 4         51 return undef;
253             }
254              
255 11 100       176 return $opt{return_value} eq QUERY_RETURN_VALUE ? $$retval : $retval;
256             }
257              
258 220     220   305 sub _query ( $self, $db, $idx, $args ) {
  220         339  
  220         303  
  220         289  
  220         302  
  220         283  
259              
260 220         360 my ( \$class, \$name, \$return_all, \$match ) = map { \$_ } $args->@*;
  880         1776  
261              
262 220         400 my $_query = __SUB__;
263              
264             # things are simple if we're looking for the last component; it must
265             # match exactly. this might be able to be inlined in the exact match
266             # checks below to avoid a recursive call, but this is clearer.
267 220 100       505 if ( $idx + 1 == @$name ) {
268 51         96 for my $component ( $name->[$idx], $class->[$idx] ) {
269 91 50 66     236 if ( exists $db->{$component}
270             && exists $db->{$component}{ +VALUE } )
271             {
272 11         27 push $match->@*, $component;
273 11         24 my $entry = $db->{$component};
274 11         18 ++$entry->{ +MATCH_COUNT };
275 11         20 my $value = $entry->{ +VALUE };
276             return $return_all
277             ? {
278             value => $value,
279 11 100       58 match_count => $entry->{ +MATCH_COUNT },
280             key => $match,
281             }
282             : \$value;
283             }
284             }
285 40         82 return undef;
286             }
287              
288             # otherwise need to possibly check lower level components
289              
290             # exactly named components
291 169         308 for my $component ( $name->[$idx], $class->[$idx] ) {
292 316 100       699 if ( my $subdb = $db->{$component} ) {
293 56         115 push $match->@*, $component;
294 56         161 my $res = $self->$_query( $subdb, $idx + 1, $args );
295 56 100       134 return $res if defined $res;
296 24         48 pop $match->@*;
297             }
298             }
299              
300             # single wildcard
301 137 100       305 if ( my $subdb = $db->{ +SINGLE } ) {
302 25         49 push $match->@*, SINGLE;
303 25         52 my $res = $self->$_query( $subdb, $idx + 1, $args );
304 25 50       56 return $res if defined $res;
305 25         45 pop $match->@*;
306             }
307              
308 137 100       265 if ( my $subdb = $db->{ +LOOSE } ) {
309 36         60 my $max = @$name;
310 36         79 push $match->@*, LOOSE;
311 36         81 for ( my $idx = $idx ; $idx < $max ; ++$idx ) {
312 120         230 my $res = $self->$_query( $subdb, $idx, $args );
313 120 100       306 return $res if defined $res;
314             }
315 24         36 pop $match->@*;
316             }
317              
318 125         230 return undef;
319             }
320              
321              
322              
323              
324              
325              
326              
327              
328              
329              
330              
331              
332              
333              
334              
335              
336 1     1 1 516 sub read_file ( $class, $file, %opts ) {
  1         3  
  1         2  
  1         4  
  1         2  
337              
338 1         48 my $self = $class->new( %opts );
339              
340 1         10 require File::Slurper;
341              
342 1         3 my @lines;
343              
344             try {
345             @lines = File::Slurper::read_lines( $file );
346             }
347 1         6 catch ( $e ) {
348             file_failure->throw( "error opening $file: $!" );
349             }
350              
351 1         449 my $idx = 0;
352 1         4 for my $line ( @lines ) {
353 750         844 ++$idx;
354 750         4200 my ( $var, $value ) = $line =~ /^\s*([^:]+?)\s*:\s*(.*?)\s*$/;
355 750 50 33     2058 file_failure->throw(
356             sprintf( "%s:%d: unable to parse line", $file, $idx ) )
357             unless defined $var and defined $value;
358 750         1270 $self->insert( $var, $value );
359             }
360              
361 1         78 return $self;
362             }
363              
364              
365              
366              
367              
368              
369              
370              
371              
372              
373 1     1 1 568 sub write_file ( $self, $file ) {
  1         3  
  1         2  
  1         3  
374 1         591 require File::Slurper;
375 1         2895 File::Slurper::write_text( $file, $self->to_string );
376             }
377              
378              
379              
380              
381              
382              
383              
384              
385              
386              
387              
388              
389              
390              
391              
392 1     1 1 5 sub to_string ( $self ) {
  1         3  
  1         2  
393              
394 1         6 my $folded = $self->_folded;
395 1         4 my @records;
396              
397 1         202 for my $key ( keys $folded->%* ) {
398 1500         2458 my $value = $folded->{$key};
399              
400 1500 50       5600 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
401 8 100   8   23525 next unless $+{component} eq VALUE;
  8         3157  
  8         3286  
  1500         5525  
402 750         2226 $key = $+{key};
403             }
404              
405 750         1996 push @records, "$key : $value";
406             }
407              
408 1 50       715 return @records ? join( "\n", @records, '' ) : '';
409             }
410              
411              
412              
413              
414              
415              
416              
417              
418              
419              
420              
421 1     1 1 6 sub merge ( $self, $other ) {
  1         2  
  1         2  
  1         1  
422              
423 1         592 require Hash::Merge;
424 1         8890 my $merger = Hash::Merge->new( 'RIGHT_PRECEDENT' );
425              
426 1         75 $self->_db->%* = $merger->merge( $self->TO_HASH->{db}, $other->TO_HASH->{db} )->%*;
427              
428 1         541 return $self;
429             }
430              
431              
432              
433              
434              
435              
436              
437              
438              
439              
440 0     0 1 0 sub clone ( $self ) {
  0         0  
  0         0  
441 0         0 require Scalar::Util;
442              
443 0         0 my \%args = $self->TO_HASH;
444 0         0 my $db = delete $args{db}; # this isn't a constructor argument.
445 0         0 my $clone = Scalar::Util::blessed( $self )->new( \%args );
446 0         0 $clone->_set__db( $db );
447 0         0 return $clone;
448             }
449              
450              
451              
452              
453              
454              
455              
456              
457              
458              
459              
460              
461              
462              
463              
464              
465              
466              
467              
468              
469              
470              
471              
472              
473              
474              
475              
476              
477              
478              
479              
480              
481              
482              
483              
484              
485              
486              
487              
488              
489              
490              
491              
492              
493              
494              
495              
496              
497              
498              
499              
500              
501              
502              
503              
504              
505              
506              
507              
508              
509             my %KV_CONSTANTS;
510             BEGIN {
511 8     8   39 %KV_CONSTANTS = ( map { uc( "KV_$_" ) => $_ }
  40         398  
512             qw( all string array value match_count ) );
513              
514             }
515 8     8   67 use constant \%KV_CONSTANTS;
  8         20  
  8         9655  
516              
517              
518 4     4   5 sub _to_kv_xx ( $self, %iopt ) {
  4         7  
  4         15  
  4         5  
519 4         20 %iopt = ( key => KV_STRING, value => KV_VALUE, %iopt );
520              
521 4         7 state $match = {
522             value =>
523 1         62 qr/^(? @{[ join '|', KV_VALUE, KV_MATCH_COUNT, KV_ALL ]} )$/xi,
524 1         32 key => qr/^(? @{[ join '|', KV_STRING, KV_ARRAY ]} )$/xi,
525             };
526              
527             my %opt = map {
528 4         28 parameter_failure->throw( "illegal value for '$_' option: $iopt{$_}" )
529 8 50       76 unless $iopt{$_} =~ $match->{$_};
530 8         76 $_ => $+{match};
531             } qw( key value );
532              
533             parameter_failure->throw( "illegal option: $_" )
534 4         24 for grep !defined $opt{$_}, keys %iopt;
535              
536             # don't clean out excess TIGHT characters if we'll need to later
537             # split it into components. otherwise we'd have to run
538             # parse_resource_name all over again.
539 4         10 my $normalize_keys = $opt{key} eq KV_STRING;
540 4         11 my $folded = $self->_folded( $normalize_keys );
541              
542             # first get values
543             # return single requested value
544 4 100       20 if ( my $component = $RMETA{ $opt{value} } ) {
    50          
545              
546 3         14 for my $key ( keys $folded->%* ) {
547 30 50       208 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
548             # only allow the requested data out
549 30         63 my $value = delete $folded->{$key};
550             $folded->{ $+{key} } = $value
551 30 100       197 if $+{component} eq $component;
552             }
553             }
554             }
555              
556             elsif ( $opt{value} eq KV_ALL ) {
557              
558 1         5 for my $key ( keys $folded->%* ) {
559 10 50       105 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
560             ( $folded->{ $+{key} } //= {} )->{ $META{ $+{component} } }
561 10   100     101 = delete $folded->{$key};
562             }
563             }
564             }
565              
566             # shouldn't get here
567             else {
568 0         0 internal_failure->throw( "internal error: unexpected value for 'value': $iopt{value}" );
569             }
570              
571             return $folded
572 4 100       28 if $opt{key} eq KV_STRING;
573              
574 5         29 return [ map { [ [ split( /[.]/, $_ ) ], $folded->{$_} ] } keys $folded->%* ]
575 1 50       9 if $opt{key} eq KV_ARRAY;
576              
577 0         0 internal_failure->throw( "internal error: unexpected value for 'key': $iopt{key}" );
578             }
579              
580              
581              
582              
583              
584              
585              
586              
587              
588              
589              
590              
591              
592              
593              
594              
595              
596              
597              
598              
599              
600              
601              
602              
603              
604              
605              
606              
607              
608              
609              
610              
611              
612              
613              
614              
615              
616              
617              
618              
619              
620              
621              
622 3     3 1 13632 sub to_kv ( $self, %opt ) {
  3         6  
  3         9  
  3         6  
623 3         12 $self->_to_kv_xx( %opt, key => 'string' );
624             }
625              
626              
627              
628              
629              
630              
631              
632              
633              
634              
635              
636              
637              
638              
639              
640              
641              
642              
643              
644              
645              
646              
647              
648              
649              
650              
651              
652              
653              
654              
655              
656              
657              
658              
659              
660              
661              
662              
663              
664              
665              
666              
667              
668              
669              
670              
671 1     1 1 8673 sub to_kv_arr ( $self, %opt ) {
  1         3  
  1         3  
  1         2  
672 1         5 $self->_to_kv_xx( %opt, key => 'array' );
673             }
674              
675              
676              
677              
678              
679              
680              
681              
682              
683              
684              
685 12     12 1 7149 sub TO_HASH ( $self ) {
  12         35  
  12         20  
686 12         3242 require Storable;
687              
688             {
689 12         23767 query_return_value => $self->_query_return_value,
690             query_on_failure => $self->_query_on_failure,
691             db => Storable::dclone( $self->_db ),
692             }
693             }
694              
695              
696              
697              
698              
699              
700              
701              
702              
703 5     5   9 sub _folded ( $self, $normalize_names = 1 ) {
  5         9  
  5         11  
  5         7  
704              
705             # Hash::Fold is overkill
706 5         1150 require Hash::Fold;
707 5         39175 my $folded = Hash::Fold->new( delimiter => '.' )->fold( $self->TO_HASH->{db} );
708              
709 5 100       80116 return $folded unless $normalize_names;
710              
711 4         279 for my $key ( keys %$folded ) {
712 1530         2443 my $nkey = normalize_key( $key );
713 1530         5657 $folded->{$nkey} = delete $folded->{$key};
714             }
715              
716 4         413 return $folded;
717             }
718              
719              
720              
721              
722              
723              
724             1;
725              
726             #
727             # This file is part of Config-XrmDatabase
728             #
729             # This software is Copyright (c) 2021 by Smithsonian Astrophysical Observatory.
730             #
731             # This is free software, licensed under:
732             #
733             # The GNU General Public License, Version 3, June 2007
734             #
735              
736             __END__