File Coverage

blib/lib/Config/XrmDatabase.pm
Criterion Covered Total %
statement 228 240 95.0
branch 45 56 80.3
condition 11 16 68.7
subroutine 32 33 96.9
pod 10 11 90.9
total 326 356 91.5


line stmt bran cond sub pod time code
1             package Config::XrmDatabase;
2              
3             # ABSTRACT: Pure Perl X Resource Manager Database
4              
5 9     9   2200028 use v5.26;
  9         36  
6 9     9   62 use warnings;
  9         24  
  9         782  
7              
8             our $VERSION = '0.08';
9              
10 9     9   5161 use Feature::Compat::Try;
  9         3618  
  9         49  
11              
12 9     9   6992 use Config::XrmDatabase::Failure ':all';
  9         37  
  9         1893  
13 9     9   5257 use Config::XrmDatabase::Util ':all';
  9         34  
  9         2292  
14 9     9   4986 use Config::XrmDatabase::Types -all;
  9         556  
  9         107  
15 9     9   17937 use Types::Standard qw( Object Str Optional HashRef );
  9         19  
  9         85  
16 9     9   40179 use Type::Params qw( compile_named );
  9         54160  
  9         111  
17 9     9   8989 use Ref::Util;
  9         6238  
  9         599  
18              
19 9     9   5803 use Moo;
  9         82125  
  9         53  
20              
21 9     9   21187 use namespace::clean;
  9         23  
  9         92  
22              
23 9     9   11610 use MooX::StrictConstructor;
  9         121786  
  9         77  
24              
25 9     9   348207 use experimental qw( signatures postderef declared_refs refaliasing );
  9         24  
  9         120  
26 9     9   2124 use if $] >= 5.034, 'experimental', 'try';
  9         32  
  9         371  
27              
28             has _db => (
29             is => 'rwp',
30             init_arg => undef,
31             default => sub { {} },
32             );
33              
34             has _query_return_value => (
35             is => 'ro',
36             isa => QueryReturnValue,
37             init_arg => 'query_return_value',
38             coerce => 1,
39             default => 'value',
40             );
41              
42             has _query_on_failure => (
43             is => 'ro',
44             isa => OnQueryFailure,
45             init_arg => 'query_on_failure',
46             coerce => 1,
47             default => 'undef',
48             );
49              
50             # fake attribute so we can use MooX::StrictConstructor
51             has _insert => (
52             is => 'ro',
53             isa => HashRef,
54             init_arg => 'insert',
55             predicate => 1,
56             clearer => 1,
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              
94 23     23 0 3816 sub BUILD ( $self, $ ) {
  23         52  
  23         44  
95 23 100       237 if ( $self->_has_insert ) {
96 1         5 my $kv = $self->_insert;
97 1         5 $self->insert( $_, $kv->{$_} ) for keys %$kv;
98 1         19 $self->_clear_insert;
99             }
100             }
101              
102              
103              
104              
105              
106              
107              
108              
109              
110              
111              
112 1591     1591 1 21234 sub insert ( $self, $name, $value ) {
  1591         2788  
  1591         2775  
  1591         2552  
  1591         2663  
113              
114 1591         3757 $name = parse_resource_name( $name );
115 1591         5947 my $db = $self->_db;
116 1591   100     19611 $db = $db->{$_} //= {} for $name->@*;
117 1591         3786 $db->{ +VALUE } = $value;
118 1591         7261 $db->{ +MATCH_COUNT } = 0;
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              
210 9     9   6074 no namespace::clean;
  9         43  
  9         92  
211             use constant {
212 9         1199 QUERY_RETURN_VALUE => 'value',
213             QUERY_RETURN_REFERENCE => 'reference',
214             QUERY_RETURN_ALL => 'all',
215             QUERY_ON_FAILURE_THROW => 'throw',
216             QUERY_ON_FAILURE_UNDEF => 'undef',
217 9     9   4431 };
  9         21  
218 9     9   63 use namespace::clean;
  9         21  
  9         64  
219              
220 19     19 1 178 sub query ( $self, $class, $name, %iopt ) {
  19         33  
  19         38  
  19         31  
  19         70  
  19         58  
221              
222 19         74 state $check = compile_named(
223             { head => [ Str, Str ] },
224             return_value => Optional [QueryReturnValue],
225             on_failure => Optional [OnQueryFailure],
226             );
227              
228 19         271517 ( $class, $name, my \%opt ) = $check->( $class, $name, %iopt );
229              
230 19   66     757 $opt{on_failure} //= $self->_query_on_failure;
231 19   66     127 $opt{return_value} //= $self->_query_return_value;
232              
233 19         45 ( $class, $name ) = map { parse_fq_resource_name( $_ ) } $class, $name;
  38         121  
234              
235 19 50       60 components_failure->throw( 'class and name must have the same number of components' )
236             if @$class != @$name;
237              
238 19         49 my $return_all = $opt{return_value} eq QUERY_RETURN_ALL;
239              
240 19         41 my $match = [];
241 19         56 my @qargs = ( $class, $name, $return_all, $match );
242 19         134 my $retval = $self->_query( $self->_db, 0, \@qargs );
243              
244 19 100       54 if ( !defined $retval ) {
245             return $opt{on_failure}->( $name, $class )
246 8 100       36 if Ref::Util::is_coderef( $opt{on_failure} );
247              
248             query_failure->throw(
249 2         13 "unable to match name: '@{[ name_arr_to_str($name) ]} '; class : '@{[ name_arr_to_str($class) ]}'" )
  2         8  
250 6 100       25 if $opt{on_failure} eq QUERY_ON_FAILURE_THROW;
251              
252 4         85 return undef;
253             }
254              
255 11 100       257 return $opt{return_value} eq QUERY_RETURN_VALUE ? $$retval : $retval;
256             }
257              
258 220     220   352 sub _query ( $self, $db, $idx, $args ) {
  220         380  
  220         362  
  220         318  
  220         375  
  220         317  
259              
260 220         431 my ( \$class, \$name, \$return_all, \$match ) = map { \$_ } $args->@*;
  880         2086  
261              
262 220         469 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       559 if ( $idx + 1 == @$name ) {
268 51         114 for my $component ( $name->[$idx], $class->[$idx] ) {
269 91 50 66     319 if ( exists $db->{$component}
270             && exists $db->{$component}{ +VALUE } )
271             {
272 11         53 push $match->@*, $component;
273 11         28 my $entry = $db->{$component};
274 11         26 ++$entry->{ +MATCH_COUNT };
275 11         24 my $value = $entry->{ +VALUE };
276             return $return_all
277             ? {
278             value => $value,
279 11 100       83 match_count => $entry->{ +MATCH_COUNT },
280             key => $match,
281             }
282             : \$value;
283             }
284             }
285 40         103 return undef;
286             }
287              
288             # otherwise need to possibly check lower level components
289              
290             # exactly named components
291 169         346 for my $component ( $name->[$idx], $class->[$idx] ) {
292 316 100       956 if ( my $subdb = $db->{$component} ) {
293 56         161 push $match->@*, $component;
294 56         184 my $res = $self->$_query( $subdb, $idx + 1, $args );
295 56 100       210 return $res if defined $res;
296 24         61 pop $match->@*;
297             }
298             }
299              
300             # single wildcard
301 137 100       345 if ( my $subdb = $db->{ +SINGLE } ) {
302 25         54 push $match->@*, SINGLE;
303 25         74 my $res = $self->$_query( $subdb, $idx + 1, $args );
304 25 50       67 return $res if defined $res;
305 25         51 pop $match->@*;
306             }
307              
308 137 100       359 if ( my $subdb = $db->{ +LOOSE } ) {
309 36         77 my $max = @$name;
310 36         76 push $match->@*, LOOSE;
311 36         112 while ( $idx < $max ) {
312 120         270 my $res = $self->$_query( $subdb, $idx, $args );
313 120 100       294 return $res if defined $res;
314 108         257 ++$idx;
315             }
316 24         48 pop $match->@*;
317             }
318              
319 125         294 return undef;
320             }
321              
322              
323              
324              
325              
326              
327              
328              
329              
330              
331              
332              
333              
334              
335              
336              
337 1     1 1 48488 sub read_file ( $class, $file, %opts ) {
  1         4  
  1         3  
  1         3  
  1         3  
338              
339 1         56 my $self = $class->new( %opts );
340              
341 1         30 require File::Slurper;
342              
343 1         2 my @lines;
344              
345 1         4 try {
346 1         7 @lines = File::Slurper::read_lines( $file );
347             }
348             catch ( $e ) {
349 0         0 file_failure->throw( "$e: error opening $file: $!" );
350             }
351              
352 1         845 my $idx = 0;
353 1         5 for my $line ( @lines ) {
354 750         1285 ++$idx;
355 750         6670 my ( $var, $value ) = $line =~ /^\s*([^:]+?)\s*:\s*(.*?)\s*$/;
356 750 50 33     2951 file_failure->throw( sprintf( '%s:%d: unable to parse line', $file, $idx ) )
357             unless defined $var and defined $value;
358 750         1792 $self->insert( $var, $value );
359             }
360              
361 1         114 return $self;
362             }
363              
364              
365              
366              
367              
368              
369              
370              
371              
372              
373 1     1 1 869 sub write_file ( $self, $file ) {
  1         3  
  1         3  
  1         2  
374 1         648 require File::Slurper;
375 1         3952 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 3 sub to_string ( $self ) {
  1         3  
  1         2  
393              
394 1         5 my $folded = $self->_folded;
395 1         4 my @records;
396              
397 1         303 for my $key ( keys $folded->%* ) {
398 1500         2838 my $value = $folded->{$key};
399              
400 1500 50       7645 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
401 1500 100       6475 next unless $+{component} eq VALUE;
402 750         2974 $key = $+{key};
403             }
404              
405 750         2086 push @records, "$key : $value";
406             }
407              
408 1 50       916 return @records ? join( "\n", @records, q{} ) : q{};
409             }
410              
411              
412              
413              
414              
415              
416              
417              
418              
419              
420              
421 1     1 1 8 sub merge ( $self, $other ) {
  1         2  
  1         2  
  1         3  
422              
423 1         688 require Hash::Merge;
424 1         7444 my $merger = Hash::Merge->new( 'RIGHT_PRECEDENT' );
425              
426 1         117 $self->_db->%* = $merger->merge( $self->TO_HASH->{db}, $other->TO_HASH->{db} )->%*;
427              
428 1         604 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 9     9   32032 %KV_CONSTANTS = ( map { uc( "KV_$_" ) => $_ } qw( all string array value match_count ) );
  45         622  
512              
513             }
514 9     9   107 use constant \%KV_CONSTANTS;
  9         47  
  9         15622  
515              
516              
517 4     4   7 sub _to_kv_xx ( $self, %iopt ) {
  4         4  
  4         9  
  4         4  
518 4         15 %iopt = ( key => KV_STRING, value => KV_VALUE, %iopt );
519              
520 4         5 state $match = {
521             ## no critic (ComplexRegexes)
522 1         60 value => qr/^(? @{[ join '|', KV_VALUE, KV_MATCH_COUNT, KV_ALL ]} )$/xi,
523 1         24 key => qr/^(? @{[ join '|', KV_STRING, KV_ARRAY ]} )$/xi,
524             };
525              
526             ## no critic (ComplexMappings)
527             my %opt = map {
528 4         8 parameter_failure->throw( "illegal value for '$_' option: $iopt{$_}" )
529 8 50       51 unless $iopt{$_} =~ $match->{$_};
530 8         50 $_ => $+{match};
531             } qw( key value );
532              
533 4         16 parameter_failure->throw( "illegal option: $_" ) for grep !defined $opt{$_}, keys %iopt;
534              
535             # don't clean out excess TIGHT characters if we'll need to later
536             # split it into components. otherwise we'd have to run
537             # parse_resource_name all over again.
538 4         8 my $normalize_keys = $opt{key} eq KV_STRING;
539 4         9 my $folded = $self->_folded( $normalize_keys );
540              
541             # first get values
542             # return single requested value
543 4 100       18 if ( my $component = $RMETA{ $opt{value} } ) {
    50          
544              
545 3         9 for my $key ( keys $folded->%* ) {
546 30 50       135 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
547             # only allow the requested data out
548 30         49 my $value = delete $folded->{$key};
549             $folded->{ $+{key} } = $value
550 30 100       135 if $+{component} eq $component;
551             }
552             }
553             }
554              
555             elsif ( $opt{value} eq KV_ALL ) {
556              
557 1         4 for my $key ( keys $folded->%* ) {
558 10 50       91 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
559             ( $folded->{ $+{key} } //= {} )->{ $META{ $+{component} } }
560 10   100     77 = delete $folded->{$key};
561             }
562             }
563             }
564              
565             # shouldn't get here
566             else {
567 0         0 internal_failure->throw( "internal error: unexpected value for 'value': $iopt{value}" );
568             }
569              
570             return $folded
571 4 100       31 if $opt{key} eq KV_STRING;
572              
573 5         23 return [ map { [ [ split( /[.]/ ) ], $folded->{$_} ] } keys $folded->%* ]
574 1 50       5 if $opt{key} eq KV_ARRAY;
575              
576 0         0 internal_failure->throw( "internal error: unexpected value for 'key': $iopt{key}" );
577             }
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 3     3 1 10822 sub to_kv ( $self, %opt ) {
  3         4  
  3         8  
  3         3  
622 3         10 $self->_to_kv_xx( %opt, key => 'string' );
623             }
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 1     1 1 5615 sub to_kv_arr ( $self, %opt ) {
  1         22  
  1         3  
  1         2  
671 1         3 $self->_to_kv_xx( %opt, key => 'array' );
672             }
673              
674              
675              
676              
677              
678              
679              
680              
681              
682              
683              
684 12     12 1 7146 sub TO_HASH ( $self ) {
  12         23  
  12         42  
685 12         111 require Storable;
686              
687             {
688 12         13673 query_return_value => $self->_query_return_value,
689             query_on_failure => $self->_query_on_failure,
690             db => Storable::dclone( $self->_db ),
691             };
692             }
693              
694              
695              
696              
697              
698              
699              
700              
701              
702 5     5   10 sub _folded ( $self, $normalize_names = 1 ) {
  5         6  
  5         8  
  5         6  
703              
704             # Hash::Fold is overkill
705 5         1135 require Hash::Fold;
706 5         44245 my $folded = Hash::Fold->new( delimiter => q{.} )->fold( $self->TO_HASH->{db} );
707              
708 5 100       116178 return $folded unless $normalize_names;
709              
710 4         422 for my $key ( keys %$folded ) {
711 1530         3137 my $nkey = normalize_key( $key );
712 1530         4517 $folded->{$nkey} = delete $folded->{$key};
713             }
714              
715 4         388 return $folded;
716             }
717              
718              
719              
720              
721              
722              
723             1;
724              
725             #
726             # This file is part of Config-XrmDatabase
727             #
728             # This software is Copyright (c) 2021 by Smithsonian Astrophysical Observatory.
729             #
730             # This is free software, licensed under:
731             #
732             # The GNU General Public License, Version 3, June 2007
733             #
734              
735             __END__