API-Docker

 view release on metacpan or  search on metacpan

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

package API::Docker::Role::Filters;
# ABSTRACT: The filters query parameter, normalised into the one shape the engine reads
our $VERSION = '0.004';
use Moo::Role;
use Carp qw( croak );
use namespace::clean;


# The boolean classes JSON::MaybeXS hands back across its backends. Named
# rather than duck-typed: a blessed object that merely overloads bool is not
# a claim of being a JSON boolean.
my %BOOLEAN_CLASS = map { $_ => 1 } qw(
  JSON::PP::Boolean
  Types::Serialiser::Boolean
);

sub _normalise_filters {
  my ($self, $filters) = @_;

  croak __PACKAGE__ . '->_normalise_filters filters must be a HashRef of '
    . 'filter name to value, e.g. { dangling => [\'true\'] }'
    unless ref $filters eq 'HASH';

  my %normalised;
  for my $name (sort keys %$filters) {
    croak __PACKAGE__ . '->_normalise_filters filter name must not be empty'
      unless length $name;
    my $value = $filters->{$name};
    # A bare value is one value, not a mistake worth refusing -- the engine
    # is the one that insists on the list.
    my @values = ref $value eq 'ARRAY' ? @$value : ($value);
    $normalised{$name} = [ map { $self->_normalise_filter_value($name, $_) } @values ];
  }

  return \%normalised;
}

sub _normalise_filter_value {
  my ($self, $name, $value) = @_;

  my $where = __PACKAGE__ . '->_normalise_filters filter \'' . $name . '\' ';

  croak $where . 'has an undefined value; the engine reads a JSON null into '
    . 'a string as the empty string and rejects it there'
    unless defined $value;

  my $ref = ref $value;
  return $value ? 'true' : 'false' if $BOOLEAN_CLASS{$ref};

  if ($ref eq 'SCALAR') {
    croak $where . 'is a ScalarRef to something other than 1 or 0; \\1 and '
      . '\\0 are read as the booleans \'true\' and \'false\''
      unless $$value eq '1' || $$value eq '0';
    return $$value ? 'true' : 'false';
  }

  croak $where . 'has a ' . $ref . ' reference as a value; filter values are '
    . 'strings, or an ArrayRef of them' if $ref;

  croak $where . 'has an empty value; the engine rejects it. A Perl boolean '
    . 'stringifies to \'\' when false -- the engine wants the string '
    . '\'false\''
    unless length $value;

  # Stringify a copy: a scalar carrying a number would otherwise be
  # JSON-encoded as one, and the engine's filter values are strings.
  return "$value";
}


1;

__END__

=pod

=encoding UTF-8

=head1 NAME

API::Docker::Role::Filters - The filters query parameter, normalised into the one shape the engine reads

=head1 VERSION

version 0.004

=head1 SYNOPSIS

    package API::Docker::API::Whatever;
    use Moo;
    with 'API::Docker::Role::Filters';

    sub list {
      my ($self, %opts) = @_;
      my %params;
      $params{filters} = $self->_normalise_filters($opts{filters})
        if defined $opts{filters};
      return $self->client->get('/whatever', params => \%params);
    }

=head1 DESCRIPTION

Every C<list> and C<prune> endpoint of the Engine API takes a C<filters>
query parameter, and every one of them wants the same thing: a JSON B<map of
string to array of string>.

    filters => { dangling => ['true'] }        correct
    filters => { dangling => 'true'   }        wrong -- not an array
    filters => { dangling => 1        }        wrong -- not an array
    filters => { dangling => [1]      }        wrong -- a number, not a string
    filters => { dangling => [\1]     }        wrong -- a JSON boolean

The transport JSON-encodes a HashRef C<params> value on its own, so the
I<encoding> was never the problem. The I<shape> is, and it is the thing
clients get wrong, because Perl has no notion of "array of string" and a
HashRef literal will happily hold whatever the caller typed.

This role normalises that shape in one place, so the twelve methods that
accept C<filters> agree on it and document it by pointing here.

=head2 What it does

=over



( run in 0.933 second using v1.01-cache-2.11-cpan-54e63673c56 )