File Coverage

blib/lib/MooseX/ConfigCascade.pm
Criterion Covered Total %
statement 6 6 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod 0 1 0.0
total 9 10 90.0


line stmt bran cond sub pod time code
1             package MooseX::ConfigCascade;
2              
3             our $VERSION = '0.01';
4              
5 6     6   3831863 use Moose::Role;
  6         44031  
  6         35  
6 6     6   48928 use MooseX::ConfigCascade::Util;
  6         32  
  6         1249  
7              
8             has cascade_util => (is => 'ro', lazy => 1, isa => 'MooseX::ConfigCascade::Util', default => sub{
9             MooseX::ConfigCascade::Util->new(
10             _to_set => $_[0],
11             _role_name => __PACKAGE__
12             );
13             });
14              
15              
16       275 0   sub BUILD{}
17             after BUILD => sub{
18             my ($self,$args) = @_;
19             my $util = $self->cascade_util;
20             foreach my $k (keys %$args){ $util->_args->{$k} = 1 }
21             $util->_parse_atts;
22             };
23              
24              
25             1;
26             __END__
27             =head1 NAME
28              
29             MooseX::ConfigCascade - Set initial accessor values of your whole Moose-based project from a single config file
30              
31             =head1 SYNOPSIS
32              
33             # /my_conf.json:
34              
35             "My::Bottle": {
36             "label": {
37             "logo": {
38             "company_name": "Bottle Company Name",
39             "slogan": "Bottle Slogan"
40             }
41             }
42             },
43              
44             "My::Label": {
45             "logo": {
46             "company_name": "Label Company Nmae",
47             "slogan": "Label Slogan"
48             }
49             },
50              
51              
52             "My::Logo": {
53              
54             "company_name": "Logo Company Name",
55             "slogan": "Logo Slogan",
56            
57             }
58              
59             # Packages:
60              
61             package Bottle;
62              
63             use Moose;
64             with 'MooseX::ConfigCascade'; # MooseX::ConfigCascade is a Moose role
65              
66             has label => (is => 'rw', isa => 'My::Label', default => sub{
67             My::Label->new;
68             });
69            
70              
71             package Label;
72              
73             use Moose;
74             with 'MooseX::ConfigCascade';
75              
76             has logo => (is => 'rw', isa => 'My::Logo', default => sub {
77             My::Logo->new;
78             });
79            
80              
81             package Logo;
82              
83             use Moose;
84             with 'MooseX::ConfigCascade';
85              
86             has company_name => (is => 'rw', isa => 'Str', default => 'Default Company Name');
87             has slogan => (is => 'ro', isa => 'Str', default => 'Default Slogan');
88              
89              
90             # and in your script...
91              
92              
93             my $logo = My::Logo->new;
94             say $logo->company_name; # prints 'Default Company Name' because the path
95             # to the config has not been set yet
96              
97             use MooseX::ConfigCascade::Util; # use this package to set path to config
98             MooseX::ConfigCascade::Util->path(
99             '/my_conf.json'
100             );
101              
102              
103             $logo = My::Logo->new;
104             say $logo->company_name; # Now this prints 'Logo Company Name'
105              
106             my $label = My::Label->new;
107             say $label->logo->company_name; # 'Label Company Name'
108             say $label->logo->slogan; # 'Label Slogan'
109              
110             my $bottle = My::Bottle->new;
111             say $bottle->label->logo->company_name; # 'Bottle Company Name'
112             say $bottle->label->logo->slogan; # 'Bottle Slogan'
113              
114              
115             =head1 DESCRIPTION
116              
117             In my opinion getting values from some kind of centralised config to attributes in nested objects is problematic. There are several modules available which load config into accessors, but in one way or another these all involve telling each specific object about the config, and changing the code of each package to accommodate that config.
118              
119             MooseX::ConfigCascade attempts to solve not only the issue of loading from a centralised config file, but also delivery of config values to objects within objects, nested to arbitrary depth, without the need for any added code within the modules. Specify a config file once (perhaps at the top of your script), and from then on any object you create can enjoy having its attributes loaded directly from the config.
120              
121             If you don't specify a config file, the object will just initialise with the default values it was going to take otherwise. Nor is there any requirement for how many attributes you choose to put in your config. Load lots of them, or just one. Any that don't get a definition in your config file will load package default values as before.
122              
123             MooseX::ConfigCascade also allows CSS style cascading of config declarations. In the example in the synopsis, the attributes 'company_name' and 'slogan' (belonging to the My::Logo package) were assigned values 3 times in the configuration file. The most specific definition in the config that matches the object structure wins. So in the example, if My::Logo is initialised on its own, then it will get the value provided in the 'My::Logo' directive in the config file. If the My::Logo object is initialised in the accessor 'logo' in My::Label however, the more specific My::Label definition wins.
124              
125             This module was born out of frustration with the paradox of trying to make sure config remains centralised while also keeping objects independent of one another. A tempting and easy way to deal with config is simply to pass a reference around to all objects that need it in your project. This works great, but it has the side effect of effectively tying all your objects to a specific heirarchy.
126              
127             If you pull out one of your objects to use somewhere else, it's still expecting that same config reference. If you coded with portability in mind originally, then you might have added code to say 'use the config if its available, but use defaults if not'. However there's also the issue that your config data structure still needs to be in the same format - and that format may not be appropriate any more.
128              
129             MooseX::ConfigCascade addresses this last point because it always expects a file format that matches the package structure of your project.
130              
131              
132             =head2 CAVEATS
133              
134             1. It's not quite true that you don't need any additional code in your modules. But you only need
135              
136             with 'MooseX::ConfigCascade';
137              
138             at the top of each module you want to take part. MooseX::ConfigCascade will not traverse into modules which don't adopt this role (as much a safety feature as anything else).
139              
140             1. MooseX::ConfigCascade will populate 'ro' and 'rw' accessors of types 'HashRef', 'ArrayRef', 'Bool' or 'Str', and any subtypes of these types (including 'Num' and 'Int' which are subtypes of 'Str' in Moose). It won't populate anything else.
141              
142             2. The magic is performed at object instantiation only.
143              
144             3. Any affected attributes defined as 'lazy' will have their laziness thrown out of the window - ie they will get the values in the config straight away whatever. (This should be irrelevant. Attributes are generally 'lazy' when they depend on other attributes, which is not the case if the value comes from the config file)
145              
146             3. MooseX::ConfigCascade will traverse objects within objects provided they follow the one object per accessor rule. In other words it will not traverse collections of objects, such as 'HashRef[My::Object]' or 'ArrayRef[My::Object]'. (I looked into this, but decided it would be complex and would bloat the module).
147              
148             4. MooseX::ConfigCascade is compatible with inheritance and roles - ie it can populate objects that are comprised of locally defined attributes and attributes inherited from parent classes, or attributes absorbed from adopted roles. However, it will NOT see or populate class attributes using L<MooseX::ClassAttribute>.
149              
150             5. Whilst some effort has gone into testing this module, it is presented at an early stage of development and without much real-world testing. It has not been tested at all with most MooseX:: extensions and there is the possibility of conflict. If you discover problems please let me know.
151              
152              
153             =head1 ATTRIBUTE LOADING
154              
155             =head2 File Format
156              
157             Off the shelf MooseX::ConfigCascade supports text files containing YAML or JSON. If the file starts with a dash (-) it is assumed to be YAML and will be read in using the L<YAML> CPAN module. If it begins with an opening curly bracket ({) then it is assumed to be JSON and will be read in using the L<JSON> CPAN module. By default, if the file starts any other way an error is returned. (I decided against including XML as standard as it would have required too many options.)
158              
159             However MooseX::ConfigCascade can potentially support any file format, but you must create and pass in your own parsing subroutine. See the ->parser method description in L<MooseX::ConfigCascade::Util>.
160              
161             =head2 Basic Attribute Assignment
162              
163             The file needs to be organised so that when the parser pulls the data into a hashref, the keys of the hash are the names of packages. When a new object is created, MooseX::ConfigCascade looks for the name of the package being created in the config file. If the package matches, it looks at the value corresponding to the package name key, where again it expects to find a hashref, this time containing ( attribute name, value) pairs. ie the overall config hashref now looks like:
164              
165             {
166             'First::Package' => {
167             fp_attribute1 => 'fp value1',
168             fp_attribute2 => 'fp value2'
169             },
170              
171             'Second::Package' => {
172             sp_attribute1 => 'sp value1',
173             sp_attribute2 => 'sp value2'
174             }
175              
176             }
177              
178             For reference, C<First::Package> might look something like this:
179              
180             package First::Package;
181              
182             use Moose;
183             with 'MooseX::ConfigCascade';
184              
185             has fp_attribute1 => ( # this will get assigned
186             is => 'rw', # 'fp value1' from the config
187             isa => 'Str'
188             );
189              
190             has fp_attribute2 => ( # this will get assigned
191             is => 'ro', # 'fp value2'
192             isa => 'Str', # It doesn't matter whether
193             default => 'some default', # it is 'ro' or 'rw', or
194             lazy => 1 # if it is lazy
195             );
196              
197             has some_other attribute => ( # our package can have other
198             is => 'rw', # attributes not mentioned in the
199             isa => 'Str', # config - these will not be
200             default => 'another default' # affected
201             );
202              
203              
204             The config structure above will work provided those four attributes are all of type 'Str'. But lets say we change First::Package so fp_attribute1 is a HashRef. From now we will assume you understand that packages can have attributes not specified in the config, and leave these out. For simplicity we'll also just focus on the one package. So C<First::Package> looks something like this:
205              
206             package First::Package;
207              
208             use Moose;
209             with 'MooseX::ConfigCascade';
210              
211             has fp_attribute1 => ( # now of type 'HashRef'
212             is => 'rw',
213             isa => 'HashRef'
214             );
215              
216             has fp_attribute2 => (
217             is => 'ro',
218             isa => 'Str',
219             default => 'some default',
220             lazy => 1
221             );
222              
223             # ... other attributes ...
224              
225              
226             and our config hashref should look something like:
227              
228             {
229             'First::Package' => {
230              
231             fp_attribute1 => {
232             hash_key1 => 'hash_value1',
233             hash_key2 => 'hash_value2',
234             # ....
235             },
236              
237             fp_attribute2 => 'value2'
238             },
239              
240             # ... rest of the config ...
241              
242             }
243              
244             If we had made fp_attribute1 an arrayref instead, then the config would need to look like:
245              
246             {
247             'First::Package' => {
248              
249             fp_attribute1 => [
250             'array_value1',
251             'array_value2',
252             # ....
253             },
254              
255             fp_attribute2 => 'value2'
256             },
257              
258             # ... rest of the config ...
259              
260             }
261              
262              
263             =head2 Object Traversal
264              
265             Lets say we have the following 2 packages:
266              
267             package Box::Package;
268              
269             use Moose;
270             with 'MooseX::ConfigCascade';
271              
272             has contents => (is => 'ro', isa => 'Contents::Package', default => sub{
273             Contents::Package->new;
274             });
275              
276             has colors => (is => 'rw', isa => 'ArrayRef');
277              
278             # ... other attributes ...
279              
280              
281             package Contents::Package;
282              
283             use Moose;
284             with 'MooseX::ConfigCascade';
285              
286             has stuff => (is => 'ro', isa => 'Str');
287              
288              
289             So effectively an instance of C<Box::Package> is a compound object containing C<Contents::Package> in the accessor C<contents>. We can populate both C<Box::Package> and C<Contents::Package> using the following config structure:
290              
291             {
292             'Box::Package' => {
293              
294             contents => {
295            
296             stuff => 'some stuff'
297              
298             },
299              
300             colors => [ 'red', 'blue', 'green' ]
301              
302             }
303              
304             }
305              
306             (So The YAML config file would look like this:
307              
308             ---
309             "Box::Package":
310              
311             contents:
312             stuff: some stuff
313              
314             colors:
315             - red
316             - blue
317             - green
318            
319              
320             or if you wanted to use JSON:
321              
322             {
323             "Box::Package": {
324              
325             "contents": {
326              
327             "stuff": "some stuff"
328              
329             },
330              
331             "colors": [
332              
333             "red",
334             "blue",
335             "green"
336              
337             ]
338              
339             }
340             }
341              
342             ). When you create a new C<Box::Package> object, MooseX::ConfigCascade looks for the attribute named C<contents>. It sees that C<contents> contains an object, and traverses into the object. It then attempts to assign the hash
343              
344             {
345              
346             stuff => 'some stuff'
347              
348             }
349              
350             to the attributes in the object. In this case it will find the attribute C<stuff> because it is a valid attribute in C<Contents::Package>, and this attribute will get assigned C<some stuff>.
351              
352             Some things to note:
353              
354             1. If we had created a new C<Box::Contents> on its own, it would not get assigned C<some stuff> because our config does not have a declaration which looks like:
355              
356             'Contents::Package' => {
357             stuff => 'some stuff'
358             }
359              
360             (we could, of course, add one...)
361              
362             2. the C<contents> attribute DOES need to be provided an initial value for this to work, either using C<default> or C<builder>. Obviously it's not possible to traverse an object that doesn't exist - and since the traversal happens at creation time, you need make sure the objects are there from the beginning.
363              
364             3. If the attribute is specified as C<lazy> it will be pulled out of its lazy state and evaluated
365              
366             4. You may have noticed that assignment to attributes of type 'HashRef' is done using a hashref, and assignment to attributes in nested objects also uses a hashref. It is true that (in the current release) there is no distinction in the config file between attributes of type 'HashRef' and attributes containing objects. You could swap out your attribute containing an object for an attribute containing a HashRef and you would not get an error. It's up to you to make sure what you are delivering makes sense.
367              
368             5. Further to point 4, nor does the config file distinguish the type of object contained in the attribute. Change C<Contents::Package> to C<DifferentContents::Package> and the attribute assignment will still work (provided C<DifferentContents::Package> also has a C<stuff> attribute of type 'Str'.)
369              
370             6. If we had defined an attribute as a collection of objects, either by using C<HashRef[Some::Package]> or C<ArrayRef[Some::Package]> - for example:
371              
372             has obj_collection => (is => 'rw, isa => 'HashRef[My::Package]', default => sub{
373             {
374             object1 => My::Package->new( %obj_1_params )
375             object2 => My::Package->new( %obj_2_params )
376             }
377             });
378              
379             then there is *no way* to assign values to the objects in the collection from the config. MooseX::ConfigCascade does NOT provide this functionality.
380              
381              
382             =head2 Cascading
383              
384             Suppose we add that declaration mentioned in point 1 above, so our config hashref now looks like:
385              
386             {
387             'Box::Package' => {
388              
389             contents => {
390            
391             stuff => 'some stuff (from box)'
392              
393             },
394              
395             colors => [ 'red', 'blue', 'green' ]
396              
397             },
398              
399             'Contents::Package' => {
400              
401             stuff => 'some stuff (from contents)'
402              
403             }
404              
405             }
406              
407             but note that we also added C<(from box)> and C<(from contents)> to distinguish where the values are going to get loaded from.
408              
409             Now if we create a C<Box::Package> object and examine the C<stuff> attribute inside C<contents> we will find:
410              
411             # case 1:
412              
413             my $box = Box::Package->new;
414             print $box->contents->stuff; # prints 'some stuff (from box)'
415              
416              
417             # case 2:
418              
419             my $contents = Contents::Package->new;
420             print $contents->stuff; # prints 'some stuff (from contents)'
421              
422             What happens in case 1 is that the C<Contents::Package> object which is created in contents first gets assigned the accessor default value (if it exists). Then it gets overwritten by C<some stuff (from contents)> which comes from the C<Contents::Package> declaration in the config. Then finally it gets overwritten by the more specific value in the C<Box::Package> config declaration - finally ending up as C<some stuff (from box)>.
423              
424             It remains to be seen how useful this feature will turn out to be. Obviously there is a performance penalty in doing this - so it's probably not a good idea to use it extensively. It should also be used very carefully since having multiple defaults for a particular value is obviously potentially confusing. However, here is an example of how multiple defaults can be used in a way that makes a lot of sense:
425              
426             # /my_config.yaml:
427             "Pets::BigDog":
428              
429             size:
430             height: 50
431             weight: 80
432              
433             "Pets::SmallDog"
434              
435             size:
436             height: 10
437             weight: 25
438              
439            
440             package Pets::Dog;
441              
442             use Moose;
443             with 'MooseX::ConfigCascade';
444              
445             has size => (is => 'ro', isa => 'Pets::Size');
446              
447             # ...
448              
449              
450             package Pets::BigDog;
451              
452             use Moose;
453             extends 'Pets::Dog';
454              
455             # ...
456              
457              
458             package Pets::SmallDog;
459              
460             use Moose;
461             extends 'Pets::Dog';
462              
463             # ...
464              
465              
466             package Pets::Size;
467              
468             use Moose;
469             with 'MooseX::ConfigCascade';
470              
471             has height => (is => 'ro', isa => 'Int');
472             has weight => (is => 'ro', isa => 'Int');
473              
474              
475             C<Pets::BigDog> and C<Pets::SmallDog> both inherit the C<size> attribute from C<Pets::Dog> - but the attributes in the C<Pets::Size> object contained in the C<size> attribute get assigned different defaults.
476              
477              
478             =head2 Loading Order
479              
480             Loading of attributes happens after C<BUILD>. This means if you use C<BUILD> to assign values to attributes, MooseX::ConfigCascade may overwrite those values (depending if there are values for those attributes specified in the config). If you want to be sure of overwriting the config values, then you could do this using
481              
482             after 'BUILD' => sub {
483             # overwrite the attributes here
484             };
485              
486             (but perhaps you shouldn't be assigning config values to attributes in the first place if you are then going to want to overwrite them?)
487              
488             You can also make sure objects get individual values by specifying them in the objects constructor in the normal way:
489              
490             my $widget = Widget->new( my_accessor => 'this value will win' );
491              
492              
493              
494             =head1 METHODS
495              
496             Remember not to 'use MooseX::ConfigCascade'. It's a role, so you should state:
497              
498             with 'MooseX::ConfigCascade';
499              
500             When you do this, a single new attribute is added to your class:
501              
502             =head2 cascade_util
503              
504             This is a L<MooseX::ConfigCascade::Util> object, which has 3 utility methods. So once you added the MooseX::ConfigCascade role to your package, you can do:
505              
506             my $object = My::Package->new;
507              
508             $object->cascade_util->conf; # access the config hash directly
509             $object->cascade_util->path; # the path to the config file (if any)
510             $object->cascade_util->parser; # the code ref to the subroutine which parses your config file
511              
512             Note C<conf>, C<path> and C<parser> are all B<class attributes> of MooseX::ConfigCascade::Util. That means it is intended that you generally set them by calling the class directly:
513              
514             MooseX::ConfigCascade::Util->path( '/path/to/config.yaml' );
515              
516             # etc ...
517              
518             so you may not ever need to use C<cascade_util> at all. However, you may find it useful that you can access the full config from anywhere in your project:
519              
520             $whatever_object->cascade_util->conf;
521              
522             See the documentation for MooseX::ConfigCascade::Util for information about these methods.
523              
524              
525             =head1 SEE ALSO
526              
527             L<MooseX::ConfigCascade::Util>
528             L<Moose>
529             L<MooseX::ClassAttribute>
530              
531             =head1 AUTHOR
532              
533             Tom Gracey E<lt>tomgracey@gmail.comE<gt>
534              
535             =head1 COPYRIGHT AND LICENSE
536              
537             Copyright (C) 2017 by Tom Gracey
538              
539             This library is free software; you can redistribute it and/or modify
540             it under the same terms as Perl itself.
541              
542             =cut