API-Docker
view release on metacpan or search on metacpan
lib/API/Docker/Role/Type.pm view on Meta::CPAN
# True while `from_data` is inflating a decoded engine response, so that the
# hashref coercion in API::Docker::Type reads a nested literal the same way
# the outermost one was read. Dynamically scoped: `local`ised around the
# whole construction, which is what a nested from_data sees.
our $RESPONSE = 0;
# Constructor-side name resolution for `new`, and one of the two places
# unknown fields are collected. A key is taken as a Perl attribute name
# first and as a wire name second, because `new` assembles a REQUEST out of
# what a caller wrote and both spellings are the caller's to choose. The
# response path does not come through here -- see L</from_data>.
#
# Two keys resolving to one attribute is refused rather than decided by hash
# order, and refused whether or not the two values agree: values that happen
# to match are not the same thing as an unambiguous call, and the caller
# should see the mistake instead of the luck (karr k85).
#
# Idempotent on purpose: where one generated class extends another (the
# `allOf` shape, see API::Docker::Type) the role is composed into both, so
# this modifier runs twice on the same arguments. The second pass sees every
# key already resolved and merges an empty set into unknown_fields.
around BUILDARGS => sub {
my ($orig, $class, @args) = @_;
my $args = $class->$orig(@args);
my $known = $class->_docker_attr_registry;
my $wire = $class->_docker_wire_index;
my $mine = $class->_entity_attribute_index;
my %unknown = %{ delete($args->{unknown_fields}) || {} };
my %rejected = %{ delete($args->{rejected_fields}) || {} };
my (%out, %from);
for my $key (keys %$args) {
my $attr = $known->{$key} ? $key
: defined $wire->{$key} ? $wire->{$key}
: $mine->{$key} ? $key
: undef;
unless (defined $attr) { $unknown{$key} = $args->{$key}; next }
croak __PACKAGE__ . ": $class got '"
. join("' and '", sort($from{$attr}, $key))
. "' for the same field '$attr'; pass one spelling, not both"
if exists $out{$attr};
$out{$attr} = $args->{$key};
$from{$attr} = $key;
}
$out{unknown_fields} = \%unknown;
$out{rejected_fields} = \%rejected;
return \%out;
};
sub from_data {
my ($class, $data, %extra) = @_;
$class = ref($class) if ref($class);
croak __PACKAGE__ . '->from_data needs a HashRef'
unless ref $data eq 'HASH';
my $reg = $class->_docker_attr_registry;
my $wire = $class->_docker_wire_index;
# Lifted out of the loop rather than handled in it, so that what the loop
# files never depends on the order the keys came out in -- the same reason
# the BUILDARGS above deletes them before it starts.
my %args;
my %unknown = %{ $data->{unknown_fields} || {} };
my %rejected = %{ $data->{rejected_fields} || {} };
# In effect for the coercions _fits runs below as well as for the
# constructor at the end: a nested hashref is part of the same response
# and has to be read as one.
local $RESPONSE = 1;
for my $key (keys %$data) {
next if $key eq 'unknown_fields' || $key eq 'rejected_fields';
my $attr = $wire->{$key};
# A decoded response is a map of wire names and nothing else, so a key
# that is not one is a field we have not heard of -- even where it spells
# an entity attribute such as `client`. Those never come from the daemon:
# the resource API injects them as the %extra above, kept apart from $data
# on purpose, so a key of that name in the response cannot overwrite ours
# and is forwarded verbatim like any other unknown field (karr k104).
unless (defined $attr) {
$unknown{$key} = $data->{$key};
next;
}
my ($fits, $value) = _fits($reg->{$attr}, $data->{$key});
if ($fits) { $args{$attr} = $value; next }
# A value the model cannot use costs that one field, not the response it
# arrived in. It is kept under its wire name exactly as an unknown field
# is, and named in rejected_fields so the caller can tell it apart from a
# field the engine never sent (karr k83).
$unknown{$key} = $data->{$key};
$rejected{$key} = $attr;
}
# Resolved to Perl names already, so the BUILDARGS above is a no-op over
# them and stays idempotent across the two passes the `allOf` shape makes.
return $class->new(%args, %extra,
unknown_fields => \%unknown,
rejected_fields => \%rejected,
);
}
# Does a value an engine sent fit the attribute the wire name resolved to?
# Answers with the coerced value where it does. The coercion runs here rather
# than being left to the constructor because it is what turns a nested
# hashref into an object and what refuses a Bool that is neither -- both have
# to happen before the value can be judged at all. Every coercion the DSL
# builds is idempotent, so the constructor running it again on the result
# changes nothing.
#
# The leniency is this sub and its one caller. `new` never comes through
# here, so a caller who writes a value the swagger does not allow is still
# croaked at by Moo: that is a typo in a request, not an engine being itself.
sub _fits {
my ($info, $value) = @_;
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) = @_;
lib/API/Docker/Role/Type.pm view on Meta::CPAN
delete $ATTR_CACHE{$class};
delete $ORDER_CACHE{$class};
delete $WIRE_CACHE{$class};
delete $ENTITY_CACHE{$class};
return;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
API::Docker::Role::Type - Instance behaviour of a generated API::Docker::Type class
=head1 VERSION
version 0.004
=head1 SYNOPSIS
# composed automatically by `use API::Docker::Type;`
my $hc = API::Docker::Type::HostConfig->from_data($from_the_daemon);
my $wire = $hc->TO_JSON; # CamelCase keys, JSON booleans
my $bytes = $hc->to_json;
=head1 DESCRIPTION
Every class under C<API::Docker::Type::*> composes this role; it is applied
by L<API::Docker::Type>'s C<import>, so a generated class never names it.
The role reads the attribute registry L<API::Docker::Type> writes -- it never
walks the object's own keys. A field that is an attribute but not in the
registry is invisible here, which is exactly what
C<maint/spec-drift-check.pl> exists to catch.
=head2 The two entry points have different jobs
L</from_data> inflates an engine B<response>: its keys are the daemon's, so
only the registry's wire names are read and everything else is preserved
under the name it arrived with. L</new> assembles a B<request> out of what a
caller wrote: its keys are the caller's, so the Perl spelling is read first
and the wire spelling is an alias for it.
The split is what keeps the passthrough invariant true. Resolving a Perl name
on the response path renames the engine's data -- Docker's swagger spells 114
fields with a lowercase first letter, so a lowercase key off an engine is
ordinary rather than exotic, and reading C<id> as the Perl name of C<Id> both
loses the field it really was and rewrites one we did know (karr k85).
The same split decides what happens to a value that does not fit its declared
type. L</from_data> keeps it -- unset attribute, raw value in
L</unknown_fields>, name in L</rejected_fields> -- because one divergent field
must not make the rest of a usable response unreachable. L</new> croaks,
because there the value is the caller's and a mistake worth stopping on.
A nested hashref follows whichever entry point started the construction, so
one object graph is read one way throughout.
=head2 unknown_fields
A HashRef of everything that reached this object under a name the model could
not translate, kept under the name it arrived with and handed back out by
L</TO_JSON> unchanged. Two things land here: a name the registry does not know
at all, and -- on the response path only -- a known wire name whose value did
not fit the type the swagger declares for it. L</rejected_fields> is what
tells the two apart.
This is the whole reason a caller whose engine is newer than the swagger this
model was generated from still gets their field to the daemon. Translating
what we know and B<forwarding the rest verbatim> is worth more to this
distribution than a tidy model: a field the caller set must never be dropped
because we have not heard of it.
That promise covers the value as well as the name, C<undef> included: a name
the model does not know has no declared type, so there is no zero value we
could read a null as, and inventing one would be us deciding what the engine
meant. A B<known> field's null is the opposite case and is read as unset --
see L</"A null on a known field is read as unset">.
=head2 rejected_fields
A HashRef naming the fields this object could B<not> use, mapping the wire
name the value arrived under to the Perl attribute it would have filled.
It exists so that "the engine did not send this" and "the engine sent it and
the model could not use it" are two different observations. Both leave the
typed accessor C<undef>; only the second puts the field's wire name in here,
and the value itself in L</unknown_fields> beside it:
my $c = API::Docker::Type::ContainerInspectResponse->from_data({
Id => 'x', State => 'exited' }); # the swagger says State is an object
$c->state # undef
$c->rejected_fields->{State} # 'state' -- sent, and refused
$c->unknown_fields->{State} # 'exited' -- kept as it arrived
$c->TO_JSON->{State} # 'exited' -- and written back out
Only L</from_data> fills it. L</new> is strict and croaks instead, so an
object a caller built has an empty one.
=head2 new
my $hc = API::Docker::Type::HostConfig->new(privileged => 1);
my $hc = API::Docker::Type::HostConfig->new(Privileged => 1); # the same
Builds an object from data a B<caller> wrote, which is what a request payload
is. Keys are matched against the Perl attribute names first and the registry's
wire names second, so either spelling reaches the attribute; anything else is
kept in L</unknown_fields>, exactly as on the response path.
Two keys that resolve to one attribute -- C<privileged> and C<Privileged>
together -- are B<refused>. Which one won was decided by hash order and
nothing else, measured at nine zeroes and eleven ones over twenty
constructions of the same arguments. They are refused even where the two
values agree: values that happen to match are not the same thing as an
unambiguous call, and the caller should be shown the mistake rather than the
( run in 1.061 second using v1.01-cache-2.11-cpan-b301d465b3d )