NAME

API::Docker::Type - The DSL and attribute registry behind the generated Docker types

VERSION

version 0.004

SYNOPSIS

package API::Docker::Type::Mount;
use API::Docker::Type;

docker target => Str;

=attr target

Container path.

=cut

docker bind_options => 'Mount::BindOptions', since => '1.41';
docker labels       => { Str, Str };
docker ulimits      => [ 'Resources::Ulimit' ];
docker cpu_shares   => Int, wire => 'CPUShares';

DESCRIPTION

API::Docker::Type is imported, never inherited. Importing it pulls Moo, the type vocabulary and API::Docker::Role::Type into the calling package and installs two keywords, docker and docker_extends.

Every class under API::Docker::Type::* is a Perl mirror of one entry under definitions: in Docker's swagger, which is checked into spec/. The classes are written from that specification and not from a running daemon; maint/spec-drift-check.pl is what keeps that claim true.

What docker does

docker $perl_name => $type;
docker $perl_name => $type, wire => 'CPUShares';
docker $perl_name => $type, since => '1.44';
docker $perl_name => $type, required => 1;

It declares a Moo attribute and writes an entry into a package-level registry. Both halves matter: the attribute is what a caller uses, the registry is what serialisation and the drift checker read. A field that is an attribute but not in the registry is invisible to the drift checker, which is the failure mode that would make the whole model untrustworthy.

Attributes are rw. These objects are request payloads a caller assembles field by field ($host_config->privileged(1)), not immutable value objects.

The wire name

The swagger's spelling is the truth and the Perl name is derived from it -- never the other way round, because the derivation does not round-trip: PortBindings and port_bindings map to each other, KernelMemoryTCP and kernel_memory_tcp do not.

At load time docker derives the wire name back from the Perl name by upper-casing the first letter of every underscore-separated part, so port_bindings becomes PortBindings. Where that would not reproduce the spec's spelling -- IP, UTSMode, KernelMemoryTCP, DeviceIDs, IOMaximumIOps, os.features -- the declaration carries an explicit wire => '...' and the Perl name is chosen by hand.

A wire name belongs to one field. docker refuses a declaration asking for a name an earlier field in the same class already has, the way it refuses a duplicate Perl name: both halves of the pair have to be unique, or inflation would reach one of the two fields and TO_JSON would write both to that one key.

What since can and cannot say

spec/ holds v1.41, v1.44 and v1.51, which is what the --from/--to mode of maint/spec-drift-check.pl diffs to produce these values -- the swagger itself carries no per-field version at all. So since => '1.51' means "not in v1.44, present in v1.51", not "introduced in v1.51": the field appeared somewhere in v1.45 .. v1.51. An attribute with no since is already in v1.41, the oldest spec checked in here.

since is documentation

since records which API version introduced a field. Nothing is checked, warned about or dropped at runtime, ever. Podman serves fields its announced version does not promise and refuses ones it does; we are not the authority on what an engine can do. The registry keeps the value so a runtime check could be retrofitted, and so the POD can state it.

required is recorded from the swagger's required: list and is likewise not enforced: the same engines omit fields the specification calls required, and croaking on a response we could otherwise use is not an improvement.

Keys that are the caller's data

The hash form marks a field whose keys the user chose:

docker labels        => { Str, Str };
docker port_bindings => { Str, [ 'PortBinding' ] };

Those keys are passed through byte for byte in both directions. Labels, Annotations, ExposedPorts, PortBindings, Volumes, StorageOpt, Tmpfs, Sysctls, DriverOpts and Options are all of this shape -- in the swagger they are the fields carrying additionalProperties, which is the marker to check before deciding a hash's keys are structure. Turning a label com.example.Some-Label into something the caller never wrote is the single most damaging mistake this model could make.

Unknown fields survive

Anything arriving under a name the registry does not know is kept verbatim in "unknown_fields" in API::Docker::Role::Type and written back out unchanged. A caller whose engine is newer than the swagger we generated from still reaches the daemon, and so does a field an engine sends that the swagger does not describe. Which names count as known depends on the entry point -- from_data reads an engine response and takes wire names only, new builds a request and takes either spelling; see that role for the reasoning.

A null is where the two name spaces part. A field the registry knows that arrives as null is read as unset and its key does not come back, because the daemon cannot tell an explicit null from an absent field in either direction; a field the registry does not know keeps its null, because without a declared type there is no zero value to read it as. The measurement and the three shapes it produces are in "A null on a known field is read as unset" in API::Docker::Role::Type.

allOf becomes inheritance

Two definitions in v1.51 are composed with allOf, and both have the same shape -- one $ref plus one inline schema:

HostConfig: allOf [ $ref Resources, { 39 properties } ]
Swarm:      allOf [ $ref ClusterInfo, { 1 property } ]

The $ref becomes a superclass and only the inline schema's properties are declared in the child:

package API::Docker::Type::HostConfig;
use API::Docker::Type;

docker_extends 'Resources';

allOf in swagger means composition, and Perl inheritance says exactly that. Nothing is duplicated: the parent's fields, their POD and the inline classes declared inside the parent all stay in one place, and the merged registry in API::Docker::Role::Type presents HostConfig with all ~70 fields. The alternative -- copying the parent's declarations into the child -- would duplicate 31 attributes and their =attr blocks and force the inline classes underneath them to be named twice.

An allOf holding a single $ref and nothing else is not composition at all; it is swagger's way of hanging a description on a $ref (Mount.Type and MountPoint.Type both do it). Such a field takes the type of what it references, which for MountType is Str.

Inline objects become classes

A property whose schema is an object with its own properties, or an array whose items are such an object, becomes a class named after the definition that declares it:

Mount.BindOptions             -> API::Docker::Type::Mount::BindOptions
Mount.VolumeOptions.DriverConfig
                              -> API::Docker::Type::Mount::VolumeOptions::DriverConfig
Resources.Ulimits[]           -> API::Docker::Type::Resources::Ulimit

The last one is the exception to the mechanical rule: an array of inline objects is named for one element, and turning Ulimits into Ulimit is a judgement call, not a derivation. Those names live in maint/spec-drift-exceptions.yaml so the checker and a generator agree on them.

A generated class loads what it references

Each class carries a plain use for every other type class it names, so loading API::Docker::Type::HostConfig brings its whole subtree with it. The declaration itself does not load anything: a class named in a docker line is loaded lazily, on the first hashref that has to be inflated into it. That is deliberate belt and braces -- v1.51's definitions happen to have no reference cycles, and if a later version grows one the use for the back edge is what a generator has to leave out, while the model keeps working either way.

THE TYPE VOCABULARY

Str  Int  Num  Bool       scalars
Any                       untyped; passed through as it arrived
[Str]                     an array of scalars
[[Str]]                   an array of arrays of scalars
['PortBinding']           an array of typed objects
'PortBinding'             a single typed object
'+Some::Other::Class'     the same, without the namespace prefix
{ Str, Str }              a hash whose KEYS ARE CALLER DATA
{ Str, ['PortBinding'] }  the same, with typed values

A bare class name is short: 'PortBinding' is API::Docker::Type::PortBinding, 'Mount::BindOptions' is API::Docker::Type::Mount::BindOptions. The expansion happens in _expand_class and nowhere else; a leading + escapes it.

describe_type

API::Docker::Type::describe_type($info->{type});   # 'hash<array<object>>'

A descriptor as one string, for the drift checker's report. Objects render as object<Class>.

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-api-docker/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.