API-Docker

 view release on metacpan or  search on metacpan

t/type.t  view on Meta::CPAN

  is_deeply($bind->TO_JSON, { CreateMountpoint => JSON->true },
    'a 1.44 field on a 1.41 engine is still sent -- Podman serves fields its '
      . 'announced version does not promise, and refuses ones it does');
};

# ---------------------------------------------------------------------------
# The wire name is derived from the Perl name, and the spec's spelling wins
# ---------------------------------------------------------------------------

subtest 'wire names' => sub {
  my $reg = API::Docker::Type::Resources->docker_attributes;
  is($reg->{cpu_shares}{wire},  'CpuShares',  'port_bindings-style derivation');
  is($reg->{nano_cpus}{wire},   'NanoCpus',   'derivation covers most fields');
  is($reg->{oom_kill_disable}{wire}, 'OomKillDisable', 'and multi-word ones');
  is($reg->{kernel_memory_tcp}{wire}, 'KernelMemoryTCP',
    'a spelling the derivation cannot reach is declared explicitly');
  is($reg->{io_maximum_iops}{wire}, 'IOMaximumIOps', 'likewise IOMaximumIOps');
  is($reg->{blkio_device_read_iops}{wire}, 'BlkioDeviceReadIOps',
    'likewise BlkioDeviceReadIOps');
  is(API::Docker::Type::Port->docker_attributes->{ip}{wire}, 'IP',
    'and a two-letter all-caps name');
  is(API::Docker::Type::HostConfig->docker_attributes->{uts_mode}{wire}, 'UTSMode',
    'and UTSMode');

  my $r = API::Docker::Type::Resources->new(
    kernel_memory_tcp => 1024, io_maximum_iops => 7);
  is_deeply($r->TO_JSON, { KernelMemoryTCP => 1024, IOMaximumIOps => 7 },
    'the explicit spelling is what goes on the wire');
  my $back = API::Docker::Type::Resources->from_data({ KernelMemoryTCP => 1024 });
  is($back->kernel_memory_tcp, 1024, 'and what is read back off it');
  is_deeply($back->unknown_fields, {},
    'a wire name the registry knows is never mistaken for an unknown field');
};

# ---------------------------------------------------------------------------
# Booleans: Docker tells an absent flag apart from a false one
# ---------------------------------------------------------------------------

subtest 'booleans' => sub {
  for my $false (JSON->false, \0, 'false', 'FALSE', 0, '') {
    my $m = API::Docker::Type::Mount->new(read_only => $false);
    is($m->TO_JSON->{ReadOnly}, JSON->false,
      'false-ish value ' . (ref $false ? ref $false : "'$false'") . ' serialises to JSON false');
  }
  for my $true (JSON->true, \1, 'true', 1, 'yes') {
    my $m = API::Docker::Type::Mount->new(read_only => $true);
    is($m->TO_JSON->{ReadOnly}, JSON->true,
      'true-ish value ' . (ref $true ? ref $true : "'$true'") . ' serialises to JSON true');
  }
  my $unset = API::Docker::Type::Mount->new(target => '/x');
  ok(!exists $unset->TO_JSON->{ReadOnly},
    'a Bool that was never set is absent, not false');
  my $explicit = API::Docker::Type::Mount->new(target => '/x', read_only => 0);
  ok(exists $explicit->TO_JSON->{ReadOnly},
    'a Bool explicitly set to false is present and false');
  is($json->encode($explicit->TO_JSON), '{"ReadOnly":false,"Target":"/x"}',
    'and encodes as JSON false, never as 1 or the empty string');
  like(
    do { local $@; eval { API::Docker::Type::Mount->new(read_only => [1]) }; $@ },
    qr/Bool wants a scalar/,
    'something that cannot mean true or false croaks instead of being guessed at');
};

# ---------------------------------------------------------------------------
# Nesting, arrays of objects, and the allOf inheritance
# ---------------------------------------------------------------------------

subtest 'nested objects and arrays' => sub {
  my $hc = API::Docker::Type::HostConfig->from_data({
    RestartPolicy       => { Name => 'on-failure', MaximumRetryCount => 3 },
    LogConfig           => { Type => 'json-file', Config => { 'max-size' => '10m' } },
    Mounts              => [ { Target => '/data', Type => 'volume' } ],
    Ulimits             => [ { Name => 'nofile', Soft => 1024, Hard => 2048 } ],
    BlkioDeviceReadIOps => [ { Path => '/dev/sda', Rate => 100 } ],
    Devices             => [ { PathOnHost => '/dev/x', PathInContainer => '/dev/x' } ],
    ConsoleSize         => [ 80, 64 ],
  });
  isa_ok($hc->restart_policy, 'API::Docker::Type::RestartPolicy');
  isa_ok($hc->log_config,     'API::Docker::Type::HostConfig::LogConfig');
  isa_ok($hc->mounts->[0],    'API::Docker::Type::Mount');
  isa_ok($hc->ulimits->[0],   'API::Docker::Type::Resources::Ulimit');
  isa_ok($hc->blkio_device_read_iops->[0], 'API::Docker::Type::ThrottleDevice');
  isa_ok($hc->devices->[0],   'API::Docker::Type::DeviceMapping');
  is($hc->ulimits->[0]->soft, 1024, 'an inline array element inflates');
  is_deeply($hc->console_size, [ 80, 64 ], 'an array of scalars stays an array of scalars');

  my $mount = API::Docker::Type::Mount->from_data({
    TmpfsOptions => { Options => [ ['noexec'], [ 'size', '64m' ] ] },
  });
  is_deeply($mount->tmpfs_options->options, [ ['noexec'], [ 'size', '64m' ] ],
    'an array of arrays of strings survives');

  my $built = API::Docker::Type::HostConfig->new(
    restart_policy => API::Docker::Type::RestartPolicy->new(name => 'always'),
  );
  is_deeply($built->TO_JSON, { RestartPolicy => { Name => 'always' } },
    'an object built by hand serialises the same way an inflated one does');
};

subtest 'allOf becomes inheritance' => sub {
  ok(API::Docker::Type::HostConfig->isa('API::Docker::Type::Resources'),
    'HostConfig is a Resources, because the swagger says allOf [ $ref Resources, ... ]');
  my $order = API::Docker::Type::HostConfig->docker_attribute_order;
  is(scalar @$order, 70, 'HostConfig carries 31 inherited plus 39 of its own');
  is($order->[0], 'cpu_shares', 'the inherited fields come first, as the allOf lists them');
  is($order->[31], 'binds', 'and the class own fields follow in spec order');
  my $hc = API::Docker::Type::HostConfig->from_data({ CpuShares => 512, Binds => ['/a:/b'] });
  is($hc->cpu_shares, 512, 'an inherited field inflates on the child');
  is_deeply($hc->TO_JSON, { CpuShares => 512, Binds => ['/a:/b'] },
    'and serialises flat, the way it sits on the wire');
  is(scalar @{ API::Docker::Type::Resources->docker_attribute_order }, 31,
    'the parent is unaffected by the child');
};

# ---------------------------------------------------------------------------
# Both spellings, and what the DSL refuses
# ---------------------------------------------------------------------------

subtest 'constructor accepts both spellings' => sub {
  my $a = API::Docker::Type::Port->new(private_port => 80, type => 'tcp');
  my $b = API::Docker::Type::Port->from_data({ PrivatePort => 80, Type => 'tcp' });

t/type.t  view on Meta::CPAN

  is_deeply($c->unknown_fields, { State => 'exited' },
    'and the raw value is kept under that wire name');
  is_deeply($c->TO_JSON, { Id => 'x', Name => '/keep', State => 'exited' },
    'so TO_JSON writes it back byte for byte -- nothing is lost');

  # The precedence question k85 defect 4 asks does not arise out of a
  # response: a value lands in unknown_fields under a KNOWN wire name only
  # when its attribute was left unset, and TO_JSON omits an unset attribute,
  # so the two never meet on this path.
  my $round = API::Docker::Type::ContainerInspectResponse->from_data($c->TO_JSON);
  is($json->encode($round->TO_JSON), $json->encode($c->TO_JSON),
    'a rejected value survives a second pass through the model unchanged');
  is_deeply($round->rejected_fields, { State => 'state' },
    'and is still reported as rejected rather than as merely unknown');

  # Absent and rejected must not be the same observation.
  my $absent = API::Docker::Type::ContainerInspectResponse->from_data({ Id => 'x' });
  is($absent->state, undef, 'an absent field leaves the accessor undef too');
  is_deeply($absent->rejected_fields, {},
    'but nothing is recorded as rejected, which is how the two are told apart');
  ok(!exists $absent->unknown_fields->{State},
    'and nothing is kept under its name');

  # Anything the coercion itself refuses is the same case, not a special one.
  my $bool = API::Docker::Type::Mount->from_data({ Target => '/x', ReadOnly => [1] });
  is($bool->read_only, undef, 'a Bool that can mean neither is not set');
  is_deeply($bool->rejected_fields, { ReadOnly => 'read_only' },
    'it is reported as rejected');
  is_deeply($bool->TO_JSON, { Target => '/x', ReadOnly => [1] },
    'and its value still reaches the daemon as it arrived');

  # One level down, through the coercion that inflates a nested object.
  my $nested = API::Docker::Type::HostConfig->from_data({
    RestartPolicy => { Name => 'always', MaximumRetryCount => 'many' } });
  is($nested->restart_policy->name, 'always', 'the nested object still inflates');
  is($nested->restart_policy->maximum_retry_count, undef,
    'and only the field inside it that did not fit is unset');
  is_deeply($nested->TO_JSON,
    { RestartPolicy => { Name => 'always', MaximumRetryCount => 'many' } },
    'the nested raw value goes back out too');
};

subtest 'new is strict, and stays strict' => sub {
  # The leniency belongs to the path that inflates an engine response. A
  # caller who writes a value the swagger does not allow has made a typo, and
  # a typo in a request is worth dying on.
  like(
    do { local $@; eval {
      API::Docker::Type::ContainerInspectResponse->new(State => 'exited') }; $@ },
    qr/did not pass type constraint|State/,
    'a value that does not fit croaks out of new');
  like(
    do { local $@; eval {
      API::Docker::Type::HostConfig->new(
        restart_policy => { Name => 'always', MaximumRetryCount => 'many' }) }; $@ },
    qr/did not pass type constraint|MaximumRetryCount|maximum_retry_count/,
    'and so does one inside a nested hashref the caller wrote');
  like(
    do { local $@; eval { API::Docker::Type::Mount->new(read_only => [1]) }; $@ },
    qr/Bool wants a scalar/,
    'a Bool that can mean neither still croaks rather than being guessed at');

  my $ok = API::Docker::Type::ContainerInspectResponse->new(Id => 'x');
  is_deeply($ok->rejected_fields, {},
    'an object a caller built never has anything in rejected_fields');

  # The leniency hangs on the entry point, not on the flag that tells a
  # nested coercion which entry point it is under. Were it the flag, a `new`
  # reached from inside a response inflation would go soft with it.
  like(
    do { local $@;
         local $API::Docker::Role::Type::RESPONSE = 1;
         eval { API::Docker::Type::ContainerInspectResponse->new(State => 'exited') };
         $@ },
    qr/did not pass type constraint|State/,
    'new croaks even while a response is being inflated around it');
};

subtest 'the DSL refuses two fields on one wire name' => sub {
  # _docker_wire_index is a map from wire name to Perl name, so a second
  # field claiming a wire name would make the first unreachable on
  # inflation while TO_JSON wrote both to the one key. Nothing in the 201
  # generated classes does this; the generator could emit it tomorrow.
  my $err = do { local $@; eval q{
    package API::Docker::Type::TypeTestDuplicateWire;
    use API::Docker::Type;
    docker cpu_shares => Int, wire => 'CPUShares';
    docker shares     => Int, wire => 'CPUShares';
    1;
  }; $@ };
  like($err, qr/\Qwire name 'CPUShares'\E/,
    'a duplicate wire name is refused where a duplicate Perl name already is');
  like($err, qr/cpu_shares/, 'and the message names the field that has it');

  my $ok = do { local $@; eval q{
    package API::Docker::Type::TypeTestDistinctWire;
    use API::Docker::Type;
    docker cpu_shares => Int, wire => 'CPUShares';
    docker shares     => Int;
    1;
  }; $@ };
  is($ok, '', 'two fields with distinct wire names are still fine');

  my $dup = do { local $@; eval q{
    package API::Docker::Type::TypeTestDuplicatePerl;
    use API::Docker::Type;
    docker shares => Int;
    docker shares => Int, wire => 'Other';
    1;
  }; $@ };
  like($dup, qr/declared twice/, 'and the Perl-name guard is untouched');
};

done_testing;



( run in 0.570 second using v1.01-cache-2.11-cpan-54e63673c56 )