File Coverage

blib/lib/Config/ROFL.pm
Criterion Covered Total %
statement 99 103 96.1
branch 23 36 63.8
condition 2 5 40.0
subroutine 29 29 100.0
pod 2 2 100.0
total 155 175 88.5


line stmt bran cond sub pod time code
1             package Config::ROFL;
2              
3 1     1   124839 use strict;
  1         2  
  1         30  
4 1     1   7 use warnings;
  1         5  
  1         23  
5              
6 1     1   16 use v5.10;
  1         3  
7              
8 1     1   5 use Carp ();
  1         1  
  1         13  
9 1     1   532 use Config::ZOMG ();
  1         61250  
  1         30  
10 1     1   607 use Data::Rmap ();
  1         1490  
  1         27  
11 1     1   427 use File::Share ();
  1         28739  
  1         28  
12 1     1   7 use Path::Tiny qw( cwd path );
  1         2  
  1         57  
13 1     1   6 use List::Util ();
  1         2  
  1         22  
14 1     1   8 use Scalar::Util qw( readonly );
  1         2  
  1         39  
15 1     1   654 use Types::Standard qw/Str HashRef/;
  1         85014  
  1         11  
16 1     1   1024 use FindBin qw/$Bin/;
  1         2  
  1         128  
17              
18 1     1   7 use Moo;
  1         3  
  1         9  
19 1     1   903 use namespace::clean;
  1         12476  
  1         6  
20              
21             has 'global_path' => is => 'lazy', isa => Str, default => sub { $ENV{CONFIG_ROFL_GLOBAL_PATH} // '/etc' };
22             has 'config' => is => 'rw', lazy => 1, builder => 1;
23             has 'config_path' => is => 'lazy', coerce => sub { ref $_[0] eq 'Path::Tiny' ? $_[0] : path($_[0]); }, builder => 1;
24             has 'dist' => is => 'lazy', isa => Str, default => '';
25             has 'relative_dir' => is => 'lazy', coerce => sub { ref $_[0] eq 'Path::Tiny' ? $_[0] : path($_[0]); }, builder => 1;
26             has 'mode' => is => 'lazy', isa => Str, default => sub { $ENV{CONFIG_ROFL_MODE} // ($ENV{HARNESS_ACTIVE} && 'test' || 'dev') };
27             has 'name' => is => 'lazy', isa => Str, default => sub { $ENV{CONFIG_ROFL_NAME} || 'config' };
28             has 'lookup_order' => is => 'lazy', default => sub {
29             [ 'global_path', (shift->mode eq 'test') ? ('relative', 'by_dist', 'by_self') : ('by_dist', 'by_self', 'relative') ]
30             };
31              
32             sub _build_relative_dir {
33 1     1   25 my ($self) = @_;
34              
35 1 50       5 return $ENV{CONFIG_ROFL_RELATIVE_DIR} if $ENV{CONFIG_ROFL_RELATIVE_DIR};
36              
37 1 50       5 if (ref $self eq __PACKAGE__) {
38 0 0       0 my $root = $Bin =~ m{/(?:bin|script|lib|t)\z}gmx ? Path::Tiny->new($Bin)->parent: $Bin;
39 0         0 return $root->child('share');
40             } else {
41 1         4 my $pm = _class_to_pm(ref $self);
42 1 50       6 if (my $path = $INC{$pm}) {
43 1         4 return path($path)->parent->parent->child('share');
44             }
45             }
46             }
47              
48             with 'MooX::Singleton';
49              
50             sub _build_config {
51 9     9   97 my ($self) = @_;
52              
53 9         157 my $config = Config::ZOMG->new(
54             name => $self->name,
55             path => $self->config_path,
56             local_suffix => $self->mode,
57             driver =>
58             { General => {'-LowerCaseNames' => 1, '-InterPolateEnv' => 1, '-InterPolateVars' => 1,}, }
59             );
60              
61 9         8076 $config->load;
62              
63 9 50       120582 if ($config->found) {
64 9         376 _post_process_config($config->load);
65 9         211 say {*STDERR} "Loaded configs: " . (
66             join ', ',
67             map {
68 10         345 my $realpath = path($_)->realpath;
69 10         2984 my $rel_path = cwd->relative($realpath);
70 10 50       4260 $rel_path =~ /^\.\./ ? $realpath : $rel_path
71             } $config->found
72 9 50       36 ) if $ENV{CONFIG_ROFL_DEBUG};
73             }
74             else {
75 0         0 Carp::croak 'Could not find config file: ' . $self->config_path . '/' . $self->name . '.(conf|yml|json)';
76             }
77              
78 9         556 return $config;
79             }
80              
81             around 'config' => sub {
82             my $orig = shift;
83             my $self = shift;
84              
85             return $orig->($self, @_)->load;
86             };
87              
88             sub _build_config_path {
89 8     8   461 my $self = shift;
90              
91 8         14 my $path;
92              
93 8         21 for my $type (@{ $self->lookup_order }) {
  8         146  
94 14         354 my $method = "_lookup_$type";
95 14 100       68 if ($path = $self->$method) {
96 8         460 warn "Found config via '$method'";
97 8 100       89 return $method eq '_lookup_global_path' ? path($path) : path($path)->child('/etc');
98             }
99             }
100              
101 0 0       0 die 'Could not find relative path (' . $self->relative_dir . ') , nor dist path (' . $self->dist . ')' unless $path;
102              
103             }
104              
105              
106             sub _post_process_config {
107 9     9   86 my ($hash) = @_;
108              
109             Data::Rmap::rmap_scalar {
110 13 50 33 13   1342 defined $_ && (!readonly $_) && ($_ =~ s/__ENV\((\w+)\)__/_env_substitute($1)/eg);
  2         7  
111             }
112 9         79 $hash;
113              
114 9         299 return;
115             }
116              
117             sub _env_substitute {
118 2     2   7 my ($prefix) = @_;
119 2   50     16 return $ENV{$prefix} || '';
120             }
121              
122             sub _class_to_pm {
123 1     1   3 my ($module) = @_;
124 1         9 $module =~ s{(-|::)}{/}g;
125 1         4 return "$module.pm";
126             }
127              
128             sub _lookup_relative {
129 4     4   13 my ($self) = @_;
130              
131 4         112 my $path = $self->relative_dir;
132 4 100       62 return $path if $path->exists;
133             }
134              
135             sub _lookup_by_dist {
136 2     2   6 my ($self) = @_;
137              
138 2         3 my $path;
139 2 100       43 return $path unless $self->dist;
140              
141 1 50       17 eval { $path = File::Share::dist_dir($self->dist) } or warn $@;
  1         17  
142              
143 1         730 return $path;
144             }
145              
146             sub _lookup_by_self {
147 1     1   3 my ($self) = @_;
148              
149 1         3 my $path;
150 1 50       2 eval { $path = File::Share::dist_dir(ref $self) } or warn $@;
  1         5  
151              
152 1         197 return $path;
153             }
154              
155             sub _lookup_global_path {
156 7     7   19 my ($self) = @_;
157              
158 7 100       30 return $ENV{CONFIG_ROFL_CONFIG_PATH} if $ENV{CONFIG_ROFL_CONFIG_PATH};
159              
160 5 100   22   109 if (List::Util::first {-e} glob path($self->global_path, $self->name) . '.{conf,yml,yaml,json,ini}') {
  22         1097  
161 1         30 return $self->global_path;
162             }
163             }
164              
165             sub get {
166 14     14 1 22032 my ($self, @keys) = @_;
167              
168 14 100   24   397 return List::Util::reduce { $a->{$b} || $a->{lc $b} } $self->config, @keys;
  24         392  
169             }
170              
171 4     4 1 3567 sub share_file { shift->config_path->parent->child(@_) }
172              
173             1;
174              
175             =encoding utf8
176              
177             =head1 NAME
178              
179             Config::ROFL - Yet another config module
180              
181             =head1 SYNOPSIS
182              
183             use Config::ROFL;
184             my $config = Config::ROFL->new;
185             $config->get("frobs");
186             $config->get(qw/mail server host/);
187              
188             $config->share_file("system.yml");
189              
190             =head1 DESCRIPTION
191              
192             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.
193              
194             =head1 ATTRIBUTES
195              
196             =head2 config
197              
198             Returns a hashref representation of the config
199              
200             =head2 dist
201              
202             The dist name used to find a share dir where the config file is located.
203              
204             =head2 global_path
205              
206             Global path overriding any lookup by dist, relative or by class of object.
207              
208             =head2 mode
209              
210             String used as part of name to lookup up config merged on top of general config.
211             For instance if mode is set to "production", the config used will be: config.production.yml merged on top of config.yml
212             Default is 'dev', except when HARNESS_ACTIVE env-var is set for instance when running tests, then mode is 'test'.
213              
214             =head2 name
215              
216             Name of config file, default is "config"
217              
218             =head2 config_path
219              
220             Path where to look for config files.
221              
222             =head2 lookup_order
223              
224             Order of config lookup. Default is ['by_dist', 'by_self', 'relative'], except when under tests when it is ['relative', 'by_dist', 'by_self']
225              
226             =head1 METHODS
227              
228             =head2 get
229              
230             Gets a config value, supports an array of strings to traverse down to a certain child hash value.
231              
232             =head2 new
233              
234             Create a new config instance
235              
236             =head2 new
237              
238             Get an existing config instance if already created see L. Beware that altering env-vars between invocations will not affect the instance init args.
239              
240             =head2 share_file
241              
242             Gets the full path to a file residing in the share folder relative to the config.
243              
244             =head1 SEE ALSO
245              
246             L
247              
248             L
249              
250             L
251              
252             L
253              
254             =head1 COPYRIGHT
255              
256             Nicolas Mendoza 2023 - All rights reserved
257              
258             =cut