File Coverage

blib/lib/Config/Structured.pm
Criterion Covered Total %
statement 171 194 88.1
branch 36 48 75.0
condition 2 5 40.0
subroutine 39 42 92.8
pod 1 3 33.3
total 249 292 85.2


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