使用环境变量构建配置类的更简洁方法?

wbn*_*wbn 7 perl6

我有一个Configuration读取环境变量的类:

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;
Run Code Online (Sandbox Code Playgroud)

是否有更简洁的方式来表达这一点?例如,我在检查中重复环境变量名称和构造函数的返回值.

我可以很容易地看到编写另一个更通用的类,它封装了config参数的必要信息:

class ConfigurationParameter {
    has $.name;
    has $.envVarName;
    has Bool $.required;

    method new (:$name, :$envVarName, :$required = True) {
        return self.bless(:$name, :$envVarName, :$required);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后将它们滚动到Configuration类中的List中.但是,我不知道如何重构构造函数Configuration以适应这一点.

Jon*_*ton 9

想到的最直接的变化是改变new为:

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')),
    );
}
Run Code Online (Sandbox Code Playgroud)

虽然//是定义检查而不是存在检查,但环境变量未定义的唯一方法是未设置.这可以归结为%*ENV每个环境变量的提及.

如果只有少数,那么我可能会停在那里,但下一个重复的事情让我感到震惊的是,属性的​​名称只是环境变量名称的小写,所以我们也可以消除那些重复,更复杂的成本:

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),
    );
}
Run Code Online (Sandbox Code Playgroud)

现在env返回a Pair,并将其展|平到参数列表中,就像它是一个命名参数一样.

最后,"电动工具"的方法是在课外写一个这样的特征:

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"
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

然后把课程写成:

class Configuration {
    has $.config_string_a is from-env;
    has $.config_string_b is from-env;
    has Bool $.config_flag_c is from-env;
}
Run Code Online (Sandbox Code Playgroud)

特征在编译时运行,并且可以以各种方式操纵声明.此特征根据属性名称计算环境变量的名称(属性名称总是如此$!config_string_a,因此substr).该set_build组将运行在创建类时初始化属性的代码.这传递了各种各样的事情,在我们的情况下并不重要,所以我们忽略了这些论点|.该with就像if defined,所以这是同样的方法为//早.最后,Any ~~ $attr.type检查询问参数是否以某种方式受约束,如果是,则执行强制(通过使用值调用类型来完成).


Sci*_*mon 5

所以我在评论中提到了这一点,但我认为作为实际答案会很好。我认为这对于构建基于 Docker 的系统的任何人来说都是有用的功能,因此采用 Jonanthan 的示例代码,添加了一些用于导出 Traits 的功能 Elizabeth 向我展示并制作了Trait::Env

用法是:

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);
}
Run Code Online (Sandbox Code Playgroud)

如果未找到,该:required标志将打开die。它与这个is default特性很好地结合在一起。属性名称是大写的,-_在检查之前替换为%*ENV

我有几个计划中的更改,让它抛出一个命名的异常而不是死掉,并且更好地处理布尔值。由于%*ENV是一个字符串有一个布尔值false是一个有点疼痛。