API-Docker
view release on metacpan or search on metacpan
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' });
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 )