File Coverage

blib/lib/Config/ROFL.pm
Criterion Covered Total %
statement 110 115 95.6
branch 25 38 65.7
condition 3 5 60.0
subroutine 30 30 100.0
pod 2 2 100.0
total 170 190 89.4


line stmt bran cond sub pod time code
1             package Config::ROFL;
2             our $VERSION = '1.09';
3              
4 1     1   252664 use strict;
  1         2  
  1         43  
5 1     1   6 use warnings;
  1         2  
  1         117  
6              
7 1     1   17 use v5.10;
  1         4  
8              
9 1     1   8 use Carp ();
  1         4  
  1         30  
10 1     1   764 use Config::ZOMG ();
  1         93191  
  1         41  
11 1     1   983 use Data::Rmap ();
  1         2169  
  1         56  
12 1     1   544 use File::Share ();
  1         55474  
  1         45  
13 1     1   8 use Path::Tiny qw( cwd path );
  1         2  
  1         99  
14 1     1   7 use List::Util ();
  1         2  
  1         28  
15 1     1   5 use Scalar::Util qw( readonly );
  1         2  
  1         53  
16 1     1   1270 use Types::Standard qw/Str HashRef/;
  1         166546  
  1         14  
17 1     1   4212 use FindBin qw/$Bin/;
  1         3  
  1         149  
18              
19 1     1   9 use Moo;
  1         2  
  1         10  
20 1     1   1498 use namespace::clean;
  1         21178  
  1         7  
21              
22             has 'global_path' => is => 'lazy', isa => Str, default => sub { $ENV{CONFIG_ROFL_GLOBAL_PATH} // '/etc' };
23             has 'config' => is => 'rw', lazy => 1, builder => 1;
24             has 'config_path' => is => 'lazy', coerce => sub { ref $_[0] eq 'Path::Tiny' ? $_[0] : path($_[0]); }, builder => 1;
25             has 'dist' => is => 'lazy', isa => Str, default => '';
26             has 'relative_dir' => is => 'lazy', coerce => sub { ref $_[0] eq 'Path::Tiny' ? $_[0] : path($_[0]); }, builder => 1;
27             has 'mode' => is => 'lazy', isa => Str, default => sub { $ENV{CONFIG_ROFL_MODE} // ($ENV{HARNESS_ACTIVE} && 'test' || 'dev') };
28             has 'name' => is => 'lazy', isa => Str, default => sub { $ENV{CONFIG_ROFL_NAME} || 'config' };
29             has 'lookup_order' => is => 'lazy', default => sub {
30             [ 'global_path', (shift->mode eq 'test') ? ('relative', 'by_dist', 'by_self') : ('by_dist', 'by_self', 'relative') ]
31             };
32             has 'envvar_prefix' => is => 'lazy', default => '';
33              
34              
35             sub _build_relative_dir {
36 1     1   30 my ($self) = @_;
37              
38 1 50       6 if (ref $self eq __PACKAGE__) {
39 0         0 my $bin = $Bin;
40 0 0       0 my $root = ($bin =~ m{/(?:bin|script|share|lib|t)}gmx) ? path($bin)->parent : path($bin);
41 0         0 return $root->child('share');
42             } else {
43 1         5 my $pm = _class_to_pm(ref $self);
44 1 50       8 if (my $path = $INC{$pm}) {
45 1         5 return path($path)->parent->parent->child('share');
46             }
47             }
48             }
49              
50             with 'MooX::Singleton';
51              
52             sub _build_config {
53 11     11   119 my ($self) = @_;
54              
55 11         274 my $config = Config::ZOMG->new(
56             name => $self->name,
57             path => $self->config_path,
58             local_suffix => $self->mode,
59             driver =>
60             { General => {'-LowerCaseNames' => 1, '-InterPolateEnv' => 1, '-InterPolateVars' => 1,}, }
61             );
62              
63 11         11530 $config->load;
64              
65 11 50       202949 if ($config->found) {
66 11         594 _post_process_config($config->load);
67             $self->_log("Loaded configs: " . (
68             join ', ',
69             map {
70 11         311 my $realpath = path($_)->realpath;
  12         475  
71 12         3924 my $rel_path = cwd->relative($realpath);
72 12 50       5615 $rel_path =~ /^\.\./ ? $realpath : $rel_path
73             } $config->found
74             )
75             );
76             }
77             else {
78 0         0 Carp::croak 'Could not find config file: ' . $self->config_path . '/' . $self->name . '.(conf|yml|json)';
79             }
80              
81 11 100       623 if (my $prefix = $self->envvar_prefix) {
82 1         16 my $hash = {};
83 1         14 foreach my $key (grep { /^${prefix}_/ } keys %ENV) {
  30         104  
84 1         3 my $unprefixed_key = $key;
85 1         15 $unprefixed_key =~ s/^${prefix}_//gmx;
86 1         7 $hash->{lc $unprefixed_key} = $ENV{$key};
87             }
88 1         11 $config->_config(Hash::Merge::Simple->merge($config->_config, $hash))
89             }
90              
91 11         265 return $config;
92             }
93              
94             around 'config' => sub {
95             my $orig = shift;
96             my $self = shift;
97              
98             return $orig->($self, @_)->load;
99             };
100              
101             sub _build_config_path {
102 10     10   689 my $self = shift;
103              
104 10         22 my $path;
105              
106 10         19 for my $type (@{ $self->lookup_order }) {
  10         902  
107 17         357 my $method = "_lookup_$type";
108 17 100       126 if ($path = $self->$method) {
109 10         225 $self->_log("Found config via '$method'");
110 10 100       111 return $method eq '_lookup_global_path' ? path($path) : path($path)->child('/etc');
111             } else {
112 7         161 $self->_log("Looking up by '$method'");
113             }
114             }
115              
116 0 0       0 die 'Could not find relative path (' . $self->relative_dir . ') , nor dist path (' . $self->dist . ')' unless $path;
117              
118             }
119              
120              
121             sub _post_process_config {
122 11     11   110 my ($hash) = @_;
123              
124             Data::Rmap::rmap_scalar {
125 17 50 33 17   2086 defined $_ && (!readonly $_) && ($_ =~ s/__ENV\((\w+)\)__/_env_substitute($1)/eg);
  4         18  
126             }
127 11         113 $hash;
128              
129 11         658 return;
130             }
131              
132             sub _env_substitute {
133 4     4   16 my ($prefix) = @_;
134 4   100     46 return $ENV{$prefix} || '';
135             }
136              
137             sub _class_to_pm {
138 1     1   4 my ($module) = @_;
139 1         9 $module =~ s{(-|::)}{/}g;
140 1         5 return "$module.pm";
141             }
142              
143             sub _lookup_relative {
144 5     5   16 my ($self) = @_;
145              
146 5         186 my $path = $self->relative_dir;
147 5 100       75 return $path if $path->exists;
148             }
149              
150             sub _lookup_by_dist {
151 2     2   8 my ($self) = @_;
152              
153 2         4 my $path;
154 2 100       82 return $path unless $self->dist;
155              
156 1 50       12 eval { $path = File::Share::dist_dir($self->dist) } or $self->_log($@);
  1         22  
157              
158 1         1041 return $path;
159             }
160              
161             sub _lookup_by_self {
162 1     1   4 my ($self) = @_;
163              
164 1         3 my $path;
165              
166 1 50       6 if (ref $self ne __PACKAGE__) {
167 1 50       6 eval { $path = File::Share::dist_dir(ref $self) } or $self->_log($@);
  1         9  
168             }
169              
170 1         283 return $path;
171             }
172              
173             sub _lookup_global_path {
174 9     9   25 my ($self) = @_;
175              
176 9 100       45 return $ENV{CONFIG_ROFL_CONFIG_PATH} if $ENV{CONFIG_ROFL_CONFIG_PATH};
177              
178 6 100   27   187 if (List::Util::first {-e} glob path($self->global_path, $self->name) . '.{conf,yml,yaml,json,ini}') {
  27         1866  
179 1         31 return $self->global_path;
180             }
181             }
182              
183             sub _log {
184 28     28   285 my $self = shift;
185 28 50       121 say {*STDERR} shift if $ENV{CONFIG_ROFL_DEBUG};
  28         17541  
186             }
187              
188             sub get {
189 16     16 1 42101 my ($self, @keys) = @_;
190              
191 16 100   26   643 return List::Util::reduce { $a->{$b} || $a->{lc $b} } $self->config, @keys;
  26         529  
192             }
193              
194 4     4 1 16827 sub share_file { shift->config_path->parent->child(@_) }
195              
196             1;
197              
198             =encoding utf8
199              
200             =head1 NAME
201              
202             Config::ROFL - Yet another config module
203              
204             =head1 SYNOPSIS
205              
206             use Config::ROFL;
207             my $config = Config::ROFL->new;
208             $config->get("frobs");
209             $config->get(qw/mail server host/);
210              
211             $config->share_file("system.yml");
212              
213             =head1 DESCRIPTION
214              
215             Subclassable and auto-magic config module utilizing L. It looks up which config path to use based on current mode, share dir and class name. Falls back to a relative share dir when run as part of tests.
216              
217             =head1 ATTRIBUTES
218              
219             =head2 config
220              
221             Returns a hashref representation of the config
222              
223             =head2 dist
224              
225             The dist name used to find a share dir where the config file is located.
226              
227             =head2 global_path
228              
229             Global path overriding any lookup by dist, relative or by class of object.
230              
231             =head2 mode
232              
233             String used as part of name to lookup up config merged on top of general config.
234             For instance if mode is set to "production", the config used will be: config.production.yml merged on top of config.yml
235             Default is 'dev', except when HARNESS_ACTIVE env-var is set for instance when running tests, then mode is 'test'.
236              
237             =head2 name
238              
239             Name of config file, default is "config"
240              
241             =head2 config_path
242              
243             Path where to look for config files.
244              
245             =head2 lookup_order
246              
247             Order of config lookup. Default is ['by_dist', 'by_self', 'relative'], except when under tests when it is ['relative', 'by_dist', 'by_self']
248              
249             =head2 envvar_prefix
250              
251             Allowe overriding config values with environment variables starting with a given prefix. Setting envvar_prefix to 'MYPREFIX', will allow setting the environment variable
252             'MYPREFIX_MYKEY' to override the config key 'mykey'.
253              
254             =head1 METHODS
255              
256             =head2 get
257              
258             Gets a config value, supports an array of strings to traverse down to a certain child hash value.
259              
260             =head2 new
261              
262             Create a new config instance
263              
264             =head2 new
265              
266             Get an existing config instance if already created see L. Beware that altering env-vars between invocations will not affect the instance init args.
267              
268             =head2 share_file
269              
270             Gets the full path to a file residing in the share folder relative to the config.
271              
272             =head1 SEE ALSO
273              
274             L
275              
276             L
277              
278             L
279              
280             L
281              
282             =head1 COPYRIGHT
283              
284             Nicolas Mendoza 2023 - All rights reserved
285              
286             =cut