Skip to content
Open
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
42 changes: 29 additions & 13 deletions packages/genui/lib/src/model/a2ui_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ sealed class A2uiMessage {
],
);
}

/// Converts this message to a JSON map.
Map<String, dynamic> toJson();
}

/// An A2UI message that signals the client to create and show a new surface.
Expand Down Expand Up @@ -155,13 +158,15 @@ final class CreateSurface extends A2uiMessage {
/// If true, the client sends the full data model in A2A metadata.
final bool sendDataModel;

/// Converts this message to a JSON map.
@override
Map<String, dynamic> toJson() => {
'version': 'v0.9',
surfaceIdKey: surfaceId,
'catalogId': catalogId,
'theme': ?theme,
'sendDataModel': sendDataModel,
'createSurface': {
surfaceIdKey: surfaceId,
'catalogId': catalogId,
'theme': theme,
'sendDataModel': sendDataModel,
},
};
}

Expand All @@ -187,10 +192,16 @@ final class UpdateComponents extends A2uiMessage {
final List<Component> components;

/// Converts this message to a JSON map.
///
/// The result is compatible with [A2uiMessage.fromJson] for round-trip
/// serialization (e.g. when saving messages locally).
@override
Map<String, dynamic> toJson() => {
'version': 'v0.9',
surfaceIdKey: surfaceId,
'components': components.map((c) => c.toJson()).toList(),
'updateComponents': {
surfaceIdKey: surfaceId,
'components': components.map((c) => c.toJson()).toList(),
},
};
}

Expand Down Expand Up @@ -224,12 +235,14 @@ final class UpdateDataModel extends A2uiMessage {
/// key at the path.
final Object? value;

/// Converts this message to a JSON map.
@override
Map<String, dynamic> toJson() => {
'version': 'v0.9',
surfaceIdKey: surfaceId,
'path': path.toString(),
'value': ?value,
'updateDataModel': {
surfaceIdKey: surfaceId,
'path': path.toString(),
'value': value,
},
};
}

Expand All @@ -246,6 +259,9 @@ final class DeleteSurface extends A2uiMessage {
/// The ID of the surface that this message applies to.
final String surfaceId;

/// Converts this message to a JSON map.
Map<String, dynamic> toJson() => {'version': 'v0.9', surfaceIdKey: surfaceId};
@override
Map<String, dynamic> toJson() => {
'version': 'v0.9',
'deleteSurface': {surfaceIdKey: surfaceId},
};
}
4 changes: 3 additions & 1 deletion packages/genui/lib/test/validation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ Future<List<ExampleValidationError>> validateCatalogItemExamples(
components: components,
);

final payload =
surfaceUpdate.toJson()['updateComponents'] as Map<String, dynamic>;
final List<ValidationError> validationErrors = await schema.validate(
surfaceUpdate.toJson(),
payload,
);
if (validationErrors.isNotEmpty) {
errors.add(
Expand Down
60 changes: 60 additions & 0 deletions packages/genui/test/model/a2ui_message_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,66 @@ void main() {
expect(message.toJson(), containsPair('version', 'v0.9'));
});

test('CreateSurface round-trip: toJson then fromJson', () {
const message = CreateSurface(
surfaceId: 's1',
catalogId: 'c1',
theme: {'color': 'blue'},
sendDataModel: true,
);
final decoded = A2uiMessage.fromJson(
Map<String, dynamic>.from(message.toJson()),
);
expect(decoded, isA<CreateSurface>());
final create = decoded as CreateSurface;
expect(create.surfaceId, message.surfaceId);
expect(create.catalogId, message.catalogId);
expect(create.theme, message.theme);
expect(create.sendDataModel, message.sendDataModel);
});

test('UpdateComponents round-trip: toJson then fromJson', () {
const message = UpdateComponents(
surfaceId: 's1',
components: [
Component(id: 'c1', type: 'Text', properties: {'text': 'Hi'}),
],
);
final decoded = A2uiMessage.fromJson(
Map<String, dynamic>.from(message.toJson()),
);
expect(decoded, isA<UpdateComponents>());
final update = decoded as UpdateComponents;
expect(update.surfaceId, message.surfaceId);
expect(update.components.length, message.components.length);
expect(update.components.first.id, message.components.first.id);
});

test('UpdateDataModel round-trip: toJson then fromJson', () {
final message = UpdateDataModel(
surfaceId: 's1',
path: DataPath('/user/name'),
value: 'Alice',
);
final decoded = A2uiMessage.fromJson(
Map<String, dynamic>.from(message.toJson()),
);
expect(decoded, isA<UpdateDataModel>());
final update = decoded as UpdateDataModel;
expect(update.surfaceId, message.surfaceId);
expect(update.path, message.path);
expect(update.value, message.value);
});

test('DeleteSurface round-trip: toJson then fromJson', () {
const message = DeleteSurface(surfaceId: 's1');
final decoded = A2uiMessage.fromJson(
Map<String, dynamic>.from(message.toJson()),
);
expect(decoded, isA<DeleteSurface>());
expect((decoded as DeleteSurface).surfaceId, message.surfaceId);
});

test('fromJson throws on unknown message type', () {
final json = <String, Object>{'version': 'v0.9', 'unknown': {}};
expect(
Expand Down
5 changes: 4 additions & 1 deletion packages/genui/test/test_infra/validation_test_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ void validateCatalogExamples(
components: components,
);

final payload =
surfaceUpdate.toJson()['updateComponents']
as Map<String, dynamic>;
final List<ValidationError> validationErrors = await schema.validate(
surfaceUpdate.toJson(),
payload,
);
expect(validationErrors, isEmpty);
});
Expand Down
Loading