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 )