Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support new surface operations clients #569

Merged
merged 14 commits into from
May 29, 2024
Merged
2 changes: 1 addition & 1 deletion src/GapicClientTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ private function createCallStack(array $callConstructionOptions)
'X-Goog-User-Project' => [$quotaProject]
];
}

if (isset($this->apiVersion)) {
$fixedHeaders += [
'X-Goog-Api-Version' => [$this->apiVersion]
Expand Down
60 changes: 43 additions & 17 deletions src/OperationResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
namespace Google\ApiCore;

use Google\LongRunning\Client\OperationsClient;
use Google\LongRunning\OperationsClient as LegacyOperationsClient;
use Google\LongRunning\CancelOperationRequest;
use Google\LongRunning\DeleteOperationRequest;
use Google\LongRunning\GetOperationRequest;
Expand Down Expand Up @@ -64,6 +65,7 @@ class OperationResponse
const DEFAULT_POLLING_MULTIPLIER = 2;
const DEFAULT_MAX_POLLING_INTERVAL = 60000;
const DEFAULT_MAX_POLLING_DURATION = 0;
private const NEW_CLIENT_NAMESPACE = '\\Client\\';

private string $operationName;
private ?object $operationsClient;
Expand All @@ -84,6 +86,9 @@ class OperationResponse
private string $getOperationMethod;
private ?string $cancelOperationMethod;
private ?string $deleteOperationMethod;
private string $getOperationRequest;
private ?string $cancelOperationRequest;
private ?string $deleteOperationRequest;
private string $operationStatusMethod;
/** @var mixed */
private $operationStatusDoneValue;
Expand Down Expand Up @@ -131,6 +136,9 @@ public function __construct(string $operationName, $operationsClient, array $opt
'additionalOperationArguments' => [],
'operationErrorCodeMethod' => null,
'operationErrorMessageMethod' => null,
'getOperationRequest' => GetOperationRequest::class,
'cancelOperationRequest' => CancelOperationRequest::class,
'deleteOperationRequest' => DeleteOperationRequest::class,
];
$this->operationReturnType = $options['operationReturnType'];
$this->metadataReturnType = $options['metadataReturnType'];
Expand All @@ -143,6 +151,9 @@ public function __construct(string $operationName, $operationsClient, array $opt
$this->operationStatusDoneValue = $options['operationStatusDoneValue'];
$this->operationErrorCodeMethod = $options['operationErrorCodeMethod'];
$this->operationErrorMessageMethod = $options['operationErrorMessageMethod'];
$this->getOperationRequest = $options['getOperationRequest'];
$this->cancelOperationRequest = $options['cancelOperationRequest'];
$this->deleteOperationRequest = $options['deleteOperationRequest'];

if (isset($options['initialPollDelayMillis'])) {
$this->defaultPollSettings['initialPollDelayMillis'] = $options['initialPollDelayMillis'];
Expand Down Expand Up @@ -263,10 +274,9 @@ public function reload()
if ($this->deleted) {
throw new ValidationException("Cannot call reload() on a deleted operation");
}
$this->lastProtoResponse = $this->operationsCall(
$this->getOperationMethod,
GetOperationRequest::class
);

$requestClass = $this->isNewSurfaceOperationsClient() ? $this->getOperationRequest : null;
$this->lastProtoResponse = $this->operationsCall($this->getOperationMethod, $requestClass);
}

/**
Expand Down Expand Up @@ -389,10 +399,9 @@ public function cancel()
if (is_null($this->cancelOperationMethod)) {
throw new LogicException('The cancel operation is not supported by this API');
}
$this->operationsCall(
$this->cancelOperationMethod,
CancelOperationRequest::class
);

$requestClass = $this->isNewSurfaceOperationsClient() ? $this->cancelOperationRequest : null;
$this->operationsCall($this->cancelOperationMethod, $requestClass);
}

/**
Expand All @@ -411,10 +420,9 @@ public function delete()
if (is_null($this->deleteOperationMethod)) {
throw new LogicException('The delete operation is not supported by this API');
}
$this->operationsCall(
$this->deleteOperationMethod,
DeleteOperationRequest::class
);

$requestClass = $this->isNewSurfaceOperationsClient() ? $this->deleteOperationRequest : null;
$this->operationsCall($this->deleteOperationMethod, $requestClass);
$this->deleted = true;
}

Expand Down Expand Up @@ -456,12 +464,24 @@ public function getMetadata()
return $metadata;
}

private function operationsCall(string $method, string $requestClass)
/**
* Call the operations client to perform an operation.
*
* @param string $method The method to call on the operations client.
* @param string|null $requestClass The request class to use for the call.
* Will be null for legacy operations clients.
*/
private function operationsCall(string $method, ?string $requestClass)
{
$firstArgument = $this->operationsClient instanceof OperationsClient
? $requestClass::build($this->getName())
: $this->getName();
$args = array_merge([$firstArgument], $this->additionalArgs);
$args = array_merge([$this->getName()], array_values($this->additionalArgs));
if ($requestClass) {
if (!method_exists($requestClass, 'build')) {
throw new LogicException('Request class must support the static build method');
}
$request = call_user_func_array($requestClass . '::build', $args);
$args = [$request];
}

return call_user_func_array([$this->operationsClient, $method], $args);
}

Expand Down Expand Up @@ -495,4 +515,10 @@ private function hasProtoResponse()
{
return !is_null($this->lastProtoResponse);
}

private function isNewSurfaceOperationsClient(): bool
{
return !$this->operationsClient instanceof LegacyOperationsClient
&& false !== strpos(get_class($this->operationsClient), self::NEW_CLIENT_NAMESPACE);
}
}
77 changes: 77 additions & 0 deletions tests/Tests/Unit/OperationResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,83 @@ public function testCustomOperation()
$operationResponse->delete();
}

public function testNewSurfaceCustomOperation()
{
// This mock requires a specific namespace, so it must be defined in a separate file
require_once __DIR__ . '/testdata/src/CustomOperationClient.php';

$phpunit = $this;
$operationName = 'test-123';
$operationClient = $this->prophesize(Client\NewSurfaceCustomOperationClient::class);
$operationClient->getNewSurfaceOperation(Argument::type(Client\GetOperationRequest::class))
->shouldBeCalledOnce()
->will(function ($args) use ($phpunit) {
list($request) = $args;
$phpunit->assertEquals('test-123', $request->name);
$phpunit->assertEquals('arg2', $request->arg2);
$phpunit->assertEquals('arg3', $request->arg3);
return new \stdClass;
});
$operationClient->cancelNewSurfaceOperation(Argument::type(Client\CancelOperationRequest::class))
->shouldBeCalledOnce()
->will(function ($args) use ($phpunit) {
list($request) = $args;
$phpunit->assertEquals('test-123', $request->name);
$phpunit->assertEquals('arg2', $request->arg2);
$phpunit->assertEquals('arg3', $request->arg3);
return true;
});
$operationClient->deleteNewSurfaceOperation(Argument::type(Client\DeleteOperationRequest::class))
->shouldBeCalledOnce()
->will(function ($args) use ($phpunit) {
list($request) = $args;
$phpunit->assertEquals('test-123', $request->name);
$phpunit->assertEquals('arg2', $request->arg2);
$phpunit->assertEquals('arg3', $request->arg3);
return true;
});
$options = [
'getOperationMethod' => 'getNewSurfaceOperation',
'cancelOperationMethod' => 'cancelNewSurfaceOperation',
'deleteOperationMethod' => 'deleteNewSurfaceOperation',
'additionalOperationArguments' => [
'setArgumentTwo' => 'arg2',
'setArgumentThree' => 'arg3'
],
'getOperationRequest' => Client\GetOperationRequest::class,
'cancelOperationRequest' => Client\CancelOperationRequest::class,
'deleteOperationRequest' => Client\DeleteOperationRequest::class,
];
$operationResponse = new OperationResponse($operationName, $operationClient->reveal(), $options);

// Test getOperationMethod
$operationResponse->reload();

// test cancelOperationMethod
$operationResponse->cancel();

// test deleteOperationMethod
$operationResponse->delete();
}

public function testRequestClassWithoutBuildThrowsException()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Request class must support the static build method');

// This mock requires a specific namespace, so it must be defined in a separate file
require_once __DIR__ . '/testdata/src/CustomOperationClient.php';

$operationClient = $this->prophesize(Client\NewSurfaceCustomOperationClient::class);
$options = [
'getOperationRequest' => \stdClass::class, // a class that does not have a "build" method.
];
$operationResponse = new OperationResponse('test-123', $operationClient->reveal(), $options);

// Test getOperationMethod
$operationResponse->reload();
}

/**
* @dataProvider provideOperationsClients
*/
Expand Down
52 changes: 52 additions & 0 deletions tests/Tests/Unit/testdata/src/CustomOperationClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

// "Client" in the namespace tells OperationResponse that this is a new surface client
namespace Google\ApiCore\Tests\Unit\Client;

class NewSurfaceCustomOperationClient
{
public function getNewSurfaceOperation(GetOperationRequest $request, array $callOptions = [])
{

}

public function cancelNewSurfaceOperation(CancelOperationRequest $request, array $callOptions = [])
{

}

public function deleteNewSurfaceOperation(DeleteOperationRequest $request, array $callOptions = [])
{

}
}

abstract class BaseOperationRequest
{
public string $name;
public string $arg2;
public string $arg3;

public static function build(string $name, string $arg2, string $arg3): static
{
$request = new static();
$request->name = $name;
$request->arg2 = $arg2;
$request->arg3 = $arg3;

return $request;
}
}

class GetOperationRequest extends BaseOperationRequest
{
}

class CancelOperationRequest extends BaseOperationRequest
{
}

class DeleteOperationRequest extends BaseOperationRequest
{
}