Skip to content

Commit

Permalink
feat: implement typed function signature (#141)
Browse files Browse the repository at this point in the history
* feat: implement typed function signature

* Address initial code review feedback

* Address code review feedback
  • Loading branch information
garethgeorge authored Jun 8, 2023
1 parent bd0d24c commit a925ca4
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 62 deletions.
30 changes: 30 additions & 0 deletions src/BadRequestError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?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 Exception;

/**
* BadRequestError indicates a malformatted request. This is thrown internally
* when a typed function cannot deserialize the request body.
* @internal
*/
class BadRequestError extends Exception
{
}
35 changes: 30 additions & 5 deletions src/CloudEventFunctionWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@

use CloudEvents\V1\CloudEventInterface;
use GuzzleHttp\Psr7\Response;
use LogicException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionParameter;

class CloudEventFunctionWrapper extends FunctionWrapper
{
use FunctionValidationTrait;

private const TYPE_LEGACY = 1;
private const TYPE_BINARY = 2;
private const TYPE_STRUCTURED = 3;
Expand All @@ -50,6 +54,32 @@ public function __construct(callable $function, bool $marshalToCloudEventInterfa
{
$this->marshalToCloudEventInterface = $marshalToCloudEventInterface;
parent::__construct($function);

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

private function throwInvalidFirstParameterException(): void
{
throw new LogicException(sprintf(
'Your function must have "%s" as the typehint for the first argument',
$this->getFunctionParameterClassName()
));
}

private function validateFirstParameter(ReflectionParameter $param): void
{
$type = $param->getType();
$class = $this->getFunctionParameterClassName();
if (!$type || $type->getName() !== $class) {
$this->throwInvalidFirstParameterException();
}
}

private function getFunctionParameterClassName(): string
{
return $this->marshalToCloudEventInterface ? CloudEventInterface::class : CloudEvent::class;
}

public function execute(ServerRequestInterface $request): ResponseInterface
Expand Down Expand Up @@ -145,9 +175,4 @@ private function fromBinaryRequest(

return CloudEvent::fromArray($content);
}

protected function getFunctionParameterClassName(): string
{
return $this->marshalToCloudEventInterface ? CloudEventInterface::class : CloudEvent::class;
}
}
54 changes: 54 additions & 0 deletions src/FunctionValidationTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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 LogicException;
use ReflectionFunctionAbstract;
use ReflectionParameter;

trait FunctionValidationTrait
{
abstract public function validateFirstParameter(ReflectionParameter $param): void;

/**
* @throws LogicException
*/
abstract public function throwInvalidFirstParameterException(): void;

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

if ($parametersCount === 0) {
$this->throwInvalidFirstParameterException();
}

$this->validateFirstParameter($parameters[0]);

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'
);
}
}
}
}
48 changes: 2 additions & 46 deletions src/FunctionWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
33 changes: 28 additions & 5 deletions src/HttpFunctionWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,37 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use GuzzleHttp\Psr7\Response;
use ReflectionParameter;

class HttpFunctionWrapper extends FunctionWrapper
{
use FunctionValidationTrait;

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

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

private function throwInvalidFirstParameterException(): void
{
throw new LogicException(sprintf(
'Your function must have "%s" as the typehint for the first argument',
ServerRequestInterface::class
));
}

private function validateFirstParameter(ReflectionParameter $param): void
{
$type = $param->getType();
if (!$type || $type->getName() !== ServerRequestInterface::class) {
$this->throwInvalidFirstParameterException();
}
}

public function execute(ServerRequestInterface $request): ResponseInterface
{
$path = $request->getUri()->getPath();
Expand All @@ -43,9 +71,4 @@ public function execute(ServerRequestInterface $request): ResponseInterface
'Function response must be string or ' . ResponseInterface::class
);
}

protected function getFunctionParameterClassName(): string
{
return ServerRequestInterface::class;
}
}
11 changes: 11 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 @@ -73,6 +75,11 @@ public function __construct($target, ?string $signatureType = null)
};
}

public function setErrorLogger(callable $logFunc)
{
$this->errorLogFunc = $logFunc;
}

public function handle(
ServerRequestInterface $request = null
): ResponseInterface {
Expand All @@ -82,6 +89,10 @@ public function handle(

try {
return $this->function->execute($request);
} catch (BadRequestError $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
Loading

0 comments on commit a925ca4

Please sign in to comment.