diff --git a/functions_framework/lib/src/cloud_event.dart b/functions_framework/lib/src/cloud_event.dart index 707f89ae..18d34f5d 100644 --- a/functions_framework/lib/src/cloud_event.dart +++ b/functions_framework/lib/src/cloud_event.dart @@ -13,11 +13,19 @@ // limitations under the License. import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; part 'cloud_event.g.dart'; -@JsonSerializable(includeIfNull: false, checked: true) -class CloudEvent { +@internal +typedef FromJson = T Function(Map json); + +@JsonSerializable( + includeIfNull: false, + checked: true, + genericArgumentFactories: true, +) +class CloudEvent { @JsonKey(required: true) final String id; @JsonKey(required: true) @@ -30,7 +38,7 @@ class CloudEvent { @JsonKey(name: 'datacontenttype') final String? dataContentType; - final Object? data; + final T data; @JsonKey(name: 'dataschema') final Uri? dataSchema; @@ -42,15 +50,21 @@ class CloudEvent { required this.source, required this.specVersion, required this.type, - this.data, + required this.data, this.dataContentType, this.dataSchema, this.subject, this.time, }); - factory CloudEvent.fromJson(Map json) => - _$CloudEventFromJson(json); + factory CloudEvent.fromJson( + Map json, + FromJson fromJsonT, + ) => + _$CloudEventFromJson( + json, + (value) => fromJsonT(value as Map), + ); - Map toJson() => _$CloudEventToJson(this); + Map toJson() => _$CloudEventToJson(this, (val) => val); } diff --git a/functions_framework/lib/src/cloud_event.g.dart b/functions_framework/lib/src/cloud_event.g.dart index de8a8433..b2acc38c 100644 --- a/functions_framework/lib/src/cloud_event.g.dart +++ b/functions_framework/lib/src/cloud_event.g.dart @@ -8,16 +8,19 @@ part of 'cloud_event.dart'; // JsonSerializableGenerator // ************************************************************************** -CloudEvent _$CloudEventFromJson(Map json) { +CloudEvent _$CloudEventFromJson( + Map json, + T Function(Object? json) fromJsonT, +) { return $checkedNew('CloudEvent', json, () { $checkKeys(json, requiredKeys: const ['id', 'source', 'specversion', 'type']); - final val = CloudEvent( + final val = CloudEvent( id: $checkedConvert(json, 'id', (v) => v as String), source: $checkedConvert(json, 'source', (v) => Uri.parse(v as String)), specVersion: $checkedConvert(json, 'specversion', (v) => v as String), type: $checkedConvert(json, 'type', (v) => v as String), - data: $checkedConvert(json, 'data', (v) => v), + data: $checkedConvert(json, 'data', (v) => fromJsonT(v)), dataContentType: $checkedConvert(json, 'datacontenttype', (v) => v as String?), dataSchema: $checkedConvert( @@ -34,7 +37,10 @@ CloudEvent _$CloudEventFromJson(Map json) { }); } -Map _$CloudEventToJson(CloudEvent instance) { +Map _$CloudEventToJson( + CloudEvent instance, + Object? Function(T value) toJsonT, +) { final val = { 'id': instance.id, 'source': instance.source.toString(), @@ -49,7 +55,7 @@ Map _$CloudEventToJson(CloudEvent instance) { } writeNotNull('datacontenttype', instance.dataContentType); - writeNotNull('data', instance.data); + val['data'] = toJsonT(instance.data); writeNotNull('dataschema', instance.dataSchema?.toString()); writeNotNull('subject', instance.subject); writeNotNull('time', instance.time?.toIso8601String()); diff --git a/functions_framework/lib/src/function_target.dart b/functions_framework/lib/src/function_target.dart index e294bddc..516c75c6 100644 --- a/functions_framework/lib/src/function_target.dart +++ b/functions_framework/lib/src/function_target.dart @@ -34,13 +34,15 @@ abstract class FunctionTarget { HandlerWithLogger function, ) = HttpWithLoggerFunctionTarget; - factory FunctionTarget.cloudEvent( - CloudEventHandler function, - ) = CloudEventFunctionTarget; - - factory FunctionTarget.cloudEventWithContext( - CloudEventWithContextHandler function, - ) = CloudEventWithContextFunctionTarget; + static FunctionTarget cloudEvent( + CloudEventHandler function, + ) => + CloudEventFunctionTarget(function); + + static FunctionTarget cloudEventWithContext( + CloudEventWithContextHandler function, + ) => + CloudEventWithContextFunctionTarget(function); FutureOr handler(Request request); } diff --git a/functions_framework/lib/src/targets/cloud_event_targets.dart b/functions_framework/lib/src/targets/cloud_event_targets.dart index 16ffa891..5f8cfb96 100644 --- a/functions_framework/lib/src/targets/cloud_event_targets.dart +++ b/functions_framework/lib/src/targets/cloud_event_targets.dart @@ -24,8 +24,19 @@ import '../json_request_utils.dart'; import '../request_context.dart'; import '../typedefs.dart'; -class CloudEventFunctionTarget extends FunctionTarget { - final CloudEventHandler function; +abstract class _CloudEventFunctionTarget extends FunctionTarget { + _CloudEventFunctionTarget(); + + Future> _eventFromRequest(Request request) async => + _requiredBinaryHeader.every(request.headers.containsKey) + ? await _decodeBinary(request, _decode) + : await _decodeStructured(request, _decode); + + T _decode(Map json) => json as T; +} + +class CloudEventFunctionTarget extends _CloudEventFunctionTarget { + final CloudEventHandler function; @override FunctionType get type => FunctionType.cloudevent; @@ -33,17 +44,16 @@ class CloudEventFunctionTarget extends FunctionTarget { @override FutureOr handler(Request request) async { final event = await _eventFromRequest(request); - await function(event); - return Response.ok(''); } CloudEventFunctionTarget(this.function); } -class CloudEventWithContextFunctionTarget extends FunctionTarget { - final CloudEventWithContextHandler function; +class CloudEventWithContextFunctionTarget + extends _CloudEventFunctionTarget { + final CloudEventWithContextHandler function; @override FunctionType get type => FunctionType.cloudevent; @@ -51,22 +61,18 @@ class CloudEventWithContextFunctionTarget extends FunctionTarget { @override Future handler(Request request) async { final event = await _eventFromRequest(request); - final context = contextForRequest(request); await function(event, context); - return Response.ok('', headers: context.responseHeaders); } CloudEventWithContextFunctionTarget(this.function); } -Future _eventFromRequest(Request request) => - _requiredBinaryHeader.every(request.headers.containsKey) - ? _decodeBinary(request) - : _decodeStructured(request); - -Future _decodeStructured(Request request) async { +Future> _decodeStructured( + Request request, + FromJson fromJson, +) async { final type = mediaTypeFromRequest(request); mustBeJson(type); @@ -79,13 +85,20 @@ Future _decodeStructured(Request request) async { }; } - return _decodeValidCloudEvent(jsonObject, 'structured-mode message'); + return _decodeValidCloudEvent( + jsonObject, + 'structured-mode message', + fromJson, + ); } const _cloudEventPrefix = 'ce-'; const _clientEventPrefixLength = _cloudEventPrefix.length; -Future _decodeBinary(Request request) async { +Future> _decodeBinary( + Request request, + FromJson fromJson, +) async { final type = mediaTypeFromRequest(request); mustBeJson(type); @@ -97,15 +110,16 @@ Future _decodeBinary(Request request) async { 'data': await decodeJson(request), }; - return _decodeValidCloudEvent(map, 'binary-mode message'); + return _decodeValidCloudEvent(map, 'binary-mode message', fromJson); } -CloudEvent _decodeValidCloudEvent( +CloudEvent _decodeValidCloudEvent( Map map, String messageType, + FromJson fromJson, ) { try { - return CloudEvent.fromJson(map); + return CloudEvent.fromJson(map, fromJson); } catch (e, stackTrace) { throw BadRequestException( 400, diff --git a/functions_framework/lib/src/typedefs.dart b/functions_framework/lib/src/typedefs.dart index 69ee6f4a..f9e28582 100644 --- a/functions_framework/lib/src/typedefs.dart +++ b/functions_framework/lib/src/typedefs.dart @@ -20,10 +20,10 @@ import 'cloud_event.dart'; import 'log_severity.dart'; import 'request_context.dart'; -typedef CloudEventHandler = FutureOr Function(CloudEvent request); +typedef CloudEventHandler = FutureOr Function(CloudEvent request); -typedef CloudEventWithContextHandler = FutureOr Function( - CloudEvent request, +typedef CloudEventWithContextHandler = FutureOr Function( + CloudEvent request, RequestContext context, ); diff --git a/functions_framework/pubspec.yaml b/functions_framework/pubspec.yaml index 66da2d4b..d664d14d 100644 --- a/functions_framework/pubspec.yaml +++ b/functions_framework/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: dev_dependencies: build_runner: ^1.10.7 build_verify: ^2.0.0 - json_serializable: ^4.0.0 + json_serializable: ^4.0.1 # Because we use the ignore_for_file build.yaml config source_gen: ^1.0.0 test: ^1.15.7 diff --git a/functions_framework_builder/lib/src/supported_function_type.dart b/functions_framework_builder/lib/src/supported_function_type.dart index b70e0247..02227fd8 100644 --- a/functions_framework_builder/lib/src/supported_function_type.dart +++ b/functions_framework_builder/lib/src/supported_function_type.dart @@ -50,7 +50,9 @@ class SupportedFunctionType { lib.exportNamespace.get(typeDefName) as TypeAliasElement; final functionType = handlerTypeAlias.instantiate( - typeArguments: [], + typeArguments: handlerTypeAlias.typeParameters + .map((e) => e.instantiate(nullabilitySuffix: NullabilitySuffix.none)) + .toList(), nullabilitySuffix: NullabilitySuffix.none, ); diff --git a/functions_framework_builder/lib/src/valid_json_utils.dart b/functions_framework_builder/lib/src/valid_json_utils.dart index bf871358..6bbca4f7 100644 --- a/functions_framework_builder/lib/src/valid_json_utils.dart +++ b/functions_framework_builder/lib/src/valid_json_utils.dart @@ -27,19 +27,8 @@ bool _validJsonType(DartType type, bool allowComplexMembers) { return memberType.isDynamic || memberType.isDartCoreObject; } - if (type.isDartCoreBool || - type.isDartCoreNum || - type.isDartCoreDouble || - type.isDartCoreInt || - type.isDartCoreString) { - return true; - } - if (type is InterfaceType) { - if (type.isDartCoreList) { - final arg = type.typeArguments.single; - return validCollectionMember(arg); - } else if (type.isDartCoreMap) { + if (type.isDartCoreMap) { final keyArg = type.typeArguments[0]; final valueArg = type.typeArguments[1]; return keyArg.isDartCoreString && validCollectionMember(valueArg); diff --git a/functions_framework_builder/test/builder_test.dart b/functions_framework_builder/test/builder_test.dart index ad72c6b4..bf579d37 100644 --- a/functions_framework_builder/test/builder_test.dart +++ b/functions_framework_builder/test/builder_test.dart @@ -226,8 +226,8 @@ $lines test('simple return type', () async { final newInputContent = inputContent - .replaceAll('void ', 'int ') - .replaceAll('', ''); + .replaceAll('void ', 'Map ') + .replaceAll('', '>'); await _testItems( newInputContent, @@ -266,48 +266,8 @@ $lines test('JSON return type', () async { final newInputContent = inputContent - .replaceAll('void ', 'int ') - .replaceAll('', ''); - - await _testItems( - newInputContent, - [ - 'syncFunction', - 'asyncFunction', - 'futureOrFunction', - 'extraParam', - 'optionalParam', - ], - (e) => """ - case '$e': - return JsonFunctionTarget( - function_library.$e, - (json) { - if (json is Map) { - try { - return function_library.JsonType.fromJson(json); - } catch (e, stack) { - throw BadRequestException( - 400, - 'There was an error parsing the provided JSON data.', - innerError: e, - innerStack: stack, - ); - } - } - throw BadRequestException( - 400, - 'The provided JSON is not the expected type ' - '`Map`.', - ); - }, - );"""); - }); - - test('complex return type', () async { - final newInputContent = inputContent - .replaceAll('void ', 'Map> ') - .replaceAll('', '>>'); + .replaceAll('void ', 'Map ') + .replaceAll('', '>'); await _testItems( newInputContent, @@ -365,13 +325,13 @@ $lines return JsonFunctionTarget.voidResult( function_library.$e, (json) { - if (json is num) { + if (json is Map) { return json; } throw BadRequestException( 400, 'The provided JSON is not the expected type ' - '`num`.', + '`Map`.', ); }, );"""); @@ -379,39 +339,8 @@ $lines test('simple return type', () async { final newInputContent = inputContent - .replaceAll('void ', 'int ') - .replaceAll('', ''); - await _testItems( - newInputContent, - [ - 'syncFunction', - 'asyncFunction', - 'futureOrFunction', - 'extraParam', - 'optionalParam', - ], - (e) => """ - case '$e': - return JsonFunctionTarget( - function_library.$e, - (json) { - if (json is num) { - return json; - } - throw BadRequestException( - 400, - 'The provided JSON is not the expected type ' - '`num`.', - ); - }, - );"""); - }); - - test('complex return type', () async { - final newInputContent = inputContent - .replaceAll('void ', 'Map> ') - .replaceAll('', '>>'); - + .replaceAll('void ', 'Map ') + .replaceAll('', '>'); await _testItems( newInputContent, [ @@ -426,13 +355,13 @@ $lines return JsonFunctionTarget( function_library.$e, (json) { - if (json is num) { + if (json is Map) { return json; } throw BadRequestException( 400, 'The provided JSON is not the expected type ' - '`num`.', + '`Map`.', ); }, );"""); @@ -441,8 +370,8 @@ $lines test('void with context', () async { final newInputContent = inputContent .replaceAll( - '(num request)', - '(num request, RequestContext context)', + '(Map request)', + '(Map request, RequestContext context)', ) .replaceAll( 'int? other', @@ -463,13 +392,13 @@ $lines return JsonWithContextFunctionTarget.voidResult( function_library.$e, (json) { - if (json is num) { + if (json is Map) { return json; } throw BadRequestException( 400, 'The provided JSON is not the expected type ' - '`num`.', + '`Map`.', ); }, );"""); @@ -556,6 +485,21 @@ package:$_pkgName/functions.dart:8:10 // Custom and JSON event types // 'Duration handleGet(DateTime request) => null;': notCompatibleMatcher, + + // + // dart:core types that aren't `Map` + // + // Map param is under-specified + 'Map handleGet(Map request) => null;': + notCompatibleMatcher, + 'int handleGet(Map request) => null;': + notCompatibleMatcher, + 'Map handleGet(int request) => null;': + notCompatibleMatcher, + // Map return type is under-specified + 'Map handleGet(Map request) => null;': + notCompatibleMatcher, + 'int handleGet(int request) => null;': notCompatibleMatcher, }; for (var shape in invalidShapes.entries) { diff --git a/functions_framework_builder/test/test_examples/valid_json_type_handlers.dart b/functions_framework_builder/test/test_examples/valid_json_type_handlers.dart index ee2f5b17..96940e7f 100644 --- a/functions_framework_builder/test/test_examples/valid_json_type_handlers.dart +++ b/functions_framework_builder/test/test_examples/valid_json_type_handlers.dart @@ -17,16 +17,20 @@ import 'dart:async'; import 'package:functions_framework/functions_framework.dart'; @CloudFunction() -void syncFunction(num request) => throw UnimplementedError(); +void syncFunction(Map request) => throw UnimplementedError(); @CloudFunction() -Future asyncFunction(num request) => throw UnimplementedError(); +Future asyncFunction(Map request) => + throw UnimplementedError(); @CloudFunction() -FutureOr futureOrFunction(num request) => throw UnimplementedError(); +FutureOr futureOrFunction(Map request) => + throw UnimplementedError(); @CloudFunction() -void extraParam(num request, [int? other]) => throw UnimplementedError(); +void extraParam(Map request, [int? other]) => + throw UnimplementedError(); @CloudFunction() -void optionalParam([num? request, int? other]) => throw UnimplementedError(); +void optionalParam([Map? request, int? other]) => + throw UnimplementedError(); diff --git a/test/hello/bin/server.dart b/test/hello/bin/server.dart index 0957940c..0d24399f 100644 --- a/test/hello/bin/server.dart +++ b/test/hello/bin/server.dart @@ -79,6 +79,10 @@ FunctionTarget? _nameToFunctionTarget(String name) { ); }, ); + case 'customTypeHandler': + return FunctionTarget.cloudEventWithContext( + function_library.customTypeHandler, + ); default: return null; } diff --git a/test/hello/lib/src/json_handlers.dart b/test/hello/lib/src/json_handlers.dart index 7e7e9db0..cecf463f 100644 --- a/test/hello/lib/src/json_handlers.dart +++ b/test/hello/lib/src/json_handlers.dart @@ -25,12 +25,12 @@ void pubSubHandler(PubSub pubSub, RequestContext context) { } @CloudFunction() -FutureOr jsonHandler( +FutureOr> jsonHandler( Map request, RequestContext context, ) { print('Keys: ${request.keys.join(', ')}'); context.responseHeaders['key_count'] = request.keys.length.toString(); context.responseHeaders['multi'] = ['item1', 'item2']; - return request.isEmpty; + return {'isEmpty': request.isEmpty}; } diff --git a/test/hello/lib/src/pub_sub_types.dart b/test/hello/lib/src/pub_sub_types.dart index 52e934a9..8c40df31 100644 --- a/test/hello/lib/src/pub_sub_types.dart +++ b/test/hello/lib/src/pub_sub_types.dart @@ -15,6 +15,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:functions_framework/functions_framework.dart'; import 'package:json_annotation/json_annotation.dart'; part 'pub_sub_types.g.dart'; @@ -47,3 +48,6 @@ class PubSubMessage { Map toJson() => _$PubSubMessageToJson(this); } + +@CloudFunction() +void customTypeHandler(CloudEvent bob, RequestContext context) {} diff --git a/test/hello/test/custom_type_test.dart b/test/hello/test/custom_type_test.dart index 2b2282b7..a2244bf6 100644 --- a/test/hello/test/custom_type_test.dart +++ b/test/hello/test/custom_type_test.dart @@ -202,7 +202,7 @@ void main() { ), ); final jsonBody = jsonDecode(response.body); - expect(jsonBody, false); + expect(jsonBody, {'isEmpty': false}); await finishServerTest( testProcess, @@ -230,7 +230,7 @@ void main() { ), ); final jsonBody = jsonDecode(response.body); - expect(jsonBody, true); + expect(jsonBody, {'isEmpty': true}); await finishServerTest( testProcess,