File Coverage

blib/lib/Config/XrmDatabase.pm
Criterion Covered Total %
statement 219 230 95.2
branch 44 54 81.4
condition 11 16 68.7
subroutine 31 32 96.8
pod 9 10 90.0
total 314 342 91.8


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   1934894 use v5.26;
  8         80  
6 8     8   44 use warnings;
  8         16  
  8         328  
7              
8             our $VERSION = '0.04';
9              
10 8     8   4114 use Feature::Compat::Try;
  8         2758  
  8         47  
11              
12 8     8   26644 use Config::XrmDatabase::Failure ':all';
  8         26  
  8         1385  
13 8     8   3692 use Config::XrmDatabase::Util ':all';
  8         20  
  8         1633  
14 8     8   3777 use Config::XrmDatabase::Types -all;
  8         372  
  8         109  
15 8     8   14326 use Types::Standard qw( Object Str Optional HashRef );
  8         27  
  8         52  
16 8     8   14870 use Type::Params qw( compile_named );
  8         87124  
  8         91  
17 8     8   6921 use Ref::Util;
  8         4680  
  8         373  
18              
19 8     8   5005 use Moo;
  8         60763  
  8         54  
20              
21 8     8   13479 use namespace::clean;
  8         26  
  8         65  
22              
23 8     8   8506 use MooX::StrictConstructor;
  8         118774  
  8         50  
24              
25 8     8   200739 use experimental qw( signatures postderef declared_refs refaliasing );
  8         22  
  8         93  
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 3267 sub BUILD ( $self, $ ) {
  23         54  
  23         38  
94 23 100       187 if ( $self->_has_insert ) {
95 1         4 my $kv = $self->_insert;
96 1         12 $self->insert( $_, $kv->{$_} ) for keys %$kv;
97 1         26 $self->_clear_insert;
98             }
99             }
100              
101              
102              
103              
104              
105              
106              
107              
108              
109              
110              
111 1591     1591 1 10456 sub insert ( $self, $name, $value ) {
  1591         2145  
  1591         2149  
  1591         2092  
  1591         2079  
112              
113 1591         3367 $name = parse_resource_name( $name );
114 1591         4395 my $db = $self->_db;
115 1591   100     15976 $db = $db->{$_} //= {} for $name->@*;
116 1591         3466 $db->{ +VALUE } = $value;
117 1591         4161 $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   5198 no namespace::clean;
  8         23  
  8         74  
210             use constant {
211 8         888 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   3373 };
  8         19  
217 8     8   61 use namespace::clean;
  8         23  
  8         103  
218              
219 19     19 1 135 sub query ( $self, $class, $name, %iopt ) {
  19         30  
  19         31  
  19         27  
  19         32  
  19         27  
220              
221 19         37 state $check = compile_named(
222             { head => [ Str, Str ] },
223             return_value => Optional[QueryReturnValue],
224             on_failure => Optional[OnQueryFailure],
225             );
226              
227 19         11809 ( $class, $name, my \%opt ) = $check->( $class, $name, %iopt );
228              
229 19   66     634 $opt{on_failure} //= $self->_query_on_failure;
230 19   66     95 $opt{return_value} //= $self->_query_return_value;
231              
232 19         44 ( $class, $name ) = map { parse_fq_resource_name( $_ ) } $class, $name;
  38         101  
233              
234 19 50       57 components_failure->throw(
235             "class and name must have the same number of components" )
236             if @$class != @$name;
237              
238 19         46 my $return_all = $opt{return_value} eq QUERY_RETURN_ALL;
239              
240 19         49 my $match = [];
241 19         46 my @qargs = ( $class, $name, $return_all, $match );
242 19         74 my $retval = $self->_query( $self->_db, 0, \@qargs );
243              
244 19 100       46 if ( ! defined $retval ) {
245             return $opt{on_failure}->( $name, $class )
246 8 100       25 if Ref::Util::is_coderef( $opt{on_failure} );
247              
248             query_failure->throw(
249             "unable to match name: '$name'; class : '$class'" )
250 6 100       23 if $opt{on_failure} eq QUERY_ON_FAILURE_THROW;
251              
252 4         50 return undef;
253             }
254              
255 11 100       169 return $opt{return_value} eq QUERY_RETURN_VALUE ? $$retval : $retval;
256             }
257              
258 220     220   272 sub _query ( $self, $db, $idx, $args ) {
  220         293  
  220         270  
  220         278  
  220         258  
  220         251  
259              
260 220         337 my ( \$class, \$name, \$return_all, \$match ) = map { \$_ } $args->@*;
  880         1590  
261              
262 220         370 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       414 if ( $idx + 1 == @$name ) {
268 51         91 for my $component ( $name->[$idx], $class->[$idx] ) {
269 91 50 66     214 if ( exists $db->{$component}
270             && exists $db->{$component}{ +VALUE } )
271             {
272 11         26 push $match->@*, $component;
273 11         20 my $entry = $db->{$component};
274 11         19 ++$entry->{ +MATCH_COUNT };
275 11         36 my $value = $entry->{ +VALUE };
276             return $return_all
277             ? {
278             value => $value,
279 11 100       55 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         273 for my $component ( $name->[$idx], $class->[$idx] ) {
292 316 100       642 if ( my $subdb = $db->{$component} ) {
293 56         96 push $match->@*, $component;
294 56         177 my $res = $self->$_query( $subdb, $idx + 1, $args );
295 56 100       136 return $res if defined $res;
296 24         42 pop $match->@*;
297             }
298             }
299              
300             # single wildcard
301 137 100       248 if ( my $subdb = $db->{ +SINGLE } ) {
302 25         42 push $match->@*, SINGLE;
303 25         56 my $res = $self->$_query( $subdb, $idx + 1, $args );
304 25 50       51 return $res if defined $res;
305 25         37 pop $match->@*;
306             }
307              
308 137 100       268 if ( my $subdb = $db->{ +LOOSE } ) {
309 36         61 my $max = @$name;
310 36         61 push $match->@*, LOOSE;
311 36         85 for ( my $idx = $idx ; $idx < $max ; ++$idx ) {
312 120         213 my $res = $self->$_query( $subdb, $idx, $args );
313 120 100       282 return $res if defined $res;
314             }
315 24         32 pop $match->@*;
316             }
317              
318 125         259 return undef;
319             }
320              
321              
322              
323              
324              
325              
326              
327              
328              
329              
330              
331              
332              
333              
334              
335              
336 2     2 1 728 sub read_file ( $class, $file, %opts ) {
  2         7  
  2         4  
  2         5  
  2         4  
337              
338 2         49 my $self = $class->new( %opts );
339              
340 2         18 require File::Slurper;
341              
342 2         4 my @lines;
343              
344             try {
345             @lines = File::Slurper::read_lines( $file );
346             }
347 2         12 catch ( $e ) {
348             file_failure->throw( "error opening $file: $!" );
349             }
350              
351 2         1334 my $idx = 0;
352 2         9 for my $line ( @lines ) {
353 1500         2256 ++$idx;
354 1500         9635 my ( $var, $value ) = $line =~ /^\s*([^:]+?)\s*:\s*(.*?)\s*$/;
355 1500 50 33     4994 file_failure->throw(
356             sprintf( "%s:%d: unable to parse line", $file, $idx ) )
357             unless defined $var and defined $value;
358 1500         3072 $self->insert( $var, $value );
359             }
360              
361 2         80 return $self;
362             }
363              
364              
365              
366              
367              
368              
369              
370              
371              
372              
373              
374              
375              
376              
377              
378 1     1 1 674 sub write_file ( $self, $file ) {
  1         3  
  1         2  
  1         3  
379 1         4 my $folded = $self->_folded;
380 1         4 my @records;
381              
382 1         301 for my $key ( keys $folded->%* ) {
383 1500         2983 my $value = $folded->{$key};
384              
385 1500 50       6832 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
386 8 100   8   22992 next unless $+{component} eq VALUE;
  8         3426  
  8         3507  
  1500         6602  
387 750         2742 $key = $+{key};
388             }
389              
390 750         2519 push @records, "$key : $value";
391             }
392              
393 1         263 File::Slurper::write_text( $file, join( "\n", @records ) );
394             }
395              
396              
397              
398              
399              
400              
401              
402              
403              
404              
405              
406 1     1 1 7 sub merge ( $self, $other ) {
  1         6  
  1         2  
  1         2  
407              
408 1         633 require Hash::Merge;
409 1         9477 my $merger = Hash::Merge->new( 'RIGHT_PRECEDENT' );
410              
411 1         75 $self->_db->%* = $merger->merge( $self->TO_HASH->{db}, $other->TO_HASH->{db} )->%*;
412              
413 1         553 return $self;
414             }
415              
416              
417              
418              
419              
420              
421              
422              
423              
424              
425 0     0 1 0 sub clone ( $self ) {
  0         0  
  0         0  
426 0         0 require Scalar::Util;
427              
428 0         0 my \%args = $self->TO_HASH;
429 0         0 my $db = delete $args{db}; # this isn't a constructor argument.
430 0         0 my $clone = Scalar::Util::blessed( $self )->new( \%args );
431 0         0 $clone->_set__db( $db );
432 0         0 return $clone;
433             }
434              
435              
436              
437              
438              
439              
440              
441              
442              
443              
444              
445              
446              
447              
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             my %KV_CONSTANTS;
495             BEGIN {
496 8     8   56 %KV_CONSTANTS = ( map { uc( "KV_$_" ) => $_ }
  40         466  
497             qw( all string array value match_count ) );
498              
499             }
500 8     8   65 use constant \%KV_CONSTANTS;
  8         28  
  8         9684  
501              
502              
503 4     4   6 sub _to_kv_xx ( $self, %iopt ) {
  4         7  
  4         9  
  4         6  
504 4         18 %iopt = ( key => KV_STRING, value => KV_VALUE, %iopt );
505              
506 4         7 state $match = {
507             value =>
508 1         66 qr/^(? @{[ join '|', KV_VALUE, KV_MATCH_COUNT, KV_ALL ]} )$/xi,
509 1         41 key => qr/^(? @{[ join '|', KV_STRING, KV_ARRAY ]} )$/xi,
510             };
511              
512             my %opt = map {
513 4         13 parameter_failure->throw( "illegal value for '$_' option: $iopt{$_}" )
514 8 50       76 unless $iopt{$_} =~ $match->{$_};
515 8         72 $_ => $+{match};
516             } qw( key value );
517              
518             parameter_failure->throw( "illegal option: $_" )
519 4         21 for grep !defined $opt{$_}, keys %iopt;
520              
521             # don't clean out excess TIGHT characters if we'll need to later
522             # split it into components. otherwise we'd have to run
523             # parse_resource_name all over again.
524 4         10 my $normalize_keys = $opt{key} eq KV_STRING;
525 4         14 my $folded = $self->_folded( $normalize_keys );
526              
527             # first get values
528             # return single requested value
529 4 100       19 if ( my $component = $RMETA{ $opt{value} } ) {
    50          
530              
531 3         13 for my $key ( keys $folded->%* ) {
532 30 50       200 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
533             # only allow the requested data out
534 30         58 my $value = delete $folded->{$key};
535             $folded->{ $+{key} } = $value
536 30 100       187 if $+{component} eq $component;
537             }
538             }
539             }
540              
541             elsif ( $opt{value} eq KV_ALL ) {
542              
543 1         5 for my $key ( keys $folded->%* ) {
544 10 50       100 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
545             ( $folded->{ $+{key} } //= {} )->{ $META{ $+{component} } }
546 10   100     98 = delete $folded->{$key};
547             }
548             }
549             }
550              
551             # shouldn't get here
552             else {
553 0         0 internal_failure->throw( "internal error: unexpected value for 'value': $iopt{value}" );
554             }
555              
556             return $folded
557 4 100       25 if $opt{key} eq KV_STRING;
558              
559 5         27 return [ map { [ [ split( /[.]/, $_ ) ], $folded->{$_} ] } keys $folded->%* ]
560 1 50       7 if $opt{key} eq KV_ARRAY;
561              
562 0         0 internal_failure->throw( "internal error: unexpected value for 'key': $iopt{key}" );
563             }
564              
565              
566              
567              
568              
569              
570              
571              
572              
573              
574              
575              
576              
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 3     3 1 12788 sub to_kv ( $self, %opt ) {
  3         5  
  3         8  
  3         5  
608 3         10 $self->_to_kv_xx( %opt, key => 'string' );
609             }
610              
611              
612              
613              
614              
615              
616              
617              
618              
619              
620              
621              
622              
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 1     1 1 7429 sub to_kv_arr ( $self, %opt ) {
  1         3  
  1         2  
  1         2  
657 1         5 $self->_to_kv_xx( %opt, key => 'array' );
658             }
659              
660              
661              
662              
663              
664              
665              
666              
667              
668              
669              
670 12     12 1 7273 sub TO_HASH ( $self ) {
  12         25  
  12         17  
671 12         2952 require Storable;
672              
673             {
674 12         24289 query_return_value => $self->_query_return_value,
675             query_on_failure => $self->_query_on_failure,
676             db => Storable::dclone( $self->_db ),
677             }
678             }
679              
680              
681              
682              
683              
684              
685              
686              
687              
688 5     5   9 sub _folded ( $self, $normalize_names = 1 ) {
  5         7  
  5         10  
  5         9  
689              
690             # Hash::Fold is overkill
691 5         1088 require Hash::Fold;
692 5         40823 my $folded = Hash::Fold->new( delimiter => '.' )->fold( $self->TO_HASH->{db} );
693              
694 5 100       99614 return $folded unless $normalize_names;
695              
696 4         462 for my $key ( keys %$folded ) {
697 1530         2925 my $nkey = normalize_key( $key );
698 1530         4315 $folded->{$nkey} = delete $folded->{$key};
699             }
700              
701 4         97 return $folded;
702             }
703              
704              
705              
706              
707              
708              
709             1;
710              
711             #
712             # This file is part of Config-XrmDatabase
713             #
714             # This software is Copyright (c) 2021 by Smithsonian Astrophysical Observatory.
715             #
716             # This is free software, licensed under:
717             #
718             # The GNU General Public License, Version 3, June 2007
719             #
720              
721             __END__