Skip to content

Commit 4ff952a

Browse files
authored
Core: refactoring protobuf support (#413)
Encode data as Uint8List when possible Add tests
1 parent ddf384a commit 4ff952a

File tree

5 files changed

+124
-24
lines changed

5 files changed

+124
-24
lines changed

functions_framework/lib/src/json_request_utils.dart

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import 'dart:convert';
16+
import 'dart:typed_data';
1617

1718
import 'package:gcp/gcp.dart';
1819
import 'package:http_parser/http_parser.dart';
@@ -49,6 +50,23 @@ MediaType mediaTypeFromRequest(Request request, {String? requiredMimeType}) {
4950
}
5051

5152
extension RequestExt on Request {
53+
Future<List<int>> readBytes() async {
54+
final length = contentLength;
55+
if (length == null) {
56+
return await read().fold<List<int>>(
57+
<int>[],
58+
(previous, element) => previous..addAll(element),
59+
);
60+
}
61+
final bytes = Uint8List(length);
62+
var offset = 0;
63+
await for (var bits in read()) {
64+
bytes.setAll(offset, bits);
65+
offset += bits.length;
66+
}
67+
return bytes;
68+
}
69+
5270
Future<Object?> decodeJson() async {
5371
try {
5472
final value = await (encoding ?? utf8)
@@ -67,22 +85,9 @@ extension RequestExt on Request {
6785
);
6886
}
6987
}
70-
}
71-
72-
const jsonContentType = 'application/json';
73-
74-
enum SupportedContentTypes {
75-
json(jsonContentType),
76-
protobuf('application/protobuf');
77-
78-
const SupportedContentTypes(this.value);
7988

80-
final String value;
81-
82-
static Future<({MediaType mimeType, Object? data})> decode(
83-
Request request,
84-
) async {
85-
final type = mediaTypeFromRequest(request);
89+
Future<({MediaType mimeType, Object? data})> decode() async {
90+
final type = mediaTypeFromRequest(this);
8691
final supportedType = SupportedContentTypes.values.singleWhere(
8792
(element) => element.value == type.mimeType,
8893
orElse: () => throw BadRequestException(
@@ -96,14 +101,22 @@ enum SupportedContentTypes {
96101
return (
97102
mimeType: type,
98103
data: switch (supportedType) {
99-
json => await request.decodeJson(),
100-
protobuf => await request.read().fold<List<int>>(
101-
<int>[],
102-
(previous, element) => previous..addAll(element),
103-
),
104+
SupportedContentTypes.json => await decodeJson(),
105+
SupportedContentTypes.protobuf => await readBytes(),
104106
},
105107
);
106108
}
107109
}
108110

111+
const jsonContentType = 'application/json';
112+
113+
enum SupportedContentTypes {
114+
json(jsonContentType),
115+
protobuf('application/protobuf');
116+
117+
const SupportedContentTypes(this.value);
118+
119+
final String value;
120+
}
121+
109122
const contentTypeHeader = 'Content-Type';

functions_framework/lib/src/targets/cloud_event_targets.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const _cloudEventPrefix = 'ce-';
8484
const _clientEventPrefixLength = _cloudEventPrefix.length;
8585

8686
Future<CloudEvent> _decodeBinary(Request request) async {
87-
final data = await SupportedContentTypes.decode(request);
87+
final data = await request.decode();
8888

8989
final map = <String, Object?>{
9090
for (var e in request.headers.entries

integration_test/bin/server.dart

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration_test/lib/functions.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,18 @@ void basicCloudEventHandler(CloudEvent event, RequestContext context) {
139139
stderr.writeln(encodeJsonPretty(event));
140140
}
141141

142+
@CloudFunction()
143+
void protoEventHandler(CloudEvent event, RequestContext context) {
144+
context.logger.info('event subject: ${event.subject}');
145+
146+
context.logger.debug(context.request.headers);
147+
148+
context.responseHeaders['x-data-runtime-types'] =
149+
event.data.runtimeType.toString();
150+
151+
stderr.writeln(encodeJsonPretty(event));
152+
}
153+
142154
final _helloWorldBytes = utf8.encode('Hello, World!');
143155

144156
const _contentTypeHeader = 'Content-Type';

integration_test/test/cloud_event_test.dart

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,67 @@ void main() {
9898
);
9999
});
100100

101+
test('valid proto input', () async {
102+
final proc = await _hostBasicEventHandler('protoEventHandler');
103+
104+
const subject = 'documents/users/ghXNtePIFmdDOBH3iEMH';
105+
final response = await _makeRequest(
106+
_protobytes,
107+
{
108+
'ce-id': '785865c0-2b16-439b-ad68-f9672343863a',
109+
'ce-source':
110+
'//firestore.googleapis.com/projects/dart-redirector/databases/(default)',
111+
'ce-specversion': '1.0',
112+
'ce-type': 'google.cloud.firestore.document.v1.updated',
113+
'Content-Type': 'application/protobuf',
114+
'ce-dataschema':
115+
'https://github.com/googleapis/google-cloudevents/blob/main/proto/google/events/cloud/firestore/v1/data.proto',
116+
'ce-subject': subject,
117+
'ce-time': '2023-06-21T12:21:25.413855Z',
118+
},
119+
);
120+
expect(response.statusCode, 200);
121+
expect(response.body, isEmpty);
122+
expect(
123+
response.headers,
124+
allOf(
125+
containsTextPlainHeader,
126+
containsPair('x-data-runtime-types', 'Uint8List'),
127+
),
128+
);
129+
await expectLater(
130+
proc.stdout,
131+
emitsInOrder(
132+
[
133+
startsWith('INFO: event subject: $subject'),
134+
startsWith('DEBUG:'),
135+
],
136+
),
137+
);
138+
139+
await finishServerTest(
140+
proc,
141+
requestOutput: endsWith('POST [200] /'),
142+
);
143+
144+
final stderrOutput = await proc.stderrStream().join('\n');
145+
final json = jsonDecode(stderrOutput) as Map<String, dynamic>;
146+
147+
expect(json, {
148+
'id': '785865c0-2b16-439b-ad68-f9672343863a',
149+
'source':
150+
'//firestore.googleapis.com/projects/dart-redirector/databases/(default)',
151+
'specversion': '1.0',
152+
'type': 'google.cloud.firestore.document.v1.updated',
153+
'datacontenttype': 'application/protobuf',
154+
'data': _protobytes,
155+
'dataschema':
156+
'https://github.com/googleapis/google-cloudevents/blob/main/proto/google/events/cloud/firestore/v1/data.proto',
157+
'subject': 'documents/users/ghXNtePIFmdDOBH3iEMH',
158+
'time': '2023-06-21T12:21:25.413855Z',
159+
});
160+
});
161+
101162
test('bad format of core header: time', () async {
102163
final stderrOutput = await _makeBadRequest(
103164
_pubSubJsonString,
@@ -311,7 +372,7 @@ Future<String> _makeBadRequest(
311372
return stderrOutput;
312373
}
313374

314-
Future<Response> _makeRequest(String body, Map<String, String> headers) async {
375+
Future<Response> _makeRequest(Object? body, Map<String, String> headers) async {
315376
final requestUrl = 'http://localhost:$autoPort/';
316377

317378
final response = await post(
@@ -322,15 +383,26 @@ Future<Response> _makeRequest(String body, Map<String, String> headers) async {
322383
return response;
323384
}
324385

325-
Future<TestProcess> _hostBasicEventHandler() async {
386+
Future<TestProcess> _hostBasicEventHandler([
387+
String name = 'basicCloudEventHandler',
388+
]) async {
326389
final proc = await startServerTest(
327390
arguments: [
328391
'--target',
329-
'basicCloudEventHandler',
392+
name,
330393
'--signature-type',
331394
'cloudevent',
332395
],
333396
expectedListeningPort: 0,
334397
);
335398
return proc;
336399
}
400+
401+
/// This is a Firestore update record in protobuf
402+
final _protobytes = base64Decode(
403+
'CoIBClFwcm9qZWN0cy9kYXJ0LXJlZGlyZWN0b3IvZGF0YWJhc2VzLyhkZWZhdWx0KS9kb2N1bWVu'
404+
'dHMvdXNlcnMvZ2hYTnRlUElGbWRET0JIM2lFTUgSEQoEbmFtZRIJigEGbHVjaWE0GgwI6+KupAYQ'
405+
'iMa2qgIiDAjF1sukBhCY2qvFARKCAQpRcHJvamVjdHMvZGFydC1yZWRpcmVjdG9yL2RhdGFiYXNl'
406+
'cy8oZGVmYXVsdCkvZG9jdW1lbnRzL3VzZXJzL2doWE50ZVBJRm1kRE9CSDNpRU1IEhEKBG5hbWUS'
407+
'CYoBBmx1Y2lhMxoMCOvirqQGEIjGtqoCIgwI8o60pAYQmJa6igMaBgoEbmFtZQ==',
408+
);

0 commit comments

Comments
 (0)