Skip to content

Commit 7e84ad4

Browse files
authored
feat: introduce declarative function signatures (#116)
1 parent 76cb91b commit 7e84ad4

12 files changed

+300
-35
lines changed

.github/workflows/conformance.yml

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,48 @@ jobs:
3636
go-version: '1.15'
3737

3838
- name: Run HTTP conformance tests
39-
uses: GoogleCloudPlatform/functions-framework-conformance/[email protected].0
39+
uses: GoogleCloudPlatform/functions-framework-conformance/[email protected].1
4040
env:
4141
FUNCTION_TARGET: 'httpFunc'
4242
FUNCTION_SIGNATURE_TYPE: 'http'
4343
FUNCTION_SOURCE: ${{ github.workspace }}/tests/conformance/index.php
4444
with:
45-
version: 'v1.0.0'
45+
version: 'v1.2.1'
46+
functionType: 'http'
47+
useBuildpacks: false
48+
cmd: "'php -S localhost:8080 router.php'"
49+
50+
- name: Run Declarative HTTP conformance tests
51+
uses: GoogleCloudPlatform/functions-framework-conformance/[email protected]
52+
env:
53+
FUNCTION_TARGET: 'declarativeHttpFunc'
54+
FUNCTION_SOURCE: ${{ github.workspace }}/tests/conformance/index.php
55+
with:
56+
version: 'v1.2.1'
4657
functionType: 'http'
4758
useBuildpacks: false
4859
cmd: "'php -S localhost:8080 router.php'"
4960

5061
- name: Run CloudEvent conformance tests
51-
uses: GoogleCloudPlatform/functions-framework-conformance/[email protected].0
62+
uses: GoogleCloudPlatform/functions-framework-conformance/[email protected].1
5263
env:
5364
FUNCTION_TARGET: 'cloudEventFunc'
5465
FUNCTION_SIGNATURE_TYPE: 'cloudevent'
5566
FUNCTION_SOURCE: ${{ github.workspace }}/tests/conformance/index.php
5667
with:
57-
version: 'v1.0.0'
68+
version: 'v1.2.1'
69+
functionType: 'cloudevent'
70+
useBuildpacks: false
71+
validateMapping: true
72+
cmd: "'php -S localhost:8080 router.php'"
73+
74+
- name: Run Declarative CloudEvent conformance tests
75+
uses: GoogleCloudPlatform/functions-framework-conformance/[email protected]
76+
env:
77+
FUNCTION_TARGET: 'declarativeCloudEventFunc'
78+
FUNCTION_SOURCE: ${{ github.workspace }}/tests/conformance/index.php
79+
with:
80+
version: 'v1.2.1'
5881
functionType: 'cloudevent'
5982
useBuildpacks: false
6083
validateMapping: true

router.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
/**
34
* Copyright 2019 Google LLC.
45
*
@@ -49,12 +50,6 @@
4950
if (false === $target) {
5051
throw new RuntimeException('FUNCTION_TARGET is not set');
5152
}
52-
if (!is_callable($target)) {
53-
throw new InvalidArgumentException(sprintf(
54-
'Function target is not callable: "%s"',
55-
$target
56-
));
57-
}
5853

5954
$signatureType = getenv('FUNCTION_SIGNATURE_TYPE', true) ?: 'http';
6055

run_conformance_tests.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/bin/bash
2+
# Runs the conformance tests locally from https://github.com/GoogleCloudPlatform/functions-framework-conformance.
3+
# Requires Go 1.16+ to run.
4+
#
5+
# Servers may fail to shutdown between tests on error, leaving port 8080 bound.
6+
# You can see what's running on port 8080 by running `lsof -i :8080`. You can
7+
# run `kill -9 <PID>` to terminate a process.
8+
#
9+
# USAGE:
10+
# ./run_conformance_tests.sh [client_version]
11+
#
12+
# client_version (optional):
13+
# The version of the conformance tests client to use, formatted as "vX.X.X".
14+
# Defaults to the latest version of the repo, which may be ahead of the
15+
# latest release.
16+
17+
CLIENT_VERSION=$1
18+
if [ $CLIENT_VERSION ]; then
19+
CLIENT_VERSION="@$CLIENT_VERSION"
20+
else
21+
echo "Defaulting to latest client."
22+
echo "Use './run_conformance_tests vX.X.X' to specify a specific release version."
23+
CLIENT_VERSION="@latest"
24+
fi
25+
26+
function print_header() {
27+
echo
28+
echo "========== $1 =========="
29+
}
30+
31+
# Fail if any command fails
32+
set -e
33+
34+
print_header "INSTALLING CLIENT$CLIENT_VERSION"
35+
echo "Note: only works with Go 1.16+ by default, see run_conformance_tests.sh for more information."
36+
# Go install @version only works on go 1.16+, if using a lower Go version
37+
# replace command with:
38+
# go get github.com/GoogleCloudPlatform/functions-framework-conformance/client$CLIENT_VERSION && go install github.com/GoogleCloudPlatform/functions-framework-conformance/client
39+
go install github.com/GoogleCloudPlatform/functions-framework-conformance/client$CLIENT_VERSION
40+
echo "Done installing client$CLIENT_VERSION"
41+
42+
print_header "HTTP CONFORMANCE TESTS"
43+
FUNCTION_TARGET='httpFunc' FUNCTION_SIGNATURE_TYPE='http' FUNCTION_SOURCE=$(realpath tests/conformance/index.php) client -buildpacks=false -type=http -cmd='php -S localhost:8080 router.php' -start-delay 5 -validate-mapping=true
44+
45+
print_header "DECLARATIVE HTTP CONFORMANCE TESTS"
46+
FUNCTION_TARGET='declarativeHttpFunc' FUNCTION_SIGNATURE_TYPE= FUNCTION_SOURCE=$(realpath tests/conformance/index.php) client -buildpacks=false -type=http -cmd='php -S localhost:8080 router.php' -start-delay 5 -validate-mapping=true
47+
48+
print_header "CLOUDEVENT CONFORMANCE TESTS"
49+
FUNCTION_TARGET='cloudEventFunc' FUNCTION_SIGNATURE_TYPE='cloudevent' FUNCTION_SOURCE=$(realpath tests/conformance/index.php) client -buildpacks=false -type=cloudevent -cmd='php -S localhost:8080 router.php' -start-delay 5 -validate-mapping=true
50+
51+
print_header "DECLARATIVE CLOUDEVENT CONFORMANCE TESTS"
52+
FUNCTION_TARGET='declarativeCloudEventFunc' FUNCTION_SIGNATURE_TYPE= FUNCTION_SOURCE=$(realpath tests/conformance/index.php) client -buildpacks=false -type=cloudevent -cmd='php -S localhost:8080 router.php' -start-delay 5 -validate-mapping=true

src/CloudEventFunctionWrapper.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
/**
34
* Copyright 2020 Google LLC.
45
*
@@ -17,6 +18,7 @@
1718

1819
namespace Google\CloudFunctions;
1920

21+
use CloudEvents\V1\CloudEventInterface;
2022
use GuzzleHttp\Psr7\Response;
2123
use Psr\Http\Message\ResponseInterface;
2224
use Psr\Http\Message\ServerRequestInterface;
@@ -27,6 +29,8 @@ class CloudEventFunctionWrapper extends FunctionWrapper
2729
private const TYPE_BINARY = 2;
2830
private const TYPE_STRUCTURED = 3;
2931

32+
private bool $marshalToCloudEventInterface;
33+
3034
// These are CloudEvent context attribute names that map to binary mode
3135
// HTTP headers when prefixed with 'ce-'. 'datacontenttype' is notably absent
3236
// from this list because the header 'ce-datacontenttype' is not permitted;
@@ -42,6 +46,12 @@ class CloudEventFunctionWrapper extends FunctionWrapper
4246
'time'
4347
];
4448

49+
public function __construct(callable $function, bool $marshalToCloudEventInterface = false)
50+
{
51+
$this->marshalToCloudEventInterface = $marshalToCloudEventInterface;
52+
parent::__construct($function);
53+
}
54+
4555
public function execute(ServerRequestInterface $request): ResponseInterface
4656
{
4757
$body = (string) $request->getBody();
@@ -89,6 +99,10 @@ public function execute(ServerRequestInterface $request): ResponseInterface
8999
], 'invalid event type');
90100
}
91101

102+
if ($this->marshalToCloudEventInterface) {
103+
$cloudevent = new CloudEventSdkCompliant($cloudevent);
104+
}
105+
92106
call_user_func($this->function, $cloudevent);
93107
return new Response();
94108
}
@@ -134,6 +148,6 @@ private function fromBinaryRequest(
134148

135149
protected function getFunctionParameterClassName(): string
136150
{
137-
return CloudEvent::class;
151+
return $this->marshalToCloudEventInterface ? CloudEventInterface::class : CloudEvent::class;
138152
}
139153
}

src/CloudEventSdkCompliant.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ public function getSubject(): ?string
6969
}
7070
public function getTime(): ?DateTimeImmutable
7171
{
72-
return DateTimeImmutable::createFromFormat(DateTimeInterface::RFC3339_EXTENDED, $this->cloudevent->getTime());
72+
$time = DateTimeImmutable::createFromFormat(DateTimeInterface::RFC3339_EXTENDED, $this->cloudevent->getTime());
73+
if ($time === false) {
74+
return null;
75+
}
76+
return $time;
7377
}
7478
public function getExtension(string $attribute)
7579
{

src/FunctionWrapper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ abstract class FunctionWrapper
3131

3232
protected $function;
3333

34-
public function __construct(callable $function, array $signature = null)
34+
public function __construct(callable $function)
3535
{
3636
$this->validateFunctionSignature(
3737
$this->getFunctionReflection($function)

src/FunctionsFramework.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/**
4+
* Copyright 2021 Google LLC.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
namespace Google\CloudFunctions;
20+
21+
class FunctionsFramework
22+
{
23+
private function __construct()
24+
{
25+
// Constructor disabled because this class should only be used statically.
26+
}
27+
28+
public static function http(string $name, callable $fn)
29+
{
30+
Invoker::registerFunction($name, new HttpFunctionWrapper($fn));
31+
}
32+
33+
public static function cloudEvent(string $name, callable $fn)
34+
{
35+
Invoker::registerFunction($name, new CloudEventFunctionWrapper($fn, true));
36+
}
37+
}

src/HttpFunctionWrapper.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@
2424

2525
class HttpFunctionWrapper extends FunctionWrapper
2626
{
27-
public function __construct(callable $function)
28-
{
29-
parent::__construct($function);
30-
}
31-
3227
public function execute(ServerRequestInterface $request): ResponseInterface
3328
{
3429
$path = $request->getUri()->getPath();

src/Invoker.php

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
/**
34
* Copyright 2019 Google LLC.
45
*
@@ -28,31 +29,45 @@ class Invoker
2829
{
2930
private $function;
3031
private $errorLogFunc;
32+
private static $registeredFunction = [];
3133

3234
/**
33-
* @param callable $target The callable to be invoked
34-
* @param string $signatureType The signature type of the target callable,
35-
* either "event" or "http".
35+
* @param array|string $target The callable to be invoked.
36+
* @param string|null $signatureType The signature type of the target callable,
37+
* either "cloudevent" or "http". If null,
38+
* env var `FUNCTION_SIGNATURE_TYPE` is used.
3639
*/
37-
public function __construct(callable $target, string $signatureType)
40+
public function __construct($target, ?string $signatureType = null)
3841
{
39-
if ($signatureType === 'http') {
40-
$this->function = new HttpFunctionWrapper($target);
41-
} elseif (
42-
$signatureType === 'event'
43-
|| $signatureType === 'cloudevent'
44-
) {
45-
$this->function = new CloudEventFunctionWrapper($target);
42+
if (is_string($target) && isset(self::$registeredFunction[$target])) {
43+
$this->function = self::$registeredFunction[$target];
4644
} else {
47-
throw new InvalidArgumentException(sprintf(
48-
'Invalid signature type: "%s"',
49-
$signatureType
50-
));
45+
if (!is_callable($target)) {
46+
throw new InvalidArgumentException(sprintf(
47+
'Function target is not callable: "%s"',
48+
$target
49+
));
50+
}
51+
52+
if ($signatureType === 'http') {
53+
$this->function = new HttpFunctionWrapper($target);
54+
} elseif (
55+
$signatureType === 'event'
56+
|| $signatureType === 'cloudevent'
57+
) {
58+
$this->function = new CloudEventFunctionWrapper($target, false);
59+
} else {
60+
throw new InvalidArgumentException(sprintf(
61+
'Invalid signature type: "%s"',
62+
$signatureType
63+
));
64+
}
5165
}
66+
5267
$this->errorLogFunc = function (string $error) {
5368
fwrite(fopen('php://stderr', 'wb'), json_encode([
54-
'message' => $error,
55-
'severity' => 'error'
69+
'message' => $error,
70+
'severity' => 'error'
5671
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
5772
};
5873
}
@@ -79,4 +94,16 @@ public function handle(
7994
]);
8095
}
8196
}
97+
98+
/**
99+
* Static registry for declaring functions. Internal only
100+
*
101+
* @internal
102+
* @param string $name The name of the registered function
103+
* @param FunctionWrapper $function The mapped FunctionWrapper instance
104+
*/
105+
public static function registerFunction(string $name, FunctionWrapper $function)
106+
{
107+
self::$registeredFunction[$name] = $function;
108+
}
82109
}

0 commit comments

Comments
 (0)