Config::ENV で各環境で必ず定義すべきキーを定めておく

metacpan.org

ちょうどいいデフォルト値が見当たらないので、このキーは各環境で適宜、定義せよ、という風にしたい。interface みたいなかんじ。

use strict;
use warnings;
use Test::More tests => 2;
use Test::Fatal qw(exception);

{
  package My::Config::NotImplemented;

  use strict;
  use warnings;
  use Scalar::Util qw(blessed);

  sub new {
    my ($class) = @_;
    return bless {}, $class;
  }

  sub equals {
    my ($self, $other) = @_;
    return blessed($other) && $other->isa(ref($self));
  }
};

{
  package My::Config;
  use strict;
  use warnings;
  use Config::ENV 'MY_ENV';

  use constant NOT_IMPLEMENTED => My::Config::NotImplemented->new;

  sub declare ($) {
    my ($name) = @_;
    return ($name => NOT_IMPLEMENTED);
  }

  common +{
    declare 'log.path',
  };

  config production => {
      'log.path' => '/var/log/app.log',
  };

  config test => +{};
};

{
  package Test::My::Config;
  use strict;
  use warnings;
  use Config::ENV ();

  sub import {
    my ($class) = @_;
    no strict 'refs';
    no warnings 'once';
    *My::Config::param = \&param;
  }

  sub param {
    my ($class, $name) = @_;
    my $value = Config::ENV::param($class, $name);
    return !My::Config::NOT_IMPLEMENTED->equals($value);
  }
};

Test::My::Config->import;

my $envs = do {
  my $envs = My::Config->_data->{envs};
  [ keys %$envs ];
};

for my $env (@$envs) {
  local $ENV{MY_ENV} = $env;
  my $current = My::Config->current;
  for my $key (keys %$current) {
    ok +My::Config->param($key), "Environment ($env) defines `$key'";
  }
}

declare($name) で各環境で定義すべきキーを宣言する。これは対応する値として意味のないオブジェクトを便宜上定義する (Config::ENV は設定値として hashref を期待するので)。

テスト時に Config::ENV を継承したクラスの param($name) を上書きして、値が NOT_IMPLEMENTED オブジェクトかどうかをアサートする、という風にする。

すると # Failed test 'Environment (test) defines `log.path'' という風にテストが失敗し、たしかに test で log.path が定義されていない、ということがわかる。

JSON schema とか XML とか使うともっとリッチな検証ができそうだけど、Config::ENV でも素朴にできるというコンセプト。