Skip to content

Commit

Permalink
feat: implement typed function signature
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed May 16, 2023
1 parent 395181d commit 691c4e3
Show file tree
Hide file tree
Showing 11 changed files with 455 additions and 49 deletions.
2 changes: 1 addition & 1 deletion src/CloudEventFunctionWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class CloudEventFunctionWrapper extends FunctionWrapper
class CloudEventFunctionWrapper extends ValidatingFunctionWrapper
{
private const TYPE_LEGACY = 1;
private const TYPE_BINARY = 2;
Expand Down
50 changes: 3 additions & 47 deletions src/FunctionWrapper.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Copyright 2019 Google LLC.
* Copyright 2023 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,10 +17,9 @@

namespace Google\CloudFunctions;

use Closure;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Closure;
use LogicException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
Expand All @@ -33,20 +32,14 @@ abstract class FunctionWrapper

public function __construct(callable $function)
{
$this->validateFunctionSignature(
$this->getFunctionReflection($function)
);

$this->function = $function;
}

abstract public function execute(
ServerRequestInterface $request
): ResponseInterface;

abstract protected function getFunctionParameterClassName(): string;

private function getFunctionReflection(
protected function getFunctionReflection(
callable $function
): ReflectionFunctionAbstract {
if ($function instanceof Closure) {
Expand All @@ -64,41 +57,4 @@ private function getFunctionReflection(

return new ReflectionMethod($function, '__invoke');
}

private function validateFunctionSignature(
ReflectionFunctionAbstract $reflection
) {
$parameters = $reflection->getParameters();
$parametersCount = count($parameters);

// Check there is at least one parameter
if ($parametersCount === 0) {
$this->throwInvalidFunctionException();
}
// Check the first parameter has the proper typehint
$type = $parameters[0]->getType();
$class = $this->getFunctionParameterClassName();
if (!$type || $type->getName() !== $class) {
$this->throwInvalidFunctionException();
}

if ($parametersCount > 1) {
for ($i = 1; $i < $parametersCount; $i++) {
if (!$parameters[$i]->isOptional()) {
throw new LogicException(
'If your function accepts more than one parameter the '
. 'additional parameters must be optional'
);
}
}
}
}

private function throwInvalidFunctionException()
{
throw new LogicException(sprintf(
'Your function must have "%s" as the typehint for the first argument',
$this->getFunctionParameterClassName()
));
}
}
5 changes: 5 additions & 0 deletions src/FunctionsFramework.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ public static function cloudEvent(string $name, callable $fn)
{
Invoker::registerFunction($name, new CloudEventFunctionWrapper($fn, true));
}

public static function typed(string $name, callable $fn)
{
Invoker::registerFunction($name, new TypedFunctionWrapper($fn));
}
}
2 changes: 1 addition & 1 deletion src/HttpFunctionWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
use Psr\Http\Message\ServerRequestInterface;
use GuzzleHttp\Psr7\Response;

class HttpFunctionWrapper extends FunctionWrapper
class HttpFunctionWrapper extends ValidatingFunctionWrapper
{
public function execute(ServerRequestInterface $request): ResponseInterface
{
Expand Down
6 changes: 6 additions & 0 deletions src/Invoker.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public function __construct($target, ?string $signatureType = null)
|| $signatureType === 'cloudevent'
) {
$this->function = new CloudEventFunctionWrapper($target, false);
} elseif ($signatureType === 'typed') {
$this->function = new TypedFunctionWrapper($target);
} else {
throw new InvalidArgumentException(sprintf(
'Invalid signature type: "%s"',
Expand All @@ -82,6 +84,10 @@ public function handle(

try {
return $this->function->execute($request);
} catch (ParseError $e) {
// Log the full error and stack trace
($this->errorLogFunc)((string) $e);
return new Response(400, [], 'Bad Request');
} catch (Exception $e) {
// Log the full error and stack trace
($this->errorLogFunc)((string) $e);
Expand Down
121 changes: 121 additions & 0 deletions src/TypedFunctionWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/**
* Copyright 2023 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\CloudFunctions;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Closure;
use Exception;
use GuzzleHttp\Psr7\Response;
use LogicException;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;

class TypedFunctionWrapper extends FunctionWrapper
{
public const FUNCTION_STATUS_HEADER = 'X-Google-Status';

/** @var callable */
protected $function;
/** @var ReflectionClass */
protected $functionArgClass;

public function __construct(callable $function)
{
parent::__construct($function);

$this->validateFunctionSignature(
$this->getFunctionReflection($function)
);
}

public function execute(ServerRequestInterface $request): ResponseInterface
{
try {
$argInst = $this->functionArgClass->newInstance();
} catch (ReflectionException $e) {
throw new LogicException('Function request class cannot be instantiated');
}
$body = $request->getBody()->getContents();

try {
$argInst->mergeFromJsonString($body);
} catch (Exception $e) {
throw new ParseError($e->getMessage(), 0, $e);
}

$funcResult = call_user_func($this->function, $argInst);

if (!method_exists($funcResult, "serializeToJsonString")) {
throw new LogicException("Return type must implement 'serializeToJsonString'");
}
$resultJson = $funcResult->serializeToJsonString();

return new Response(200, ['content-type' => 'application/cloudevents+json'], $resultJson);
}

private function validateFunctionSignature(
ReflectionFunctionAbstract $reflection
) {
$parameters = $reflection->getParameters();
$parametersCount = count($parameters);

// Check there is at least one parameter
if ($parametersCount === 0) {
$this->throwInvalidFunctionException();
}

// Check the first parameter has the proper typehint
$type = $parameters[0]->getType();
if ($type == null) {
$this->throwInvalidFunctionException();
}

for ($i = 1; $i < $parametersCount; $i++) {
if (!$parameters[$i]->isOptional()) {
throw new LogicException(
'If your function accepts more than one parameter the '
. 'additional parameters must be optional'
);
}
}

try {
$this->functionArgClass = new ReflectionClass($type->getName());
} catch (ReflectionException $e) {
$name = $type->getName();
$message = $e->getMessage();
throw new LogicException("Could not find function parameter type $name, error: $message");
}
}

private function throwInvalidFunctionException()
{
throw new LogicException(
'Your function must declare exactly one required parameter that has a valid type hint'
);
}
}

class ParseError extends Exception
{
}
72 changes: 72 additions & 0 deletions src/ValidatingFunctionWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* Copyright 2019 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\CloudFunctions;

use LogicException;
use ReflectionFunctionAbstract;

abstract class ValidatingFunctionWrapper extends FunctionWrapper
{
public function __construct(callable $function)
{
parent::__construct($function);

$this->validateFunctionSignature(
$this->getFunctionReflection($function)
);
}

abstract protected function getFunctionParameterClassName(): string;

private function validateFunctionSignature(
ReflectionFunctionAbstract $reflection
) {
$parameters = $reflection->getParameters();
$parametersCount = count($parameters);

// Check there is at least one parameter
if ($parametersCount === 0) {
$this->throwInvalidFunctionException();
}
// Check the first parameter has the proper typehint
$type = $parameters[0]->getType();
$class = $this->getFunctionParameterClassName();
if (!$type || $type->getName() !== $class) {
$this->throwInvalidFunctionException();
}

if ($parametersCount > 1) {
for ($i = 1; $i < $parametersCount; $i++) {
if (!$parameters[$i]->isOptional()) {
throw new LogicException(
'If your function accepts more than one parameter the '
. 'additional parameters must be optional'
);
}
}
}
}

private function throwInvalidFunctionException()
{
throw new LogicException(sprintf(
'Your function must have "%s" as the typehint for the first argument',
$this->getFunctionParameterClassName()
));
}
}
17 changes: 17 additions & 0 deletions tests/FunctionsFrameworkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@

namespace Google\CloudFunctions\Tests;

require_once 'tests/common/types.php';

use Google\CloudFunctions\FunctionsFramework;
use Google\CloudFunctions\FunctionsFrameworkTesting;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use CloudEvents\V1\CloudEventInterface;
use Google\CloudFunctions\Tests\Common\IntValue;

/**
* @group gcf-framework
Expand Down Expand Up @@ -56,6 +59,20 @@ public function testRegisterAndRetrieveCloudEventFunction(): void
);
}

public function testRegisterAndRetrieveTypedFunction(): void
{
$fn = function (IntValue $event) {
return $event;
};

FunctionsFramework::typed('testFn', $fn);

$this->assertEquals(
$fn,
FunctionsFrameworkTesting::getRegisteredFunction('testFn')
);
}

public function testRetrieveNonexistantFunction(): void
{
$this->assertNull(
Expand Down
Loading

0 comments on commit 691c4e3

Please sign in to comment.