File Coverage

blib/lib/Config/YAMLMacros.pm
Criterion Covered Total %
statement 91 97 93.8
branch 31 42 73.8
condition 4 7 57.1
subroutine 10 10 100.0
pod 0 3 0.0
total 136 159 85.5


line stmt bran cond sub pod time code
1              
2             package Config::YAMLMacros;
3              
4 1     1   34051 use strict;
  1         3  
  1         49  
5 1     1   7 use warnings;
  1         2  
  1         41  
6 1     1   581 use Config::YAMLMacros::YAML qw(Load);
  1         3  
  1         72  
7 1     1   5 use File::Slurp qw(read_file);
  1         2  
  1         260  
8 1     1   7 use Carp qw(confess);
  1         1  
  1         56  
9 1     1   5 use File::Basename qw(basename dirname);
  1         1  
  1         2612  
10             require Hash::Merge;
11             require Exporter;
12              
13             our @ISA = qw(Exporter);
14             our @EXPORT = qw(get_config);
15             our @EXPORT_OK = (@EXPORT, qw(listify replace));
16              
17             my $max_replace_iterations = 10;
18              
19             sub listify(\%@)
20             {
21 7     7 0 18 my ($href, @keys) = @_;
22 7         13 for my $k (@keys) {
23 12 100       37 next unless exists $href->{$k};
24 5 100       26 if (! ref($href->{$k})) {
    50          
25 2         10 $href->{$k} = [ $href->{$k} ];
26             } elsif (ref($href->{$k}) eq 'ARRAY') {
27             # fine
28             } else {
29 0         0 confess;
30             }
31             }
32             }
33              
34             sub replace(\%\$)
35             {
36 11     11 0 16 my ($href, $sref) = @_;
37 11         29 my $jlist = join('|', map { "\Q$_\E" } keys %$href);
  18         49  
38 11 100       30 return unless $jlist;
39 8         182 my $re = qr/$jlist/;
40 8         17 my $iteration = 0;
41             my $replace = sub {
42             # print STDERR "# replacing '$_[0]' with '$href->{$_[0]}'\n";
43 8     8   51 return $href->{$_[0]};
44 8         32 };
45 8         9 for (;;) {
46 15 100       266 $$sref =~ s/($re)/$replace->($1)/ge or last;
  8         21  
47 7 50       26 if ($iteration++ >= $max_replace_iterations) {
48 0         0 confess "too many replacements in $$sref";
49             }
50             }
51             }
52              
53             sub get_config
54             {
55 5     5 0 27 my ($config_file, %opts) = @_;
56              
57 5         22 my $raw = read_file($config_file);
58 5         560 my @sections = split(/^---\n/m, $raw);
59              
60 5         25 my %metakeys = (
61             EVAL_REPLACE => 'do string replacements with evaluated perl',
62             REPLACE => 'do string replacements',
63             NO_REPLACE => 'stop doing string replacements',
64             INCLUDE => 'include another file',
65             OVERRIDE_FROM => 'overrides from another file',
66             );
67              
68 5         20 my $old_behavior = Hash::Merge::get_behavior();
69 5   50     79 Hash::Merge::set_behavior($opts{merge_behavior} || 'RETAINMENT_PRECEDENT');
70              
71 5 100       78 my %replacements = $opts{replacements} ? %{$opts{replacements}} : ();
  4         22  
72 5         11 my $config = {};
73 5         14 while (@sections) {
74 14         7279 my $yaml = shift @sections;
75 14 100       38 next unless $yaml; # skip empty sections
76 11         19 $yaml =~ s/^(\t+)/" " x length($1) * 8/e;
  0         0  
77 11         17 my $newstuff = eval { Load( { file => $config_file }, "---\n$yaml"); };
  11         62  
78 11 50       33 die "When loadking from $config_file, YAML error: $@" if $@;
79 11         12 my $meta = 0;
80 11         11 my $non_meta = 0;
81 11         32 for my $k (keys %$newstuff) {
82 20 100       37 if ($metakeys{$k}) {
83 7         15 $meta++;
84             } else {
85 13         24 $non_meta++;
86             }
87             }
88 11 50 66     57 if ($meta && $non_meta) {
    100          
89 0         0 die;
90             } elsif ($meta) {
91 5 100       14 if ($newstuff->{NO_REPLACE}) {
92 2         5 listify(%$newstuff, 'NO_REPLACE');
93 2         3 delete @replacements{@{$newstuff->{NO_REPLACE}}};
  2         8  
94             }
95 5         16 replace(%replacements, $yaml);
96 5         36 $newstuff = Load( { file => $config_file }, "---\n$yaml");
97 5 100       25 @replacements{keys %{$newstuff->{REPLACE}}} = values %{$newstuff->{REPLACE}}
  1         54  
  1         3  
98             if $newstuff->{REPLACE};
99 5 100       14 if ($newstuff->{EVAL_REPLACE}) {
100 1 50       5 die "In $config_file, EVAL_REPLACE should be a hash"
101             unless ref($newstuff->{EVAL_REPLACE}) eq 'HASH';
102 1         2 for my $ekey (keys %{$newstuff->{EVAL_REPLACE}}) {
  1         3  
103 1         91 $replacements{$ekey} = eval $newstuff->{EVAL_REPLACE}{$ekey};
104 1 50       6 die "Eval failure for $ekey in $config_file: $@" if $@;
105             }
106             }
107 5         16 listify(%$newstuff, qw(INCLUDE OVERRIDE_FROM));
108 5         9 for my $include (@{$newstuff->{INCLUDE}}) {
  5         13  
109 3 50       172 die if ref($include);
110              
111 3 50       94 if (! -e $include) {
112 0         0 my $alt = dirname($config_file) . "/" . $include;
113 0 0       0 $include = $alt if -e $alt;
114             }
115              
116 3         18 my $conf = get_config($include, %opts, replacements => \%replacements);
117              
118 3         11 $config = Hash::Merge::merge($config, $conf);
119             }
120 5         402 for my $override (@{$newstuff->{OVERRIDE_FROM}}) {
  5         28  
121 1         4 my $conf = get_config($override, %opts, replacements => \%replacements);
122 1         4 Hash::Merge::set_behavior('RIGHT_PRECEDENT');
123 1         16 $config = Hash::Merge::merge($config, $conf);
124 1   50     269 Hash::Merge::set_behavior($opts{merge_behavior} || 'RETAINMENT_PRECEDENT');
125             }
126             } else {
127             # non-meta, normal
128 6         16 replace(%replacements, $yaml);
129 6         34 $newstuff = Load( { file => $config_file }, "---\n$yaml");
130 6         31 $config = Hash::Merge::merge($config, $newstuff);
131             }
132             }
133 5 50       890 Hash::Merge::set_behavior($old_behavior) if $old_behavior;
134 5         95 return $config;
135             }
136              
137              
138             __END__