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   120923 use strict;
  1         2  
  1         37  
4 1     1   5 use warnings;
  1         9  
  1         24  
5              
6 1     1   9 use v5.10;
  1         4  
7              
8 1     1   6 use Carp ();
  1         2  
  1         13  
9 1     1   523 use Config::ZOMG ();
  1         59239  
  1         24  
10 1     1   543 use Data::Rmap ();
  1         1464  
  1         23  
11 1     1   410 use File::Share ();
  1         26459  
  1         27  
12 1     1   7 use Path::Tiny qw( cwd path );
  1         2  
  1         68  
13 1     1   13 use List::Util ();
  1         2  
  1         23  
14 1     1   7 use Scalar::Util qw( readonly );
  1         2  
  1         46  
15 1     1   628 use Types::Standard qw/Str HashRef/;
  1         80242  
  1         8  
16 1     1   1433 use FindBin qw/$Bin/;
  1         3  
  1         112  
17              
18 1     1   6 use Moo;
  1         4  
  1         17  
19 1     1   858 use namespace::clean;
  1         12064  
  1         29  
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   26 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   76 my ($self) = @_;
52              
53 9         138 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         7586 $config->load;
62              
63 9 50       113102 if ($config->found) {
64 9         338 _post_process_config($config->load);
65 9         194 say {*STDERR} "Loaded configs: " . (
66             join ', ',
67             map {
68 10         295 my $realpath = path($_)->realpath;
69 10         2461 my $rel_path = cwd->relative($realpath);
70 10 50       3954 $rel_path =~ /^\.\./ ? $realpath : $rel_path
71             } $config->found
72 9 50       28 ) 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         515 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   386 my $self = shift;
90              
91 8         13 my $path;
92              
93 8         14 for my $type (@{ $self->lookup_order }) {
  8         130  
94 14         301 my $method = "_lookup_$type";
95 14 100       61 if ($path = $self->$method) {
96 8         448 warn "Found config via '$method'";
97 8 100       71 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   63 my ($hash) = @_;
108              
109             Data::Rmap::rmap_scalar {
110 13 50 33 13   1235 defined $_ && (!readonly $_) && ($_ =~ s/__ENV\((\w+)\)__/_env_substitute($1)/eg);
  2         5  
111             }
112 9         51 $hash;
113              
114 9         271 return;
115             }
116              
117             sub _env_substitute {
118 2     2   5 my ($prefix) = @_;
119 2   50     15 return $ENV{$prefix} || '';
120             }
121              
122             sub _class_to_pm {
123 1     1   2 my ($module) = @_;
124 1         8 $module =~ s{(-|::)}{/}g;
125 1         3 return "$module.pm";
126             }
127              
128             sub _lookup_relative {
129 4     4   15 my ($self) = @_;
130              
131 4         90 my $path = $self->relative_dir;
132 4 100       47 return $path if $path->exists;
133             }
134              
135             sub _lookup_by_dist {
136 2     2   5 my ($self) = @_;
137              
138 2         3 my $path;
139 2 100       39 return $path unless $self->dist;
140              
141 1 50       14 eval { $path = File::Share::dist_dir($self->dist) } or warn $@;
  1         15  
142              
143 1         636 return $path;
144             }
145              
146             sub _lookup_by_self {
147 1     1   4 my ($self) = @_;
148              
149 1         2 my $path;
150 1 50       2 eval { $path = File::Share::dist_dir(ref $self) } or warn $@;
  1         4  
151              
152 1         178 return $path;
153             }
154              
155             sub _lookup_global_path {
156 7     7   12 my ($self) = @_;
157              
158 7 100       20 return $ENV{CONFIG_ROFL_CONFIG_PATH} if $ENV{CONFIG_ROFL_CONFIG_PATH};
159              
160 5 100   22   104 if (List::Util::first {-e} glob path($self->global_path, $self->name) . '.{conf,yml,yaml,json,ini}') {
  22         960  
161 1         29 return $self->global_path;
162             }
163             }
164              
165             sub get {
166 14     14 1 20464 my ($self, @keys) = @_;
167              
168 14 100   24   361 return List::Util::reduce { $a->{$b} || $a->{lc $b} } $self->config, @keys;
  24         364  
169             }
170              
171 4     4 1 3437 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