Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

More concise way to build a configuration class using environment variables?

Tags:

raku

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.

like image 980
wbn Avatar asked Aug 19 '18 05:08

wbn


People also ask

What is the advantage of having configurable environment variables?

The major benefits of using environment variables are: Easy configuration. Better security. Fewer production mistakes.

What is the purpose of environmental variables setup?

Environment variables help programs know what directory to install files in, where to store temporary files, and where to find user profile settings. They help shape the environment that the programs on your computer use to run.

Why do we configure the environmental variables to use Java?

Java does not need any environment variables to be set. However, setting some environment variables makes some things easier. PATH If the jre/bin folder is on the path, you don't have to qualify to run the java command. If the jdk/bin folder is on the path, you don't have to qualify to run the java and javac commands.

How will you set an environmental variable?

On the Windows taskbar, right-click the Windows icon and select System. In the Settings window, under Related Settings, click Advanced system settings. On the Advanced tab, click Environment Variables. Click New to create a new environment variable.


2 Answers

The most immediate change that comes to mind is to change new to be:

method new() {
    sub env(Str $envVar) {
        %*ENV{$envVar} // die "environment variable $envVar must exist"
    }

    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')),
    );
}

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:

method new() {
    multi env(Str $envVar) {
        $envVar.lc => %*ENV{$envVar} // die "environment variable $envVar must exist"
    }
    multi env(Str $envVar, $type) {
        .key => $type(.value) given env($envVar)
    }

    return self.bless(
        |env('CONFIG_STRING_A'),
        |env('CONFIG_STRING_B'),
        |env('CONFIG_FLAG_C', Bool),
    );
}

Now env returns a Pair, 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:

multi trait_mod:<is>(Attribute $attr, :$from-env!) {
    my $env-name = $attr.name.substr(2).uc;
    $attr.set_build(-> | {
        with %*ENV{$env-name} -> $value {
            Any ~~ $attr.type ?? $value !! $attr.type()($value)
        }
        else {
            die "environment variable $env-name must exist"
        }
    });
}

And then write the class as:

class Configuration {
    has $.config_string_a is from-env;
    has $.config_string_b is from-env;
    has Bool $.config_flag_c is from-env;
}

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 the substr). The set_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 |. The with is just like if defined, so this is the same approach as the // earlier. Finally, the Any ~~ $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).

like image 166
Jonathan Worthington Avatar answered Jan 02 '23 12:01

Jonathan Worthington


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 :

use Trait::Env;
class Configuration {
    has $.config_string_a is env;
    has $.config-string-b is env(:required);
    has Bool $.config-flag-c is env is default(True);
}

The :required flag turns on die if not found. And it plays nicely with the is 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.

like image 20
Scimon Proctor Avatar answered Jan 02 '23 12:01

Scimon Proctor