API-Docker

 view release on metacpan or  search on metacpan

.claude/skills/getty-perl-core/SKILL.md  view on Meta::CPAN

## Control flow

- **Postfix `if`/`unless`** for guards and short conditions: `croak(...) if $self->readonly;`
- **`unless $x`** instead of `if !$x`.
- **Guard clauses return bare:** `return unless $res->is_success;` — not `return undef;`.
- **Nested ternaries** for a return that picks between expressions, instead of an if/elsif chain.

## Data

- **`{ %hash }` and `\%hash` are different operations, not two styles.** `{ %h }` builds a new anonymous copy; `\%h` references the existing hash. Return a copy when the caller must not mutate your state; return the reference when sharing is the poin...
- **`Path::Tiny`** for every file operation — not `File::Spec`, not bare `open`. `path(...)->child(...)->slurp_utf8`.
- **`JSON::MaybeXS`** always — never `JSON::PP`, `JSON::XS`, `Cpanel::JSON::XS` directly. Encoders get `canonical => 1, convert_blessed => 1`.
- **Every serialiser is deterministic.** MessagePack `->canonical`, DBIC `serializer_options => { canonical => 1 }`. Same rule, every format.
- **Booleans: `JSON->true` / `JSON->false`.** `use JSON::MaybeXS;` covers codec and booleans.
- `$YAML::XS::Boolean = 'JSON::PP'` is one of YAML::XS's fixed mode names, not a module choice — leave it alone.
- **Align `=>` in multi-line hash literals** when keys are of similar length.
- **Optional pairs inline:** `$cond ? ( experimental => 1 ) : (),`

## Configuration

Config comes from environment variables prefixed with the project name

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

  my ($self, $class, $list) = @_;
  return [ map { $self->_wrap($class, $_) } @$list ];
}

# The wire field is base64; the public contract is raw bytes. Guarding the
# character range here keeps the failure a croak naming this class instead of
# MIME::Base64's "Wide character in subroutine entry" from two frames down.
sub _encode_data {
  my ($self, $method, $data) = @_;
  croak __PACKAGE__ . "->$method Data must be a byte string, not decoded "
    . 'characters -- encode it first (Encode::encode_utf8)'
    if $data =~ /[^\x00-\xff]/;
  return encode_base64($data, '');
}

sub list {
  my ($self, %opts) = @_;
  my %params;
  $params{filters} = $self->_normalise_filters($opts{filters})
    if defined $opts{filters};
  return $self->_wrap_list('API::Docker::Type::Config',

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


The alphabet is B<standard> base64 with padding (C<+> and C</>), unwrapped,
and not the URL-safe one. The Engine API reference calls the field
"base64-url-safe-encoded" and that is measurably not what the engine takes:
four bytes sent as C<-v_--w==> were rejected B<500>, the same four as
C<+v/++w==> stored correctly.

C<Data> must be a byte string. Characters above C<U+00FF> croak here rather
than reaching L<MIME::Base64> and dying with a bare
C<Wide character in subroutine entry>; encode first, e.g. with
C<Encode::encode_utf8>.

B<The reverse trip is not symmetric, deliberately.> L</inspect> and L</list>
hand back what the daemon sent with nothing rewritten, so
C<< $config->spec->data >> is still base64. Decoding is a separate,
explicit call on the entity:

    my $text = $config->decoded_data;

The asymmetry follows one rule: this class encodes where getting it wrong is
silent, and rewrites nothing where getting it wrong is visible. An unencoded

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

  my ($self, $class, $list) = @_;
  return [ map { $self->_wrap($class, $_) } @$list ];
}

# The wire field is base64; the public contract is raw bytes. Guarding the
# character range here keeps the failure a croak naming this class instead of
# MIME::Base64's "Wide character in subroutine entry" from two frames down.
sub _encode_data {
  my ($self, $method, $data) = @_;
  croak __PACKAGE__ . "->$method Data must be a byte string, not decoded "
    . 'characters -- encode it first (Encode::encode_utf8)'
    if $data =~ /[^\x00-\xff]/;
  return encode_base64($data, '');
}

sub list {
  my ($self, %opts) = @_;
  my %params;
  $params{filters} = $self->_normalise_filters($opts{filters})
    if defined $opts{filters};
  return $self->_wrap_list('API::Docker::Type::Secret',

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

The alphabet is B<standard> base64 with padding (C<+> and C</>), not the
URL-safe one, and unwrapped. The Engine API reference calls the field
"base64-url-safe-encoded"; that is measurably not what the engine accepts. The
same four bytes sent as C<-v_--w==> were rejected with B<500>
C<"secret data must be larger than 0 and less than 512000 bytes"> -- the
URL-safe alphabet decoded to nothing -- where C<+v/++w==> was stored correctly.

C<Data> must be a byte string. A string holding characters above C<U+00FF>
croaks here rather than reaching L<MIME::Base64>, which would die with a bare
C<Wide character in subroutine entry>. Encode it first, for instance with
C<Encode::encode_utf8>.

To send an already-encoded value verbatim, bypass this class and use the
transport directly:

    $docker->post('/secrets/create', { Name => 'my-secret', Data => $b64 });

=head2 update takes the current version, and it is mandatory

C<POST /secrets/{id}/update> carries a C<version> query parameter, and the
daemon rejects the request without it. The value is the C<Version.Index> of

lib/API/Docker/Role/Entity/Config.pm  view on Meta::CPAN


    my $text = $config->decoded_data;

The config's content: C<< $config->spec->data >> run through
L<MIME::Base64/decode_base64>. Returns nothing when the object carries no
C<Spec> or no C<Data> in it.

The result is B<raw bytes>, symmetric with what
L<API::Docker::API::Configs/create> takes -- decode the character set yourself
if the config holds text above C<U+007F>, for instance with
C<Encode::decode_utf8>.

The spec is left alone; see
L</"Decoding is offered here, not in the API class">.

=head2 version_index

    my $index = $config->version_index;

The C<< ->index >> out of C<< ->version >>, which is what the daemon wants as
the C<version> query parameter on an update. Returns nothing when the object

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

  return $value if $value =~ /\A[0-9]+\z/;

  $self->_croak_truncated($ctx, phase => 'content-length',
    detail => "the Content-Length header '" . $value . "' is not a number");
}

sub _uri_encode {
  my ($str) = @_;
  # Escape a character string by its UTF-8 bytes ('ü' -> %C3%BC, not %FC), and
  # a byte string as it stands. ord() on a character is not its wire byte: a
  # name or tag typed under `use utf8`, or read through a :utf8 layer, arrives
  # as characters and used to escape to a lone high byte or a bare codepoint
  # (%FC, %4E2D) that is not UTF-8 at all. But the encoding cannot be
  # unconditional: encode_json has already handed a HASH param (filters among
  # them) its UTF-8 octets, and re-encoding those would double them
  # (%C3%BC -> %C3%83%C2%BC). The utf8 flag is exactly that distinction -- on
  # for a decoded string, off for encode_json's output -- so a copy is encoded
  # only when it carries one, leaving the caller's own value untouched either
  # way.
  my $bytes = $str;
  utf8::encode($bytes) if utf8::is_utf8($bytes);
  $bytes =~ s/([^A-Za-z0-9\-_.~:\/])/sprintf("%%%02X", ord($1))/ge;
  return $bytes;
}

sub get {
  my ($self, $path, %opts) = @_;
  return $self->_request('GET', $path, %opts);
}


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

  local $@;
  my $coerced = $info->{coerce} ? eval { $info->{coerce}->($value) } : $value;
  return (0) if $@;
  return (0) if $info->{isa} && !$info->{isa}->check($coerced);
  return (1, $coerced);
}


sub from_json {
  my ($class, $json) = @_;
  return $class->from_data(JSON::MaybeXS->new(utf8 => 1)->decode($json));
}


sub TO_JSON {
  my ($self) = @_;
  my %out = %{ $self->unknown_fields };
  my $reg = $self->_docker_attr_registry;
  for my $attr (@{ $self->_docker_attr_order }) {
    my $value = $self->$attr;
    next unless defined $value;
    $out{ $reg->{$attr}{wire} }
      = API::Docker::Type::_encode_value($reg->{$attr}{type}, $value);
  }
  return \%out;
}


sub to_json {
  my ($self) = @_;
  return JSON::MaybeXS->new(utf8 => 1, canonical => 1, convert_blessed => 1)
    ->encode($self->TO_JSON);
}


sub docker_attributes { return $_[0]->_docker_attr_registry }


sub docker_attribute_order { return $_[0]->_docker_attr_order }

# --- attributes that are not the daemon's ----------------------------------

t/lib/Test/API/Docker/Mock.pm  view on Meta::CPAN

);

my $FIXTURES_DIR = path(__FILE__)->parent->parent->parent->parent->parent->child('fixtures');

my @_cleanups;

sub load_fixture {
  my ($name) = @_;
  my $file = $FIXTURES_DIR->child("$name.json");
  croak "Fixture not found: $file" unless $file->exists;
  return decode_json($file->slurp_utf8);
}

# Some fixtures are not JSON: the framed log/exec streams are captured
# engine bytes, and the build/pull event streams are newline-delimited JSON
# whose line framing is the thing under test. Both must come back byte-exact.
sub load_fixture_raw {
  my ($name) = @_;
  my $file = $FIXTURES_DIR->child($name);
  croak "Fixture not found: $file" unless $file->exists;
  return $file->slurp_raw;

t/role_http.t  view on Meta::CPAN

    'slash is left raw too -- image references keep their path shape';
  is $encode->('abcXYZ019-_.~'), 'abcXYZ019-_.~',
    'unreserved characters (alnum - _ . ~) are never escaped';
  is $encode->('a b'), 'a%20b', 'space is percent-encoded';
  is $encode->('foo?bar=baz'), 'foo%3Fbar%3Dbaz',
    '? and = are percent-encoded';
  is $encode->('100%'), '100%25', 'a literal percent sign is escaped itself';
  is $encode->("a\nb"), 'a%0Ab', 'control characters are escaped, not passed through';

  # A character string -- what a name/tag/author/comment/search term arrives as
  # under `use utf8` or through a :utf8 layer -- is escaped by its UTF-8 bytes,
  # not by its codepoint. The old code took ord() of the character, so 'ü'
  # became %FC (not even valid UTF-8) and '中' became %4E2D.
  is $encode->("\x{4E2D}"), '%E4%B8%AD',
    'a wide character is escaped by its UTF-8 bytes, not its codepoint';
  {
    my $u = "\x{00FC}";
    utf8::upgrade($u); # what a decoded 'ü' is: codepoint 252, the utf8 flag on
    is $encode->($u), '%C3%BC',
      'a Latin-1 character with the utf8 flag is UTF-8 encoded before escaping';
  }

  # The other half, and the reason the encoding is not unconditional: a byte
  # string is already octets and must be escaped as-is. encode_json hands a
  # HASH param (filters among them) its UTF-8 bytes, and re-encoding those would
  # turn %C3%BC into %C3%83%C2%BC -- trading this bug for a broader one.
  is $encode->("\xC3\xBC"), '%C3%BC',
    'a byte string of UTF-8 octets is escaped as-is, never double-encoded';
};

t/tls.t  view on Meta::CPAN

};

# ===========================================================================
# The transport now reads both attributes
# ===========================================================================

subtest 'the transport consults tls and cert_path' => sub {
  # The inverse of the claim this replaces, which asserted that
  # API::Docker::Role::HTTP never mentions either attribute and that its
  # tcp:// branch is unconditionally a plain socket.
  my $source = path($INC{'API/Docker/Role/HTTP.pm'})->slurp_utf8;

  ok length($source) > 1000, 'the transport source was actually read';
  like $source, qr/\$self->tls\b/, 'the socket builder asks whether TLS is wanted';
  like $source, qr/\$self->cert_path\b/, 'and reads the certificate directory';
  like $source, qr/IO::Socket::SSL->new/, 'the tcp:// branch can be an SSL socket';
  like $source, qr/IO::Socket::INET->new/, 'and is still a plain one without TLS';
};

SKIP: {
  skip 'IO::Socket::SSL is not installed', 4 unless $HAVE_SSL;



( run in 3.237 seconds using v1.01-cache-2.11-cpan-364913b4093 )