Skip to content

typed events #188

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions functions_framework/lib/src/cloud_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T Function(Map<String, dynamic> json);

@JsonSerializable(
includeIfNull: false,
checked: true,
genericArgumentFactories: true,
)
class CloudEvent<T> {
@JsonKey(required: true)
final String id;
@JsonKey(required: true)
Expand All @@ -30,7 +38,7 @@ class CloudEvent {

@JsonKey(name: 'datacontenttype')
final String? dataContentType;
final Object? data;
final T data;

@JsonKey(name: 'dataschema')
final Uri? dataSchema;
Expand All @@ -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<String, dynamic> json) =>
_$CloudEventFromJson(json);
factory CloudEvent.fromJson(
Map<String, dynamic> json,
FromJson<T> fromJsonT,
) =>
_$CloudEventFromJson(
json,
(value) => fromJsonT(value as Map<String, dynamic>),
);

Map<String, dynamic> toJson() => _$CloudEventToJson(this);
Map<String, dynamic> toJson() => _$CloudEventToJson(this, (val) => val);
}
16 changes: 11 additions & 5 deletions functions_framework/lib/src/cloud_event.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions functions_framework/lib/src/function_target.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
CloudEventHandler<T> function,
) =>
CloudEventFunctionTarget<T>(function);

static FunctionTarget cloudEventWithContext<T>(
CloudEventWithContextHandler<T> function,
) =>
CloudEventWithContextFunctionTarget<T>(function);

FutureOr<Response> handler(Request request);
}
52 changes: 33 additions & 19 deletions functions_framework/lib/src/targets/cloud_event_targets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,55 @@ import '../json_request_utils.dart';
import '../request_context.dart';
import '../typedefs.dart';

class CloudEventFunctionTarget extends FunctionTarget {
final CloudEventHandler function;
abstract class _CloudEventFunctionTarget<T> extends FunctionTarget {
_CloudEventFunctionTarget();

Future<CloudEvent<T>> _eventFromRequest(Request request) async =>
_requiredBinaryHeader.every(request.headers.containsKey)
? await _decodeBinary(request, _decode)
: await _decodeStructured(request, _decode);

T _decode(Map<String, dynamic> json) => json as T;
}

class CloudEventFunctionTarget<T> extends _CloudEventFunctionTarget<T> {
final CloudEventHandler<T> function;

@override
FunctionType get type => FunctionType.cloudevent;

@override
FutureOr<Response> 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<T>
extends _CloudEventFunctionTarget<T> {
final CloudEventWithContextHandler<T> function;

@override
FunctionType get type => FunctionType.cloudevent;

@override
Future<Response> 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<CloudEvent> _eventFromRequest(Request request) =>
_requiredBinaryHeader.every(request.headers.containsKey)
? _decodeBinary(request)
: _decodeStructured(request);

Future<CloudEvent> _decodeStructured(Request request) async {
Future<CloudEvent<T>> _decodeStructured<T>(
Request request,
FromJson<T> fromJson,
) async {
final type = mediaTypeFromRequest(request);

mustBeJson(type);
Expand All @@ -79,13 +85,20 @@ Future<CloudEvent> _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<CloudEvent> _decodeBinary(Request request) async {
Future<CloudEvent<T>> _decodeBinary<T>(
Request request,
FromJson<T> fromJson,
) async {
final type = mediaTypeFromRequest(request);
mustBeJson(type);

Expand All @@ -97,15 +110,16 @@ Future<CloudEvent> _decodeBinary(Request request) async {
'data': await decodeJson(request),
};

return _decodeValidCloudEvent(map, 'binary-mode message');
return _decodeValidCloudEvent(map, 'binary-mode message', fromJson);
}

CloudEvent _decodeValidCloudEvent(
CloudEvent<T> _decodeValidCloudEvent<T>(
Map<String, dynamic> map,
String messageType,
FromJson<T> fromJson,
) {
try {
return CloudEvent.fromJson(map);
return CloudEvent.fromJson(map, fromJson);
} catch (e, stackTrace) {
throw BadRequestException(
400,
Expand Down
6 changes: 3 additions & 3 deletions functions_framework/lib/src/typedefs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import 'cloud_event.dart';
import 'log_severity.dart';
import 'request_context.dart';

typedef CloudEventHandler = FutureOr<void> Function(CloudEvent request);
typedef CloudEventHandler<T> = FutureOr<void> Function(CloudEvent<T> request);

typedef CloudEventWithContextHandler = FutureOr<void> Function(
CloudEvent request,
typedef CloudEventWithContextHandler<T> = FutureOr<void> Function(
CloudEvent<T> request,
RequestContext context,
);

Expand Down
2 changes: 1 addition & 1 deletion functions_framework/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand Down
13 changes: 1 addition & 12 deletions functions_framework_builder/lib/src/valid_json_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading