File Coverage

blib/lib/Clustericious/Config.pm
Criterion Covered Total %
statement 165 165 100.0
branch 51 60 85.0
condition 6 7 85.7
subroutine 31 32 96.8
pod 1 1 100.0
total 254 265 95.8


line stmt bran cond sub pod time code
1             package Clustericious::Config;
2              
3 60     60   130278 use strict;
  60         188  
  60         2150  
4 53     53   273 use warnings;
  53         116  
  53         1280  
5 52     52   913 use 5.010;
  52         173  
6 52     52   5270 use Clustericious;
  52         141  
  52         1472  
7 52     51   425 use List::Util ();
  51         98  
  51         690  
8 51     51   1359 use YAML::XS ();
  51         44886  
  51         888  
9 51     51   10746 use Mojo::Template;
  51         2637063  
  51         589  
10 51     51   16503 use Log::Log4perl qw( :nowarn );
  51         1484148  
  51         310  
11 51     51   12896 use Storable ();
  51         54329  
  51         982  
12 51     51   9334 use Clustericious::Config::Helpers ();
  51         133  
  51         956  
13 51     51   6257 use Mojo::URL;
  51         123212  
  51         595  
14 51     51   1368 use File::Spec;
  51         110  
  51         1159  
15 51     51   233 use File::Temp ();
  51         100  
  51         611  
16 51     51   276 use Carp ();
  51         100  
  51         50292  
17              
18             # ABSTRACT: Configuration files for Clustericious nodes.
19             our $VERSION = '1.27'; # VERSION
20              
21              
22             our %singletons;
23              
24             our $class_suffix = {};
25             sub Clustericious::_config_uncache {
26 27     27   83 my($class, $name) = @_;
27 27         63 delete $singletons{$name};
28 27   100     199 $class_suffix->{$name} //= 1;
29 27         77 $class_suffix->{$name}++;
30             }
31              
32              
33             sub new {
34 104     104 1 8647 my $class = shift;
35              
36 104         649 my $logger = Log::Log4perl->get_logger(__PACKAGE__);
37              
38             # (undocumented; for now)
39             # callback is used by the configdebug command;
40             # may be used elsewise at a later time
41 104 50   114   17182 my $callback = ref $_[-1] eq 'CODE' ? pop : sub {};
42              
43 104 100       355 my %t_args = (ref $_[-1] eq 'ARRAY' ? @{( pop )} : () );
  3         9  
44              
45 104         198 my $arg = $_[0];
46 104 50       295 ($arg = caller) =~ s/:.*$// unless $arg; # Determine from caller's class
47 104 50       310 return $singletons{$arg} if exists($singletons{$arg});
48              
49 104         169 my $conf_data;
50              
51 104         185 state $package_counter = 0;
52 104         273 my $namespace = "Clustericious::Config::TemplatePackage::Package$package_counter";
53 104     41   7253 eval qq{ package $namespace; use Clustericious::Config::Helpers; };
  41     30   279  
  41     20   87  
  41         3429  
  30         205  
  30         96  
  30         2128  
  20         147  
  20         43  
  20         1590  
54 104 50       464 die $@ if $@;
55 104         209 $package_counter++;
56            
57 104         812 my $mt = Mojo::Template->new(namespace => $namespace)->auto_escape(0);
58 104         2149 $mt->prepend( join "\n", map " my \$$_ = q{$t_args{$_}};", sort keys %t_args );
59              
60 104 100       866 if(ref $arg eq 'HASH')
61             {
62 42         1605 $conf_data = Storable::dclone $arg;
63             }
64             else
65             {
66 62         121 my $filename;
67            
68 62 100       252 if($arg =~ /\.conf$/)
69             {
70 1         2 $filename = $arg;
71             }
72             else
73             {
74 61         124 my $name = $arg;
75 61         182 $name =~ s/::/-/g;
76             ($filename) =
77 67     67   805 List::Util::first { -f $_ }
78 61         824 map { File::Spec->catfile($_, "$name.conf") }
  90         1095  
79             Clustericious->_config_path;
80            
81 61 100       392 unless($filename)
82             {
83 10 100       62 $logger->trace("could not find $name file.") if $logger->is_trace;
84 10         108 $conf_data = {};
85             }
86             }
87              
88 62 100       220 if ($filename) {
89 52 100       263 $logger->trace("reading from config file $filename") if $logger->is_trace;
90 52         521 $callback->(pre_rendered => $filename);
91 52         279 my $rendered = $mt->render_file($filename);
92 52         651 $callback->(rendered => $filename => $rendered);
93              
94 52 50       191 die $rendered if ( (ref $rendered) =~ /Exception/ );
95              
96 52         103 $conf_data = eval { YAML::XS::Load($rendered) };
  52         2524  
97 52 50       261 $logger->logdie("Could not parse\n-------\n$rendered\n---------\n$@\n") if $@;
98             } else {
99 10         46 $callback->('not_found' => "$arg");
100             }
101             }
102              
103 104   100     368 $conf_data ||= {};
104 104         641 Clustericious::Config::Helpers->_do_merges($conf_data);
105              
106             # Use derived classes so that AUTOLOADING keeps namespaces separate
107             # for various apps.
108 104 100       7851 if ($class eq __PACKAGE__)
109             {
110 64 100       353 if(ref $arg)
    100          
111             {
112 2         4 $arg = "$arg";
113 2         6 $arg =~ tr/a-zA-Z0-9//cd;
114             }
115             elsif($arg =~ s/\.conf$//)
116             {
117             # NOTE: may revisit this later.
118 1 50       5 $arg = "cwd::$arg" unless $arg =~ s{^/+}{root::};
119 1         6 $arg =~ s{[\\/]}{::}g;
120 1         2 $arg =~ s{\.\.::}{__up__::}g;
121 1         3 $arg =~ tr/a-zA-Z0-0_://cd;
122 1         2 $arg =~ s/:{3,}/::/g;
123             }
124 64         187 $arg =~ s/-/::/g;
125 64         224 $class = join '::', $class, 'App', $arg;
126 64 100       317 $class .= $class_suffix->{$arg} if $class_suffix->{$arg};
127 64         303 my $dome = '# line '. __LINE__ . ' "' . __FILE__ . qq("\n) . '@'."$class"."::ISA = ('".__PACKAGE__. "')";
128 64         2325 eval $dome;
129 64 50       325 die "error setting ISA : $@" if $@;
130             }
131 104         1078 bless $conf_data, $class;
132             }
133              
134             # defined so that AUTOLOAD doesn't get called
135             # when config falls out of scope.
136       0     sub DESTROY {
137             }
138              
139             sub AUTOLOAD {
140 231     231   20977 my($self, %args) = @_;
141            
142             # NOTE: I hope to deprecated and later remove defining defaults in this way in the near future.
143 231         536 my $default = $args{default};
144 231         443 my $default_exists = exists $args{default};
145              
146 231         309 our $AUTOLOAD;
147 231         357 my $called = $AUTOLOAD;
148 231         1259 $called =~ s/.*:://g;
149              
150 231         572 my $value = $self->{$called};
151 231         432 my $invocant = ref $self;
152 231 100       625 my $obj = ref $value eq 'HASH' ? $invocant->new($value) : undef;
153              
154             my $sub = sub {
155 355     355   1608 my $self = shift;
156 355         472 my $value;
157            
158 355 100       1412 if(exists $self->{$called})
    100          
159             {
160 203         411 $value = $self->{$called};
161             }
162             elsif($default_exists)
163             {
164 149 100       544 $value = $self->{$called} = ref $default eq 'CODE' ? $default->() : $default;
165 149 100       5999 $obj = ref $value eq 'HASH' ? $invocant->new($value) : undef;
166             }
167             else
168             {
169 3         7 Carp::croak "'$called' configuration item not found. Values present: @{[keys %$self]}";
  3         257  
170             }
171              
172 352         678 my $ref = ref $value;
173 352 100       795 if($ref)
174             {
175 165 100       357 if(wantarray)
176             {
177 94 100       265 return %$value if $ref eq 'HASH';
178 91 50       525 return @$value if $ref eq 'ARRAY';
179             }
180 71 100       313 return $obj if $obj;
181 27 100       33 $value = $value->execute if eval { $value->can('execute') };
  27         183  
182             }
183 214         1057 $value;
184 231         1062 };
185 51     51   401 do { no strict 'refs'; *{ $invocant . "::$called" } = $sub };
  51         127  
  51         3969  
  231         383  
  231         309  
  231         1032  
186 231         510 $sub->($self);
187             }
188              
189              
190             package Clustericious::Config::Callback;
191              
192 51     51   331 use JSON::MaybeXS qw( encode_json );
  51         126  
  51         8266  
193              
194             sub new
195             {
196 6     6   8046 my($class, @args) = @_;
197 6         21 bless [@args], $class;
198             }
199              
200 3     3   1003 sub args { @{ shift() } }
  3         24  
201              
202 1     1   5 sub execute { '' }
203              
204             sub to_yaml
205             {
206 6     6   12 my($self) = @_;
207 6         10 "!!perl/array:@{[ ref $self ]} @{[ encode_json [@$self] ]}";
  6         20  
  6         42  
208             }
209              
210             package Clustericious::Config::Callback::Password;
211              
212 51     51   354 use base qw( Clustericious::Config::Callback );
  51         105  
  51         10501  
213              
214             sub execute
215             {
216 4     4   408 state $pass;
217 4   66     12 $pass //= do { require Term::Prompt; Term::Prompt::prompt('p', 'Password:', '', '') };
  2         12  
  2         9  
218 4         17 $pass;
219             }
220              
221             1;
222              
223             __END__
224              
225             =pod
226              
227             =encoding UTF-8
228              
229             =head1 NAME
230              
231             Clustericious::Config - Configuration files for Clustericious nodes.
232              
233             =head1 VERSION
234              
235             version 1.27
236              
237             =head1 SYNOPSIS
238              
239             In your ~/etc/MyApp.conf file:
240              
241             ---
242             % extends_config 'global';
243             % extends_config 'hypnotoad', url => 'http://localhost:9999', app => 'MyApp';
244              
245             url : http://localhost:9999
246             start_mode : hypnotoad
247             hypnotoad :
248             - heartbeat_timeout : 500
249            
250             arbitrary_key: value
251              
252             In your ~/etc/globa.conf file:
253              
254             ---
255             somevar : somevalue
256              
257             In your ~/etc/hypnotoad.conf:
258              
259             listen :
260             - <%= $url %>
261             # home uses ~ to find the calling users'
262             # home directory
263             pid_file : <%= home %>/<%= $app %>/hypnotoad.pid
264             env :
265             MOJO_HOME : <%= home %>/<%= $app %>
266              
267             From a L<Clustericious::App>:
268              
269             package MyApp;
270            
271             use Mojo::Base qw( Clustericious::App );
272            
273             package MyApp::Routes;
274            
275             use Clustericious::RouteBuilder;
276            
277             get '/' => sub {
278             my $c = shift;
279             my $config = $c; # $config isa Clustericious::Config
280            
281             # returns the value if it is defined, foo otherwise
282             my $value1 = $config->arbitrary_key1(default => 'foo');
283            
284             # returns the value if it is defined, bar otherwise
285             # code reference is only called if the value is NOT
286             # defined
287             my $value2 = $config->arbitrary_key2(default => sub { 'bar' });
288             };
289              
290             From a script:
291              
292             use Clustericious::Config;
293            
294             my $c = Clustericious::Config->new("MyApp");
295             my $c = Clustericious::Config->new( \%config_data_structure );
296              
297             print $c->url;
298             print $c->{url};
299              
300             print $c->hypnotoad->listen;
301             print $c->hypnotoad->{listen};
302             my %hash = $c->hypnotoad;
303             my @ary = $c->hypnotoad;
304              
305             # Supply a default value for a missing configuration parameter :
306             $c->url(default => "http://localhost:9999");
307             print $c->this_param_is_missing(default => "something_else");
308              
309             =head1 DESCRIPTION
310              
311             Clustericious::Config reads configuration files which are Mojo::Template's
312             of JSON or YAML files. There should generally be an entry for
313             'url', which may be used by either a client or a server depending on
314             how this node in the cluster is being used.
315              
316             After rendering the template and parsing the JSON, the resulting
317             object may be called using method calls or treated as hashes.
318              
319             Config files are looked for in the following places (in order, where
320             "MyApp" is the name of the app) :
321              
322             $CLUSTERICIOUS_CONF_DIR/MyApp.conf
323             $HOME/etc/MyApp.conf
324             /etc/MyApp.conf
325              
326             The helper "extends_config" may be used to read default settings
327             from another config file. The first argument to extends_config is the
328             basename of the config file. Additional named arguments may be passed
329             to that config file and used as variables within that file. After
330             reading another file, the hashes are merged (i.e. with Hash::Merge);
331             so values anywhere inside the data structure may be overridden.
332              
333             YAML config files must begin with "---", otherwise they are interpreted
334             as JSON.
335              
336             This module provides a number of helpers
337             which can be used to get system details (such as the home directory of
338             the calling user or to prompt for passwords). See L<Clustericious::Config::Helpers>
339             for details.
340              
341             =head1 CONSTRUCTOR
342              
343             =head2 new
344              
345             Create a new Clustericious::Config object. See the SYNOPSIS for
346             possible invocations.
347              
348             =head1 CAVEATS
349              
350             Some filesystems do not support filenames with a colon (:) character in
351             them, so for applications with a double colon in them (for example
352             L<Clustericious::HelloWorld>), a single dash character will be
353             substituted for the name (for example C<Clustericious-HelloWorld.conf>).
354              
355             L<Clustericious::Config> uses C<AUTOLOAD> to perform its magic, so some
356             configuration keys that are reserved by Perl cannot be used. Notably
357             C<new>, C<can>, C<isa>, etc.
358              
359             =head1 SEE ALSO
360              
361             L<Mojo::Template>, L<Hash::Merge>, L<Clustericious>, L<Clustericious::Client>, L<Clustericious::Config::Helpers>
362              
363             =head1 AUTHOR
364              
365             Original author: Brian Duggan
366              
367             Current maintainer: Graham Ollis E<lt>plicease@cpan.orgE<gt>
368              
369             Contributors:
370              
371             Curt Tilmes
372              
373             Yanick Champoux
374              
375             =head1 COPYRIGHT AND LICENSE
376              
377             This software is copyright (c) 2013 by NASA GSFC.
378              
379             This is free software; you can redistribute it and/or modify it under
380             the same terms as the Perl 5 programming language system itself.
381              
382             =cut