File Coverage

blib/lib/Config/XrmDatabase.pm
Criterion Covered Total %
statement 224 235 95.3
branch 44 54 81.4
condition 11 16 68.7
subroutine 32 33 96.9
pod 10 11 90.9
total 321 349 91.9


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   1881295 use v5.26;
  8         71  
6 8     8   48 use warnings;
  8         14  
  8         328  
7              
8             our $VERSION = '0.05';
9              
10 8     8   3977 use Feature::Compat::Try;
  8         2838  
  8         36  
11              
12 8     8   26147 use Config::XrmDatabase::Failure ':all';
  8         73  
  8         1404  
13 8     8   3510 use Config::XrmDatabase::Util ':all';
  8         21  
  8         1607  
14 8     8   3542 use Config::XrmDatabase::Types -all;
  8         333  
  8         108  
15 8     8   14299 use Types::Standard qw( Object Str Optional HashRef );
  8         18  
  8         58  
16 8     8   14305 use Type::Params qw( compile_named );
  8         85414  
  8         88  
17 8     8   6601 use Ref::Util;
  8         4564  
  8         372  
18              
19 8     8   4744 use Moo;
  8         59955  
  8         45  
20              
21 8     8   13284 use namespace::clean;
  8         22  
  8         66  
22              
23 8     8   8320 use MooX::StrictConstructor;
  8         115818  
  8         45  
24              
25 8     8   195654 use experimental qw( signatures postderef declared_refs refaliasing );
  8         21  
  8         80  
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 3017 sub BUILD ( $self, $ ) {
  23         47  
  23         36  
94 23 100       180 if ( $self->_has_insert ) {
95 1         5 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 30581 sub insert ( $self, $name, $value ) {
  1591         2426  
  1591         2117  
  1591         2078  
  1591         1953  
112              
113 1591         3395 $name = parse_resource_name( $name );
114 1591         4758 my $db = $self->_db;
115 1591   100     15889 $db = $db->{$_} //= {} for $name->@*;
116 1591         3368 $db->{ +VALUE } = $value;
117 1591         17590 $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   5088 no namespace::clean;
  8         30  
  8         82  
210             use constant {
211 8         839 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   3387 };
  8         20  
217 8     8   62 use namespace::clean;
  8         28  
  8         49  
218              
219 19     19 1 140 sub query ( $self, $class, $name, %iopt ) {
  19         32  
  19         24  
  19         33  
  19         30  
  19         32  
220              
221 19         42 state $check = compile_named(
222             { head => [ Str, Str ] },
223             return_value => Optional[QueryReturnValue],
224             on_failure => Optional[OnQueryFailure],
225             );
226              
227 19         11888 ( $class, $name, my \%opt ) = $check->( $class, $name, %iopt );
228              
229 19   66     641 $opt{on_failure} //= $self->_query_on_failure;
230 19   66     128 $opt{return_value} //= $self->_query_return_value;
231              
232 19         43 ( $class, $name ) = map { parse_fq_resource_name( $_ ) } $class, $name;
  38         96  
233              
234 19 50       56 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         33 my $match = [];
241 19         51 my @qargs = ( $class, $name, $return_all, $match );
242 19         59 my $retval = $self->_query( $self->_db, 0, \@qargs );
243              
244 19 100       45 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       22 if $opt{on_failure} eq QUERY_ON_FAILURE_THROW;
251              
252 4         48 return undef;
253             }
254              
255 11 100       168 return $opt{return_value} eq QUERY_RETURN_VALUE ? $$retval : $retval;
256             }
257              
258 220     220   277 sub _query ( $self, $db, $idx, $args ) {
  220         278  
  220         264  
  220         262  
  220         273  
  220         258  
259              
260 220         322 my ( \$class, \$name, \$return_all, \$match ) = map { \$_ } $args->@*;
  880         1703  
261              
262 220         366 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         85 for my $component ( $name->[$idx], $class->[$idx] ) {
269 91 50 66     230 if ( exists $db->{$component}
270             && exists $db->{$component}{ +VALUE } )
271             {
272 11         27 push $match->@*, $component;
273 11         20 my $entry = $db->{$component};
274 11         17 ++$entry->{ +MATCH_COUNT };
275 11         22 my $value = $entry->{ +VALUE };
276             return $return_all
277             ? {
278             value => $value,
279 11 100       60 match_count => $entry->{ +MATCH_COUNT },
280             key => $match,
281             }
282             : \$value;
283             }
284             }
285 40         78 return undef;
286             }
287              
288             # otherwise need to possibly check lower level components
289              
290             # exactly named components
291 169         281 for my $component ( $name->[$idx], $class->[$idx] ) {
292 316 100       648 if ( my $subdb = $db->{$component} ) {
293 56         102 push $match->@*, $component;
294 56         170 my $res = $self->$_query( $subdb, $idx + 1, $args );
295 56 100       131 return $res if defined $res;
296 24         44 pop $match->@*;
297             }
298             }
299              
300             # single wildcard
301 137 100       249 if ( my $subdb = $db->{ +SINGLE } ) {
302 25         39 push $match->@*, SINGLE;
303 25         59 my $res = $self->$_query( $subdb, $idx + 1, $args );
304 25 50       53 return $res if defined $res;
305 25         73 pop $match->@*;
306             }
307              
308 137 100       241 if ( my $subdb = $db->{ +LOOSE } ) {
309 36         51 my $max = @$name;
310 36         62 push $match->@*, LOOSE;
311 36         86 for ( my $idx = $idx ; $idx < $max ; ++$idx ) {
312 120         270 my $res = $self->$_query( $subdb, $idx, $args );
313 120 100       328 return $res if defined $res;
314             }
315 24         38 pop $match->@*;
316             }
317              
318 125         215 return undef;
319             }
320              
321              
322              
323              
324              
325              
326              
327              
328              
329              
330              
331              
332              
333              
334              
335              
336 1     1 1 476 sub read_file ( $class, $file, %opts ) {
  1         3  
  1         2  
  1         3  
  1         3  
337              
338 1         43 my $self = $class->new( %opts );
339              
340 1         9 require File::Slurper;
341              
342 1         2 my @lines;
343              
344             try {
345             @lines = File::Slurper::read_lines( $file );
346             }
347 1         8 catch ( $e ) {
348             file_failure->throw( "error opening $file: $!" );
349             }
350              
351 1         515 my $idx = 0;
352 1         4 for my $line ( @lines ) {
353 750         1036 ++$idx;
354 750         4593 my ( $var, $value ) = $line =~ /^\s*([^:]+?)\s*:\s*(.*?)\s*$/;
355 750 50 33     2442 file_failure->throw(
356             sprintf( "%s:%d: unable to parse line", $file, $idx ) )
357             unless defined $var and defined $value;
358 750         1540 $self->insert( $var, $value );
359             }
360              
361 1         44 return $self;
362             }
363              
364              
365              
366              
367              
368              
369              
370              
371              
372              
373 1     1 1 531 sub write_file ( $self, $file ) {
  1         3  
  1         2  
  1         2  
374 1         463 require File::Slurper;
375 1         3044 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         2  
  1         2  
393              
394 1         5 my $folded = $self->_folded;
395 1         3 my @records;
396              
397 1         131 for my $key ( keys $folded->%* ) {
398 1500         2392 my $value = $folded->{$key};
399              
400 1500 50       6384 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
401 8 100   8   22951 next unless $+{component} eq VALUE;
  8         3062  
  8         3262  
  1500         6603  
402 750         2736 $key = $+{key};
403             }
404              
405 750         2285 push @records, "$key : $value";
406             }
407              
408 1         467 return 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         1  
  1         2  
422              
423 1         581 require Hash::Merge;
424 1         8950 my $merger = Hash::Merge->new( 'RIGHT_PRECEDENT' );
425              
426 1         74 $self->_db->%* = $merger->merge( $self->TO_HASH->{db}, $other->TO_HASH->{db} )->%*;
427              
428 1         539 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   42 %KV_CONSTANTS = ( map { uc( "KV_$_" ) => $_ }
  40         365  
512             qw( all string array value match_count ) );
513              
514             }
515 8     8   60 use constant \%KV_CONSTANTS;
  8         25  
  8         9624  
516              
517              
518 4     4   5 sub _to_kv_xx ( $self, %iopt ) {
  4         7  
  4         10  
  4         6  
519 4         19 %iopt = ( key => KV_STRING, value => KV_VALUE, %iopt );
520              
521 4         8 state $match = {
522             value =>
523 1         68 qr/^(? @{[ join '|', KV_VALUE, KV_MATCH_COUNT, KV_ALL ]} )$/xi,
524 1         31 key => qr/^(? @{[ join '|', KV_STRING, KV_ARRAY ]} )$/xi,
525             };
526              
527             my %opt = map {
528 4         11 parameter_failure->throw( "illegal value for '$_' option: $iopt{$_}" )
529 8 50       79 unless $iopt{$_} =~ $match->{$_};
530 8         71 $_ => $+{match};
531             } qw( key value );
532              
533             parameter_failure->throw( "illegal option: $_" )
534 4         22 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         11 my $normalize_keys = $opt{key} eq KV_STRING;
540 4         10 my $folded = $self->_folded( $normalize_keys );
541              
542             # first get values
543             # return single requested value
544 4 100       21 if ( my $component = $RMETA{ $opt{value} } ) {
    50          
545              
546 3         12 for my $key ( keys $folded->%* ) {
547 30 50       206 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
548             # only allow the requested data out
549 30         58 my $value = delete $folded->{$key};
550             $folded->{ $+{key} } = $value
551 30 100       183 if $+{component} eq $component;
552             }
553             }
554             }
555              
556             elsif ( $opt{value} eq KV_ALL ) {
557              
558 1         6 for my $key ( keys $folded->%* ) {
559 10 50       111 if ( $key =~ /^(?.*)[.](?${META_QR})$/ ) {
560             ( $folded->{ $+{key} } //= {} )->{ $META{ $+{component} } }
561 10   100     152 = 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       27 if $opt{key} eq KV_STRING;
573              
574 5         27 return [ map { [ [ split( /[.]/, $_ ) ], $folded->{$_} ] } keys $folded->%* ]
575 1 50       7 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 13588 sub to_kv ( $self, %opt ) {
  3         6  
  3         8  
  3         3  
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 7388 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 7221 sub TO_HASH ( $self ) {
  12         25  
  12         21  
686 12         3113 require Storable;
687              
688             {
689 12         24498 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   12 sub _folded ( $self, $normalize_names = 1 ) {
  5         7  
  5         13  
  5         6  
704              
705             # Hash::Fold is overkill
706 5         1169 require Hash::Fold;
707 5         39868 my $folded = Hash::Fold->new( delimiter => '.' )->fold( $self->TO_HASH->{db} );
708              
709 5 100       93028 return $folded unless $normalize_names;
710              
711 4         242 for my $key ( keys %$folded ) {
712 1530         2814 my $nkey = normalize_key( $key );
713 1530         5449 $folded->{$nkey} = delete $folded->{$key};
714             }
715              
716 4         144 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__