NAME
MooseX::ConfigCascade - Set initial accessor values of your whole Moose-based project from a single config file
SYNOPSIS
# /my_conf.json:
"My::Bottle": {
"label": {
"logo": {
"company_name": "Bottle Company Name",
"slogan": "Bottle Slogan"
}
}
},
"My::Label": {
"logo": {
"company_name": "Label Company Nmae",
"slogan": "Label Slogan"
}
},
"My::Logo": {
"company_name": "Logo Company Name",
"slogan": "Logo Slogan",
}
# Packages:
package Bottle;
use Moose;
with 'MooseX::ConfigCascade'; # MooseX::ConfigCascade is a Moose role
has label => (is => 'rw', isa => 'My::Label', default => sub{
My::Label->new;
});
package Label;
use Moose;
with 'MooseX::ConfigCascade';
has logo => (is => 'rw', isa => 'My::Logo', default => sub {
My::Logo->new;
});
package Logo;
use Moose;
with 'MooseX::ConfigCascade';
has company_name => (is => 'rw', isa => 'Str', default => 'Default Company Name');
has slogan => (is => 'ro', isa => 'Str', default => 'Default Slogan');
# and in your script...
my $logo = My::Logo->new;
say $logo->company_name; # prints 'Default Company Name' because the path
# to the config has not been set yet
use MooseX::ConfigCascade::Util; # use this package to set path to config
MooseX::ConfigCascade::Util->path(
'/my_conf.json'
);
$logo = My::Logo->new;
say $logo->company_name; # Now this prints 'Logo Company Name'
my $label = My::Label->new;
say $label->logo->company_name; # 'Label Company Name'
say $label->logo->slogan; # 'Label Slogan'
my $bottle = My::Bottle->new;
say $bottle->label->logo->company_name; # 'Bottle Company Name'
say $bottle->label->logo->slogan; # 'Bottle Slogan'
DESCRIPTION
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.
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.
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.
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.
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.
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.
MooseX::ConfigCascade addresses this last point because it always expects a file format that matches the package structure of your project.
CAVEATS
1. It's not quite true that you don't need any additional code in your modules. But you only need
with 'MooseX::ConfigCascade';
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).
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.
2. The magic is performed at object instantiation only.
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)
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).
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 MooseX::ClassAttribute.
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.
ATTRIBUTE LOADING
File Format
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 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 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.)
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 MooseX::ConfigCascade::Util.
Basic Attribute Assignment
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 might look something like this:
{
'First::Package' => {
fp_attribute1 => 'fp value1',
fp_attribute2 => 'fp value2'
},
'Second::Package' => {
sp_attribute1 => 'sp value1',
sp_attribute2 => 'sp value2'
}
}
First::Package
might look something like this:
package First::Package;
use Moose;
with 'MooseX::ConfigCascade';
has fp_attribute1 => ( # this will get assigned
is => 'rw', # 'fp value1' from the config
isa => 'Str'
);
has fp_attribute2 => ( # this will get assigned
is => 'ro', # 'fp value2'
isa => 'Str', # It doesn't matter whether
default => 'some default', # it is 'ro' or 'rw', or
lazy => 1 # if it is lazy
);
has some_other attribute => ( # our package can have other
is => 'rw', # attributes not mentioned in the
isa => 'Str', # config - these will not be
default => 'another default' # affected
);
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 now First::Package
looks something like this:
package First::Package;
use Moose;
with 'MooseX::ConfigCascade';
has fp_attribute1 => ( # now of type 'HashRef'
is => 'rw',
isa => 'HashRef'
);
has fp_attribute2 => (
is => 'ro',
isa => 'Str',
default => 'some default',
lazy => 1
);
# ... other attributes ...
and our config hashref should look something like:
{
'First::Package' => {
fp_attribute1 => {
hash_key1 => 'hash_value1',
hash_key2 => 'hash_value2',
# ....
},
fp_attribute2 => 'value2'
},
# ... rest of the config ...
}
If we had made fp_attribute1
an ArrayRef
instead, then the config would need to look like:
{
'First::Package' => {
fp_attribute1 => [
'array_value1',
'array_value2',
# ....
},
fp_attribute2 => 'value2'
},
# ... rest of the config ...
}
Object Traversal
Lets say we have the following 2 packages:
package Box::Package;
use Moose;
with 'MooseX::ConfigCascade';
has contents => (is => 'ro', isa => 'Contents::Package', default => sub{
Contents::Package->new;
});
has colors => (is => 'rw', isa => 'ArrayRef');
# ... other attributes ...
package Contents::Package;
use Moose;
with 'MooseX::ConfigCascade';
has stuff => (is => 'ro', isa => 'Str');
So effectively an instance of Box::Package
is a compound object containing Contents::Package
in the accessor contents
. We can populate both Box::Package
and Contents::Package
using the following config structure:
{
'Box::Package' => {
contents => {
stuff => 'some stuff'
},
colors => [ 'red', 'blue', 'green' ]
}
}
(So The YAML config file would look like this:
---
"Box::Package":
contents:
stuff: some stuff
colors:
- red
- blue
- green
or if you wanted to use JSON:
{
"Box::Package": {
"contents": {
"stuff": "some stuff"
},
"colors": [
"red",
"blue",
"green"
]
}
}
). When you create a new Box::Package
object, MooseX::ConfigCascade looks for the attribute named contents
. It sees that contents
contains an object, and traverses into the object. It then attempts to assign the hash
{
stuff => 'some stuff'
}
to the attributes in the object. In this case it will find the attribute stuff
because it is a valid attribute in Contents::Package
, and this attribute will get assigned some stuff
.
Some things to note:
1. If we had created a new Box::Contents
on its own, it would not get assigned some stuff
because our config does not have a declaration which looks like:
'Contents::Package' => {
stuff => 'some stuff'
}
(we could, of course, add one...)
2. the contents
attribute DOES need to be provided an initial value for this to work, either using default
or 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.
3. If the attribute is specified as lazy
it will be pulled out of its lazy state and evaluated
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.
5. Further to point 4, nor does the config file distinguish the type of object contained in the attribute. Change Contents::Package
to DifferentContents::Package
and the attribute assignment will still work (provided DifferentContents::Package
also has a stuff
attribute of type Str
.)
6. If we had defined an attribute as a collection of objects, either by using HashRef[Some::Package]
or ArrayRef[Some::Package]
- for example:
has obj_collection => (is => 'rw, isa => 'HashRef[My::Package]', default => sub{
{
object1 => My::Package->new( %obj_1_params )
object2 => My::Package->new( %obj_2_params )
}
});
then there is no way to assign values to the objects in the collection from the config. MooseX::ConfigCascade does NOT provide this functionality.
Cascading
Suppose we add that declaration mentioned in point 1 above, so our config hashref now looks like:
{
'Box::Package' => {
contents => {
stuff => 'some stuff (from box)'
},
colors => [ 'red', 'blue', 'green' ]
},
'Contents::Package' => {
stuff => 'some stuff (from contents)'
}
}
but note that I also added (from box)
and (from contents)
to distinguish where the values are going to get loaded from.
Now if we create a Box::Package
object and examine the stuff
attribute inside contents
we will find:
# case 1:
my $box = Box::Package->new;
print $box->contents->stuff; # prints 'some stuff (from box)'
# case 2:
my $contents = Contents::Package->new;
print $contents->stuff; # prints 'some stuff (from contents)'
What happens in case 1 is that the Contents::Package
object which is created in contents first gets assigned the accessor default value (if it exists). Then it gets overwritten by some stuff (from contents)
which comes from the Contents::Package
declaration in the config. Then finally it gets overwritten by the more specific value in the Box::Package
config declaration - finally ending up as some stuff (from box)
.
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:
# /my_config.yaml:
"Pets::BigDog":
size:
height: 50
weight: 80
"Pets::SmallDog"
size:
height: 10
weight: 25
package Pets::Dog;
use Moose;
with 'MooseX::ConfigCascade';
has size => (is => 'ro', isa => 'Pets::Size');
# ...
package Pets::BigDog;
use Moose;
extends 'Pets::Dog';
# ...
package Pets::SmallDog;
use Moose;
extends 'Pets::Dog';
# ...
package Pets::Size;
use Moose;
with 'MooseX::ConfigCascade';
has height => (is => 'ro', isa => 'Int');
has weight => (is => 'ro', isa => 'Int');
Pets::BigDog
and Pets::SmallDog
both inherit the size
attribute from Pets::Dog
- but the attributes in the Pets::Size
object contained in the size
attribute get assigned different defaults.
Loading Order
Loading of attributes happens after BUILD
. This means if you use 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
after 'BUILD' => sub {
# overwrite the attributes here
};
(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?)
You can also make sure objects get individual values by specifying them in the objects constructor in the normal way:
my $widget = Widget->new( my_accessor => 'this value will win' );
METHODS
Remember not to use MooseX::ConfigCascade
. It's a role, so you should state:
with 'MooseX::ConfigCascade';
When you do this, a single new attribute is added to your class:
cascade_util
This is a MooseX::ConfigCascade::Util object, which has 3 utility methods. So once you added the MooseX::ConfigCascade role to your package, you can do:
my $object = My::Package->new;
$object->cascade_util->conf; # access the config hash directly
$object->cascade_util->path; # the path to the config file (if any)
$object->cascade_util->parser; # the code ref to the subroutine which parses your config file
Note conf
, path
and parser
are all class attributes of MooseX::ConfigCascade::Util. That means it is intended that you generally set them by calling the class directly:
MooseX::ConfigCascade::Util->path( '/path/to/config.yaml' );
# etc ...
so you may not ever need to use cascade_util
at all. However, you may find it useful that you can access the full config from anywhere in your project:
$whatever_object->cascade_util->conf;
See the documentation for MooseX::ConfigCascade::Util for information about these methods.
SEE ALSO
MooseX::ConfigCascade::Util Moose MooseX::ClassAttribute
AUTHOR
Tom Gracey <tomgracey@gmail.com>
COPYRIGHT AND LICENSE
Copyright (C) 2017 by Tom Gracey
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.