API-Docker

 view release on metacpan or  search on metacpan

lib/API/Docker/Type.pm  view on Meta::CPAN

  new BUILDARGS unknown_fields rejected_fields from_data from_json TO_JSON to_json
  docker_attributes docker_attribute_order docker docker_extends
);

sub import {
  my ($class) = @_;
  $class->_setup_class(scalar caller);
  return;
}

# Per-class state that has to outlive the class's own compilation, because a
# generated class ends its body with `use namespace::clean`. That pragma takes
# a snapshot of the package's subs when it is reached and strips every one of
# them at the end of the class's compilation -- the imported Moo and type
# sugar, the two DSL keywords, AND anything a role composed at that point had
# already installed. Two consequences shape the setup below.
#
#   has / extends -- The runtime `docker ...;` and `docker_extends ...;` lines
#     fire after that cleanup, so by then `has` and `extends` are gone from the
#     class and `$target->can('has')` returns undef. The `docker` keyword and
#     the `Str`/`Int`/... it is handed survive the same cleanup only because
#     Perl bound their CV into the call site at compile time; a name the DSL
#     looks up by string at runtime has no such binding, so the two it resolves
#     that way are captured here while they are still imported.
#
#   the role -- API::Docker::Role::Type is NOT composed here. Composed at
#     import (a BEGIN action) its methods and its two attributes would sit in
#     namespace::clean's snapshot and be stripped with the sugar. So it is
#     composed on the first `docker`/`docker_extends` call instead -- at
#     runtime, after the cleanup, exactly as a `with 'Role'` line in a Moo
#     class body would run. Every generated class issues at least one such
#     call, and the composition lands before any object of the class is built.
my %CLASS_SUGAR;

sub _setup_class {
  my ($class, $target) = @_;
  Moo->import::into($target);
  Types::Standard->import::into($target, qw( Any Bool Int Num Str ));
  $CLASS_SUGAR{$target} = {
    has     => $target->can('has'),
    extends => $target->can('extends'),
  };
  my $stash = Package::Stash->new($target);
  $stash->add_symbol('&docker'         => sub { $class->_docker($target, @_) });
  $stash->add_symbol('&docker_extends' => sub { $class->_docker_extends($target, @_) });
  return;
}

# Compose API::Docker::Role::Type once, on the first DSL call the class makes.
# Runtime, so it outlasts the class's `use namespace::clean`; idempotent, so
# the second and later DSL calls are a cheap flag check.
sub _ensure_role {
  my ($target) = @_;
  return if $CLASS_SUGAR{$target}{role_composed};
  $CLASS_SUGAR{$target}{role_composed} = 1;
  Moo::Role->apply_roles_to_package($target, 'API::Docker::Role::Type');
  return;
}

# The one place a short class name becomes a full one. 'Mount' is
# API::Docker::Type::Mount; a name that already starts with the prefix is
# left alone; a leading + means "this is the full name, take it as it is".
# Docker's definitions are flat -- there are no groups to map, which is why
# there is no prefix table here and only this one rule.
sub _expand_class {
  my ($short) = @_;
  return substr($short, 1) if $short =~ /\A\+/;
  return $short if $short =~ /\AAPI::Docker::Type::/;
  return 'API::Docker::Type::' . $short;
}

# PortBindings <- port_bindings. One direction only; see the POD above.
sub _wire_from_perl {
  my ($name) = @_;
  return join '', map { ucfirst } split /_/, $name;
}

sub _is_type_tiny { return blessed($_[0]) && $_[0]->isa('Type::Tiny') }

# A type spec becomes a descriptor, recursively:
#   { kind => 'scalar', scalar => 'Str' }
#   { kind => 'object', class  => 'API::Docker::Type::PortBinding' }
#   { kind => 'array',  inner  => <descriptor> }
#   { kind => 'hash',   inner  => <descriptor> }   keys are caller data
#   { kind => 'any' }
# One recursive shape rather than a flag per combination, so { Str,
# ['PortBinding'] } and [[Str]] need no cases of their own anywhere.
sub _parse_type {
  my ($spec, $where) = @_;
  if (_is_type_tiny($spec)) {
    my $name = $spec->name;
    return { kind => 'any' } if $name eq 'Any';
    return { kind => 'scalar', scalar => $name } if $SCALAR_TYPE{$name};
    croak __PACKAGE__ . ": $where has unsupported type " . $name;
  }
  if (ref $spec eq 'ARRAY') {
    croak __PACKAGE__ . ": $where is an array type with "
      . scalar(@$spec) . ' element types, it needs exactly one'
      unless @$spec == 1;
    return { kind => 'array', inner => _parse_type($spec->[0], $where) };
  }
  if (ref $spec eq 'HASH') {
    my @keys = keys %$spec;
    croak __PACKAGE__ . ": $where is a hash type; write it as { Str, \$value_type }"
      unless @keys == 1 && $keys[0] eq 'Str';
    return { kind => 'hash', inner => _parse_type($spec->{Str}, $where) };
  }
  if (!ref $spec) {
    return { kind => 'any' } if $spec eq 'Any';
    return { kind => 'scalar', scalar => $spec } if $SCALAR_TYPE{$spec};
    return { kind => 'object', class => _expand_class($spec) };
  }
  croak __PACKAGE__ . ": $where has an unreadable type spec (" . ref($spec) . ')';
}


sub describe_type {
  my ($d) = @_;
  my $kind = $d->{kind};
  return lc $d->{scalar} if $kind eq 'scalar';
  return 'object<' . $d->{class} . '>' if $kind eq 'object';
  return 'array<' . describe_type($d->{inner}) . '>' if $kind eq 'array';
  return 'hash<' . describe_type($d->{inner}) . '>' if $kind eq 'hash';
  return 'any';
}

# Nothing is required, so every attribute is Maybe[...]. Hash values and
# array elements are Maybe[...] too: the daemon really does answer
# "2377/tcp": null inside a PortMap, and croaking while inflating a response
# we could otherwise use is not an improvement.
sub _isa_for {
  my ($d) = @_;
  my $kind = $d->{kind};
  return undef if $kind eq 'any';
  return $SCALAR_TYPE{ $d->{scalar} } if $kind eq 'scalar';
  return InstanceOf[ $d->{class} ] if $kind eq 'object';
  my $inner = _isa_for($d->{inner});
  return $kind eq 'array' ? ArrayRef : HashRef unless $inner;
  return $kind eq 'array' ? ArrayRef[ Maybe[$inner] ] : HashRef[ Maybe[$inner] ];
}

# The boolean normalisation, borrowed from IO::K8s::Resource (../io-k8s-p5),
# which documents the two traps: every reference is true in Perl, so \0 and a
# JSON::PP::Boolean have to be dereferenced rather than tested; and 'false'
# is a non-empty string and therefore true, so the strings are spelled out.
#
# undef stays undef rather than becoming 0. Docker tells an absent flag apart
# from a false one, TO_JSON omits undef, and "no value" must not turn into an
# explicit false on the wire.
sub _normalize_bool {
  my ($value) = @_;
  if (ref $value) {
    my $reftype = Scalar::Util::reftype($value);
    croak __PACKAGE__ . ': a Bool wants a scalar or a scalar ref, got ' . $reftype
      unless $reftype eq 'SCALAR' || $reftype eq 'REF';
    $value = $$value;
    croak __PACKAGE__ . ': a Bool scalar ref dereferenced to another reference ('
      . ref($value) . '), not a boolean' if ref $value;
  }
  return undef unless defined $value;
  return 0 if lc($value) eq 'false';
  return $value ? 1 : 0;
}

# A hashref handed to an object-typed field is inflated the way the entry
# point that started the construction reads keys: through from_data while an
# engine response is being inflated, through new otherwise. So a nested
# literal in a request a caller assembled takes both spellings, and a nested
# object in a daemon response resolves wire names only -- the same
# distinction the two entry points draw at the top level, carried one level
# down. The class is loaded on first use rather than at declaration time: the
# registry is the only place its name appears, and loading it while the
# declaring class is still compiling would close a cycle the moment two
# definitions reference each other.
sub _coerce_for {
  my ($d) = @_;
  my $kind = $d->{kind};
  return \&_normalize_bool if $kind eq 'scalar' && $d->{scalar} eq 'Bool';
  if ($kind eq 'object') {
    my $class = $d->{class};
    my $loaded;
    return sub {
      my ($value) = @_;
      return $value unless ref $value eq 'HASH';
      $loaded ||= use_module($class);
      return $API::Docker::Role::Type::RESPONSE
        ? $class->from_data($value)
        : $class->new(%$value);
    };
  }
  my $inner = $kind eq 'array' || $kind eq 'hash' ? _coerce_for($d->{inner}) : undef;
  return undef unless $inner;
  return sub {
    my ($value) = @_;
    return $value unless ref $value eq 'ARRAY';
    return [ map { $inner->($_) } @$value ];
  } if $kind eq 'array';
  return sub {
    my ($value) = @_;
    return $value unless ref $value eq 'HASH';
    # The keys are the caller's data: copied across, never touched.
    return { map { ($_ => $inner->($value->{$_})) } keys %$value };
  };
}

# The serialisation half of a descriptor. Called by
# API::Docker::Role::Type::TO_JSON, which decides what to skip.
sub _encode_value {
  my ($d, $value) = @_;
  return undef unless defined $value;
  my $kind = $d->{kind};
  if ($kind eq 'scalar') {
    my $scalar = $d->{scalar};
    return $value ? JSON::MaybeXS::true() : JSON::MaybeXS::false() if $scalar eq 'Bool';
    return int($value) if $scalar eq 'Int';
    return 0 + $value  if $scalar eq 'Num';
    return "$value";
  }
  return $value->TO_JSON if $kind eq 'object';
  return [ map { _encode_value($d->{inner}, $_) } @$value ] if $kind eq 'array';
  return { map { ($_ => _encode_value($d->{inner}, $value->{$_})) } keys %$value }
    if $kind eq 'hash';
  return [ @$value ] if ref $value eq 'ARRAY';
  return { %$value } if ref $value eq 'HASH';
  return $value;
}



( run in 2.777 seconds using v1.01-cache-2.11-cpan-b301d465b3d )