API-Docker
view release on metacpan or search on metacpan
lib/API/Docker/Type.pm view on Meta::CPAN
sub _docker {
my ($class, $target, $name, $type_spec, %opt) = @_;
_ensure_role($target);
croak __PACKAGE__ . ": '$name' is not a snake_case attribute name"
unless defined $name && $name =~ /\A[a-z][a-z0-9_]*\z/;
croak __PACKAGE__ . ": '$name' in $target is a name this role already uses"
if $RESERVED{$name};
croak __PACKAGE__ . ": '$name' is declared twice in $target"
if $REGISTRY{$target} && $REGISTRY{$target}{$name};
my $wire = delete $opt{wire} // _wire_from_perl($name);
# The Perl-name guard above has a twin: _docker_wire_index maps a wire name
# to one Perl name, so a second field claiming a wire name would make the
# first unreachable on inflation while TO_JSON wrote both to that one key.
# None of the 201 generated classes does this; the generator could emit it
# the day a hand-picked `wire` collides with a derived one (karr k85).
if (my ($taken) = sort grep { $REGISTRY{$target}{$_}{wire} eq $wire }
keys %{ $REGISTRY{$target} || {} }) {
croak __PACKAGE__ . ": '$name' in $target asks for the wire name "
. "'$wire', which '$taken' already has";
}
my $since = delete $opt{since};
my $enum = delete $opt{enum};
my $required = delete $opt{required} ? 1 : 0;
croak __PACKAGE__ . ": '$name' in $target got unknown option(s): "
. join(', ', sort keys %opt) if %opt;
my $descriptor = _parse_type($type_spec, "$target\::$name");
my $isa = _isa_for($descriptor);
my $coerce = _coerce_for($descriptor);
$REGISTRY{$target}{$name} = {
name => $name,
wire => $wire,
type => $descriptor,
since => $since,
enum => $enum,
required => $required,
# The same two the Moo attribute below is given, kept so the response
# path can ask whether a value fits before handing it to the constructor
# rather than finding out by being croaked at (karr k83).
isa => $isa ? Maybe[$isa] : undef,
coerce => $coerce,
};
{
no strict 'refs';
push @{"${target}::_docker_attr_order"}, $name;
}
API::Docker::Role::Type::_invalidate_docker_cache($target);
my $has = $CLASS_SUGAR{$target}{has} // $target->can('has');
my $info = $REGISTRY{$target}{$name};
$has->($name,
is => 'rw',
($info->{isa} ? (isa => $info->{isa}) : ()),
($info->{coerce} ? (coerce => $info->{coerce}) : ()),
);
return;
}
sub _docker_extends {
my ($class, $target, @parents) = @_;
_ensure_role($target);
croak __PACKAGE__ . ": docker_extends in $target needs at least one class"
unless @parents;
my @full = map { use_module(_expand_class($_)) } @parents;
my $extends = $CLASS_SUGAR{$target}{extends} // $target->can('extends');
$extends->(@full);
API::Docker::Role::Type::_invalidate_docker_cache($target);
return;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
API::Docker::Type - The DSL and attribute registry behind the generated Docker types
=head1 VERSION
version 0.004
=head1 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';
=head1 DESCRIPTION
C<API::Docker::Type> is imported, never inherited. Importing it pulls
L<Moo>, the type vocabulary and L<API::Docker::Role::Type> into the calling
package and installs two keywords, C<docker> and C<docker_extends>.
Every class under C<API::Docker::Type::*> is a Perl mirror of one entry
under C<definitions:> in Docker's swagger, which is checked into C<spec/>.
The classes are written from that specification and B<not> from a running
daemon; C<maint/spec-drift-check.pl> is what keeps that claim true.
=head2 What C<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 B<and> writes an entry into a package-level
registry. Both halves matter: the attribute is what a caller uses, the
lib/API/Docker/Type.pm view on Meta::CPAN
C<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.
C<required> is recorded from the swagger's C<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.
=head2 Keys that are the caller's data
The hash form marks a field whose I<keys> the user chose:
docker labels => { Str, Str };
docker port_bindings => { Str, [ 'PortBinding' ] };
Those keys are passed through byte for byte in both directions. C<Labels>,
C<Annotations>, C<ExposedPorts>, C<PortBindings>, C<Volumes>, C<StorageOpt>,
C<Tmpfs>, C<Sysctls>, C<DriverOpts> and C<Options> are all of this shape --
in the swagger they are the fields carrying C<additionalProperties>, which
is the marker to check before deciding a hash's keys are structure. Turning
a label C<com.example.Some-Label> into something the caller never wrote is
the single most damaging mistake this model could make.
=head2 Unknown fields survive
Anything arriving under a name the registry does not know is kept verbatim
in L<API::Docker::Role::Type/unknown_fields> 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 --
C<from_data> reads an engine response and takes wire names only, C<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 C<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
L<API::Docker::Role::Type/"A null on a known field is read as unset">.
=head2 C<allOf> becomes inheritance
Two definitions in v1.51 are composed with C<allOf>, and both have the same
shape -- one C<$ref> plus one inline schema:
HostConfig: allOf [ $ref Resources, { 39 properties } ]
Swarm: allOf [ $ref ClusterInfo, { 1 property } ]
The C<$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';
C<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 L<API::Docker::Role::Type> presents C<HostConfig> with all ~70
fields. The alternative -- copying the parent's declarations into the child
-- would duplicate 31 attributes and their C<=attr> blocks and force the
inline classes underneath them to be named twice.
An C<allOf> holding a single C<$ref> and nothing else is not composition at
all; it is swagger's way of hanging a description on a C<$ref>
(C<Mount.Type> and C<MountPoint.Type> both do it). Such a field takes the
type of what it references, which for C<MountType> is C<Str>.
=head2 Inline objects become classes
A property whose schema is an object with its own C<properties>, or an array
whose C<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 C<Ulimits> into C<Ulimit> is a
judgement call, not a derivation. Those names live in
C<maint/spec-drift-exceptions.yaml> so the checker and a generator agree on
them.
=head2 A generated class loads what it references
Each class carries a plain C<use> for every other type class it names, so
loading C<API::Docker::Type::HostConfig> brings its whole subtree with it.
The declaration itself does B<not> load anything: a class named in a
C<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
C<use> for the back edge is what a generator has to leave out, while the
model keeps working either way.
=head1 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: C<'PortBinding'> is
C<API::Docker::Type::PortBinding>, C<'Mount::BindOptions'> is
C<API::Docker::Type::Mount::BindOptions>. The expansion happens in
C<_expand_class> and nowhere else; a leading C<+> escapes it.
=head2 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 C<< object<Class> >>.
( run in 0.663 second using v1.01-cache-2.11-cpan-80ec619307d )