I have a class Configuration
that reads in environment variables:
class Configuration {
has $.config_string_a;
has $.config_string_b;
has Bool $.config_flag_c;
method new() {
sub assertHasEnv(Str $envVar) {
die "environment variable $envVar must exist" unless %*ENV{$envVar}:exists;
}
assertHasEnv('CONFIG_STRING_A');
assertHasEnv('CONFIG_STRING_B');
assertHasEnv('CONFIG_FLAG_C');
return self.bless(
config_string_a => %*ENV{'CONFIG_STRING_A'},
config_string_b => %*ENV{'CONFIG_STRING_B'},
config_flag_c => Bool(%*ENV{'CONFIG_FLAG_C'}),
);
}
}
my $config = Configuration.new;
say $config.config_string_a;
say $config.config_string_b;
say $config.config_flag_c;
Is there a more concise way to express this? For example, I am repeating the environment variable name in the check and the return value of the constructor.
I could easily see writing another, more generic class that encapsulates the necessary info for a config parameter:
class ConfigurationParameter {
has $.name;
has $.envVarName;
has Bool $.required;
method new (:$name, :$envVarName, :$required = True) {
return self.bless(:$name, :$envVarName, :$required);
}
}
Then rolling these into a List in the Configuration
class. However, I don't know how to refactor the constructor in Configuration
to accommodate this.
So I mentioned this in a comment but I figured it would be good as an actual answer. I figured this would be useful functionality for anyone building a Docker based system so took Jonanthan's example code, added some functionality for exporting Traits Elizabeth showed me and made Trait::Env
Usage is :
The
:required
flag turns ondie
if not found. And it plays nicely with theis default
trait. Attribute names are upper cased and-
is replaced with_
before checking%*ENV
.I have a couple of planned changes, make it throw a named Exception rather than just die and handle Boolean's a bit better. As
%*ENV
is Strings having a Boolean False is a bit of a pain.The most immediate change that comes to mind is to change
new
to be:While
//
is a definedness check rather than an existence one, the only way an environment variable will be undefined is if it isn't set. That gets down to one mention of%*ENV
and also of each environment variable.If there's only a few, then I'd likely stop there, but the next bit of repetition that strikes me is the names of the attributes are just lowercase of the names of the environment variables, so we could eliminate that duplication too, at the cost of a little more complexity:
Now
env
returns aPair
, and|
flattens it in to the argument list as if it's a named argument.Finally, the "power tool" approach is to write a trait like this outside of the class:
And then write the class as:
Traits run at compile time, and can manipulate a declaration in various ways. This trait calculates the name of the environment variable based on the attribute name (attribute names are always like
$!config_string_a
, thus thesubstr
). Theset_build
sets the code that will be run to initialize the attribute when the class is created. That gets passed various things that in our situation aren't important, so we ignore the arguments with|
. Thewith
is just likeif defined
, so this is the same approach as the//
earlier. Finally, theAny ~~ $attr.type
check asks if the parameter is constrained in some way, and if it is, performs a coercion (done by invoking the type with the value).