File Coverage

blib/lib/Config/Structured.pm
Criterion Covered Total %
statement 166 189 87.8
branch 36 48 75.0
condition 2 5 40.0
subroutine 38 41 92.6
pod 1 3 33.3
total 243 286 84.9


line stmt bran cond sub pod time code
1             package Config::Structured;
2             $Config::Structured::VERSION = '2.002';
3             # ABSTRACT: Provides generalized and structured configuration value access
4              
5 10     10   640344 use 5.022;
  10         119  
6              
7 10     10   4941 use Moose;
  10         4280438  
  10         64  
8 10     10   67882 use Moose::Util::TypeConstraints;
  10         22  
  10         91  
9 10     10   26711 use Mojo::DynamicMethods -dispatch;
  10         1729467  
  10         88  
10              
11 10     10   5314 use Syntax::Keyword::Junction;
  10         71062  
  10         75  
12 10     10   787 use Carp;
  10         22  
  10         494  
13 10     10   4600 use IO::All;
  10         101785  
  10         72  
14 10     10   803 use List::Util qw(reduce);
  10         24  
  10         589  
15 10     10   5361 use Data::DPath qw(dpath);
  10         663524  
  10         77  
16 10     10   6902 use Text::Glob qw(match_glob);
  10         7250  
  10         506  
17              
18 10     10   4947 use Readonly;
  10         36174  
  10         480  
19              
20 10     10   4793 use Config::Structured::Deserializer;
  10         108  
  10         379  
21              
22 10     10   6258 use Data::Printer;
  10         269198  
  10         71  
23              
24 10     10   1454 use experimental qw(signatures lexical_subs);
  10         21  
  10         82  
25              
26             # Symbol constants
27             Readonly::Scalar my $EMPTY => q{};
28             Readonly::Scalar my $SLASH => q{/};
29              
30             # Token key constants
31             Readonly::Scalar my $DEF_ISA => q{isa};
32             Readonly::Scalar my $DEF_DEFAULT => q{default};
33             Readonly::Scalar my $CFG_SOURCE => q{source};
34             Readonly::Scalar my $CFG_REF => q{ref};
35              
36             # Token value constants
37             Readonly::Scalar my $CONF_FROM_FILE => q(file);
38             Readonly::Scalar my $CONF_FROM_ENV => q(env);
39              
40             # Method names that are needed by Config::Structured and cannot be overridden by config node names
41             Readonly::Array my @RESERVED =>
42             qw(get meta BUILDCARGS BUILD BUILD_DYNAMIC _config _structure _hooks _base _add_helper __register_default __register_as);
43              
44             #
45             # The configuration structure (e.g., $app.conf.def contents)
46             #
47             has '_structure_v' => (
48             is => 'ro',
49             isa => 'Str|HashRef',
50             init_arg => 'structure',
51             required => 1,
52             );
53              
54             has '_structure' => (
55             is => 'ro',
56             isa => 'HashRef',
57             init_arg => undef,
58             lazy => 1,
59             default => sub {Config::Structured::Deserializer->decode(shift->_structure_v)}
60             );
61              
62             has '_hooks' => (
63             is => 'ro',
64             isa => 'HashRef[HashRef[CodeRef]]',
65             init_arg => 'hooks',
66             required => 0,
67             default => sub {{}},
68             );
69              
70             #
71             # The file-based configuration (e.g., $app.conf contents)
72             #
73             has '_config_v' => (
74             is => 'ro',
75             isa => 'Str|HashRef',
76             init_arg => 'config',
77             required => 1,
78             );
79              
80             has '_config' => (
81             is => 'ro',
82             isa => 'HashRef',
83             init_arg => undef,
84             lazy => 1,
85             default => sub {Config::Structured::Deserializer->decode(shift->_config_v)}
86             );
87              
88             #
89             # This instance's base path (e.g., /db)
90             # Recursively constucted through re-instantiation of non-leaf config nodes
91             #
92             has '_base' => (
93             is => 'ro',
94             isa => 'Str',
95             default => $SLASH,
96             );
97              
98             #
99             # Convenience method for adding dynamic methods to an object
100             #
101             sub _add_helper {
102 49     49   177 Mojo::DynamicMethods::register __PACKAGE__, @_;
103             }
104              
105             around BUILDARGS => sub ($orig, $class, @args) {
106             my %args = ref($args[0]) eq 'HASH' ? %{$args[0]} : @args;
107             delete($args{hooks}) unless (defined($args{hooks}));
108             return $class->$orig(%args);
109             };
110              
111             #
112             # Dynamically create methods at instantiation time, corresponding to configuration structure's dpaths
113             # Use lexical subs and closures to avoid polluting namespace unnecessarily (preserving it for config nodes)
114             #
115 35     35 0 65707 sub BUILD ($self, $args) {
  35         65  
  35         57  
  35         57  
116             # lexical subroutines
117              
118 3     3   5 state sub pkg_prefix($msg) {
  3         1451  
  3         5  
119 3         73 '[' . __PACKAGE__ . "] $msg";
120             }
121              
122 35     35   50 state sub is_hashref($node) {
  35         61  
  35         119  
123 35         119 return ref($node) eq 'HASH';
124             }
125              
126 92     92   118 state sub is_leaf_node($node) {
  92         122  
  92         132  
127 92         305 exists($node->{isa});
128             }
129              
130 39     39   44 state sub is_ref_node ($def, $node) {
  39         56  
  39         52  
  39         58  
131 39 100       105 return 0 if ($def->{isa} =~ /hash/i);
132 38 100       142 return 0 unless (ref($node) eq 'HASH');
133 2   33     15 return (exists($node->{$CFG_SOURCE}) && exists($node->{$CFG_REF}));
134             }
135              
136 2     2   3 state sub ref_content_value($node) {
  2         4  
  2         2  
137 2         4 my $source = $node->{$CFG_SOURCE};
138 2         4 my $ref = $node->{$CFG_REF};
139 2 100       9 if ($source eq $CONF_FROM_FILE) {
    50          
140 1 50       47 if (-f -r $ref) {
141 1         6 chomp(my $contents = io->file($ref)->slurp);
142 1         7634 return $contents;
143             }
144             } elsif ($source eq $CONF_FROM_ENV) {
145 1 50       6 return $ENV{$ref} if (exists($ENV{$ref}));
146             }
147 0         0 return;
148             }
149              
150 40     40   51 state sub node_value ($el, $node) {
  40         4490  
  40         64  
  40         63  
151 40 100       99 if (defined($node)) {
152 39 100       116 my $v = is_ref_node($el, $node) ? ref_content_value($node) : $node;
153 39 50       198 return $v if (defined($v));
154             }
155 1         5 return $el->{$DEF_DEFAULT};
156             }
157              
158             state sub concat_path {
159 49 50   49   444 reduce {local $/ = $SLASH; chomp($a); join(($b =~ m|^$SLASH|) ? $EMPTY : $SLASH, $a, $b)} @_;
  49         200  
  49         104  
  49         477  
160             }
161              
162 36     36   49 state sub typecheck ($isa, $value) {
  36         48  
  36         67  
  36         83  
163 36         135 my $tc = Moose::Util::TypeConstraints::find_or_parse_type_constraint($isa);
164 36 100       7576 if (defined($tc)) {
165 35         96 return $tc->check($value);
166             } else {
167 1         5 carp(pkg_prefix "Invalid typeconstraint '$isa'. Skipping typecheck");
168 1         729 return 1;
169             }
170             }
171              
172             # Closures
173 40     40   52 my $get_node_value = sub ($el, $path) {
  40         59  
  40         85  
  40         64  
174 40         119 return node_value($el, dpath($path)->matchr($self->_config)->[0]);
175 35         181 };
176              
177 65     65   83 my $get_hooks = sub($path) {
  65         95  
  65         91  
178 65 100       95 return map {$self->_hooks->{$_}} grep {match_glob($_, $path) ? $_ : ()} keys(%{$self->_hooks});
  21         2499  
  40         1761  
  65         1746  
179 35         119 };
180              
181 36     36   47 my $make_leaf_generator = sub ($el, $path) {
  36         56  
  36         111  
  36         62  
182 36         62 my $isa = $el->{isa};
183 36         136 my $v = $get_node_value->($el, $path);
184              
185 36 50       171 if (defined($v)) {
186 36 100       79 if (typecheck($isa, $v)) {
187 35         2349 my @hooks = grep {defined} map {$_->{on_access}} $get_hooks->($path);
  14         32  
  14         26  
188             return sub {
189             # access hook
190 22         62 foreach (@hooks) {$_->($path, $v)}
  6         73  
191 22         434 return $v;
192             }
193 35         684 } else {
194 1         59 carp(pkg_prefix "Value '" . np($v) . "' does not conform to type '$isa' for node $path");
195             }
196             }
197             return sub {
198 0         0 return;
199             }
200 35         137 };
  1         503  
201              
202 13     13   77 my $make_branch_generator = sub($path) {
  13         21  
  13         24  
203             return sub {
204 17         527 return __PACKAGE__->new(
205             structure => $self->_structure,
206             config => $self->_config,
207             hooks => $self->_hooks,
208             _base => $path
209             );
210             }
211 35         109 };
  13         83  
212              
213 35         1037 foreach my $el (dpath($self->_base)->match($self->_structure)) {
214 35 50       3375 if (is_hashref($el)) {
215 35         59 foreach my $def (keys(%{$el})) {
  35         104  
216 50 100 50     225 carp(pkg_prefix "Reserved word '$def' used as config node name: ignored") and next if ($def eq any(@RESERVED));
217 49         5117 $self->meta->remove_method($def)
218             ; # if the config node refers to a method already defined on our instance, remove that method
219 49         4105 my $path = concat_path($self->_base, $def); # construct the new directive path by concatenating with our base
220              
221             # Detect whether the resulting node is a branch or leaf node (leaf nodes are required to have an "isa" attribute)
222             # if it's a branch node, return a new Config instance with a new base location, for method chaining (e.g., config->db->pass)
223             $self->_add_helper(
224 49 100       257 $def => (is_leaf_node($el->{$def}) ? $make_leaf_generator->($el->{$def}, $path) : $make_branch_generator->($path)));
225             }
226             }
227             }
228              
229             # Run on_load hooks immediately from root node only since we can't assume that non-root nodes will be created immediately
230 35 100       1950 if ($self->_base eq $SLASH) {
231 31     31   44 sub ($path, $node) {
  31         57  
  31         50  
  31         51  
232 31         49 foreach (keys(%{$node})) {
  31         95  
233 43 100       316 my $p = join($path eq $SLASH ? $EMPTY : $SLASH, $path, $_); #don't duplicate initial slash in path
234 43         79 my $n = $node->{$_};
235 43 100       89 if (is_leaf_node($n)) {
236 30         86 my @hooks = grep {defined} map {$_->{on_load}} $get_hooks->($p);
  7         21  
  7         21  
237 30 100       896 if (@hooks) {
238 4         11 my $v = $get_node_value->($n, $p); #put off resolving the node value until we know we need it
239 4         15 foreach (@hooks) {$_->($p, $v)}
  4         11  
240             }
241             } else {
242 13         61 __SUB__->($p, $n); #recurse on the new branch node
243             }
244             }
245             }
246 18         455 ->($self->_base, $self->_structure); #initially call on root of structure
247             }
248             }
249              
250             #
251             # Handle dynamic method dispatch
252             #
253             sub BUILD_DYNAMIC {
254 25     25 0 718 my ($class, $method, $dyn_methods) = @_;
255             return sub {
256 39     39   4252 my ($self, @args) = @_;
        12      
        12      
        12      
        39      
        42      
        9      
        7      
257 39         128 my $dynamic = $dyn_methods->{$self}{$method};
258 39 50       144 return $self->$dynamic(@args) if ($dynamic);
259 0           my $package = ref $self;
260 0           croak qq{Can't locate object method "$method" via package "$package"};
261             }
262 25         226 }
263              
264             #
265             # Saved Named/Default Config instances
266             #
267             our $saved_instances = {
268             default => undef,
269             named => {}
270             };
271              
272             #
273             # Instance method
274             # Saves the current instance as the default instance
275             #
276 0     0     sub __register_default($self) {
  0            
  0            
277 0           $saved_instances->{default} = $self;
278 0           return $self;
279             }
280              
281             #
282             # Instance method
283             # Saves the current instance by the specified name
284             # Parameters:
285             # Name (Str), required
286             #
287 0     0     sub __register_as ($self, $name) {
  0            
  0            
  0            
288 0 0         croak 'Registration name is required' unless (defined $name);
289              
290 0           $saved_instances->{named}->{$name} = $self;
291 0           return $self;
292             }
293              
294             #
295             # Class method
296             # Return a previously saved instance. Returns undef if no instances have been saved. Returns the default instance if no name is provided
297             # Parameters:
298             # Name (Str), optional
299             #
300 0     0 1   sub get ($class, $name = undef) {
  0            
  0            
  0            
301 0 0         if (defined $name) {
302 0           return $saved_instances->{named}->{$name};
303             } else {
304 0           return $saved_instances->{default};
305             }
306             }
307              
308             1;
309              
310             __END__
311              
312             =pod
313              
314             =encoding UTF-8
315              
316             =head1 NAME
317              
318             Config::Structured - Provides generalized and structured configuration value access
319              
320             =head1 VERSION
321              
322             version 2.002
323              
324             =head1 SYNOPSIS
325              
326             Basic usage:
327              
328             use Config::Structured;
329              
330             my $conf = Config::Structured->new(
331             structure => { ... },
332             config => { ... }
333             );
334              
335             say $conf->some->nested->value();
336              
337             Hooks exammple showing how to ensure config directories exist prior to first
338             use:
339              
340             my $conf = Config::Structured->new(
341             ...
342             hooks => {
343             '/paths/*' => {
344             on_load => sub($node,$value) {
345             Mojo::File->new($value)->make_path
346             }
347             }
348             }
349             )
350              
351             =head1 DESCRIPTION
352              
353             L<Config::Structured> provides a structured method of accessing configuration values
354              
355             This is predicated on the use of a configuration C<structure> (required), This structure
356             provides a hierarchical structure of configuration branches and leaves. Each branch becomes
357             a L<Config::Structured> method which returns a new L<Config::Structured> instance rooted at
358             that node, while each leaf becomes a method which returns the configuration value.
359              
360             The configuration value is normally provided in the C<config> hash. However, a C<config> node
361             for a non-Hash value can be a hash containing the "source" and "ref" keys. This permits sourcing
362             the config value from a file (when source="file") whose filesystem location is given in the "ref"
363             value, or an environment variable (when source="env") whose name is given in the "ref" value.
364              
365             I<Structure Leaf Nodes> are required to include an "isa" key, whose value is a type
366             (see L<Moose::Util::TypeConstraints>). If typechecking is not required, use isa => 'Any'.
367             There are a few other keys that L<Config::Structured> respects in a leaf node:
368              
369             =over
370              
371             =item C<default>
372              
373             This key's value is the default configuration value if a data source or value is not provided by
374             the configuation.
375              
376             =item C<description>
377              
378             =item C<notes>
379              
380             A human-readable description and implementation notes, respectively, of the configuration node.
381             L<Config::Structured> does not do anything with these values at present, but they provides inline
382             documentation of configuration directivess within the structure (particularly useful in the common
383             case where the structure is read from a file)
384              
385             =back
386              
387             Besides C<structure> and C<config>, L<Config::Structured> also accepts a C<hooks> argument at
388             initialization time. This argument must be a HashRef whose keys are patterns matching config
389             node paths, and whose values are HashRefs containing C<on_load> and/or C<on_access> keys. These
390             in turn point to CodeRefs which are run when the config value is initially loaded, or every time
391             it is accessed, respectively.
392              
393             =head1 METHODS
394              
395             =head2 get($name?)
396              
397             Class method.
398              
399             Returns a registered L<Config::Structured> instance. If C<$name> is not provided, returns the default instance.
400             Instances can be registered with C<__register_default> or C<__register_as>. This mechanism is used to provide
401             global access to a configuration, even from code contexts that otherwise cannot share data.
402              
403             =head2 __register_default()
404              
405             Call on a L<Config::Structured> instance to set the instance as the default.
406              
407             =head2 __register_as($name)
408              
409             Call on a L<Config::Structured> instance to register the instance as the provided name.
410              
411             =head1 AUTHOR
412              
413             Mark Tyrrell <mtyrrell@concertpharma.com>
414              
415             =head1 COPYRIGHT AND LICENSE
416              
417             This software is copyright (c) 2019 by Concert Pharmaceuticals, Inc.
418              
419             This is free software; you can redistribute it and/or modify it under
420             the same terms as the Perl 5 programming language system itself.
421              
422             =cut