Group
Extension

JMAP-Tester-WebSocket/lib/JMAP/Tester/WebSocket.pm

use v5.10.0;
use warnings;

package JMAP::Tester::WebSocket 0.004;
# ABSTRACT: a WebSocket JMAP client made for testing JMAP servers

use Moo;
use IO::Async::Loop;
use Net::Async::WebSocket::Client 0.13;
use Protocol::WebSocket::Request;
use Params::Util qw(_HASH0 _ARRAY0);
use Data::Dumper;
use Scalar::Util qw(weaken);
use Try::Tiny;

use JMAP::Tester::WebSocket::Response;
use JMAP::Tester::WebSocket::Result::Failure;

extends qw(JMAP::Tester);

has +json_codec => (
  is => 'bare',
  handles => {
    json_encode => 'encode',
    json_decode => 'decode',
  },
  default => sub {
    require JSON;

    # Not ->utf8-> or we die decoding things with "wide character"...
    # Maybe to be fixed in Protocol::WebSocket? Or IO::Async is doing this
    # for us?
    return JSON->new->convert_blessed;
  },
);


has 'ws_api_uri' => (
  is        => 'rw',
  required  => 1,
);

has cache_connection => (
  is      => 'ro',
  default => 0,
);

has 'authorization' => (
  is        => 'rw',
  predicate => 'has_authorization',
);

has _cached_client => (
  is => 'rw',
);

has loop => (
  is      => 'rw',
  default => sub { IO::Async::Loop->new }, 
);

sub request {
  my ($self, $input_request) = @_;

  state $ident = 'a';
  my %seen;
  my @suffixed;

  my %default_args = %{ $self->default_arguments };

  my $request = _ARRAY0($input_request)
              ? { methodCalls => $input_request }
              : { %$input_request };

  for my $call (@{ $request->{methodCalls} }) {
    my $copy = [ @$call ];
    if (defined $copy->[2]) {
      $seen{$call->[2]}++;
    } else {
      my $next;
      do { $next = $ident++ } until ! $seen{$ident}++;
      $copy->[2] = $next;
    }

    my %arg = (
      %default_args,
      %{ $copy->[1] // {} },
    );

    for my $key (keys %arg) {
      if ( ref $arg{$key}
        && ref $arg{$key} eq 'SCALAR'
        && ! defined ${ $arg{$key} }
      ) {
        delete $arg{$key};
      }
    }

    $copy->[1] = \%arg;

    push @suffixed, $copy;
  }

  $request->{methodCalls} = \@suffixed;

  $request = $request->{methodCalls}
    if $ENV{JMAP_TESTER_NO_WRAPPER} && _ARRAY0($input_request);

  if ($self->_has_default_using && ! exists $request->{using}) {
    $request->{using} = $self->default_using;
  }

  # Required by RFC 8887
  $request->{'@type'} = 'Request';

  my $json = $self->json_encode($request);

  my $client = $self->_cached_client || $self->connect_ws;

  $client->send_text_frame($json);

  my $res = $self->loop->run;

  unless ($self->_cached_client) {
    $self->loop->remove($client);
  }

  return $self->_jresponse_from_wsresponse($res);
}

sub connect_ws {
  my ($self) = @_;

  my $loop = $self->loop;

  weaken($loop);

  my $client = Net::Async::WebSocket::Client->new(
    on_text_frame => sub {
      my ($c, $f) = @_;

      $loop->stop($f);
    },
  );

  $client->{framebuffer} = Protocol::WebSocket::Frame->new(
    max_payload_size => 0
  );

  $self->loop->add($client);

  $client->connect(
    url => $self->ws_api_uri,
    req => Protocol::WebSocket::Request->new(
      headers => [
        ( $self->authorization
          ? ( Authorization => $self->authorization ) 
          : ()
        ),
      ],
      subprotocol => 'jmap',
    ),
  )->get;

  if ($self->cache_connection) {
    $self->_cached_client($client);
  }

  return $client;
}

sub _jresponse_from_wsresponse {
  my ($self, $ws_res) = @_;

  my ($data, $error);

  try {
    $data = $self->apply_json_types($self->json_decode( $ws_res ));
  } catch {
    $error = $_;
  };

  if (defined $error) {
    return JMAP::Tester::WebSocket::Result::Failure->new(
      ws_response => $ws_res,
      ident => $error,
    );
  }

  my ($items, $props);
  if (_HASH0($data)) {
    $props = $data;
    $items = $props->{methodResponses};
  } elsif (_ARRAY0($data)) {
    $props = {};
    $items = $data;
  } else {
    abort("illegal response to JMAP request: $data");
  }

  return JMAP::Tester::WebSocket::Response->new({
    items               => $items,
    ws_response         => $ws_res,
    wrapper_properties  => $props,
  });
}

1;

=pod

=encoding UTF-8

=head1 NAME

JMAP::Tester::WebSocket - a WebSocket JMAP client made for testing JMAP servers

=head1 VERSION

version 0.004

=head1 SYNOPSIS

  use JMAP::Tester::WebSocket;

  my $jtest = JMAP::Tester::WebSocket->new({
    ws_uri => 'ws://jmap.local/account/123',
  });

  my $response = $jtest->request([
    [ getMailboxes => {} ],
    [ getMessageUpdates => { sinceState => "123" } ],
  ]);

=head1 DESCRIPTION

This module provides a WebSockets wrapper around L<JMAP::Tester>.

See L<JMAP::Tester> for more information.

=head1 SEE ALSO

L<JMAP::Tester> - a JMAP client made for testing JMAP servers

=head1 AUTHOR

Matthew Horsfall <wolfsage@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2018 by FastMail, Ltd.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut

__END__

#pod =head1 SYNOPSIS
#pod
#pod   use JMAP::Tester::WebSocket;
#pod
#pod   my $jtest = JMAP::Tester::WebSocket->new({
#pod     ws_uri => 'ws://jmap.local/account/123',
#pod   });
#pod
#pod   my $response = $jtest->request([
#pod     [ getMailboxes => {} ],
#pod     [ getMessageUpdates => { sinceState => "123" } ],
#pod   ]);
#pod
#pod =head1 DESCRIPTION
#pod
#pod This module provides a WebSockets wrapper around L<JMAP::Tester>.
#pod
#pod See L<JMAP::Tester> for more information.
#pod
#pod =head1 SEE ALSO
#pod
#pod L<JMAP::Tester> - a JMAP client made for testing JMAP servers
#pod
#pod =cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.