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';
};
};
# ===========================================================================
# 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 0.914 second using v1.01-cache-2.11-cpan-364913b4093 )