Skip to content

Commit 2bbfb50

Browse files
committed
fix: apply adjacency-list guard to prevent native lists from parsing incorrectly
1 parent 0d1406f commit 2bbfb50

File tree

1 file changed

+29
-9
lines changed

1 file changed

+29
-9
lines changed

packages/genui/lib/src/model/data_model.dart

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ class DataContext {
115115
/// Manages the application's Object? data model and provides
116116
/// a subscription-based mechanism for reactive UI updates.
117117
class DataModel {
118+
static const _a2uiValueKeys = [
119+
'valueString',
120+
'valueNumber',
121+
'valueBoolean',
122+
'valueMap',
123+
];
124+
118125
JsonMap _data = {};
119126
final Map<DataPath, ValueNotifier<Object?>> _subscriptions = {};
120127
final Map<DataPath, ValueNotifier<Object?>> _valueSubscriptions = {};
@@ -129,13 +136,20 @@ class DataModel {
129136
'DataModel.update: path=$absolutePath, contents='
130137
'${const JsonEncoder.withIndent(' ').convert(contents)}',
131138
);
132-
final Object? parsedContents = contents is List
139+
final bool isAdjacencyList = contents is List && _isAdjacencyList(contents);
140+
final Object? parsedContents = isAdjacencyList
133141
? _parseDataModelContents(contents)
134142
: contents;
135143

136144
if (absolutePath == null || absolutePath.segments.isEmpty) {
137-
if (contents is List) {
145+
if (parsedContents is Map) {
138146
_data = parsedContents as Map<String, Object?>;
147+
} else if (parsedContents is List) {
148+
genUiLogger.warning(
149+
'DataModel.update: literal list cannot be used as '
150+
'root data model: $contents',
151+
);
152+
_data = <String, Object?>{};
139153
} else if (contents is Map) {
140154
// Permissive: Allow a map to be sent for the root, even though the
141155
// schema expects a list.
@@ -188,6 +202,18 @@ class DataModel {
188202
return notifier;
189203
}
190204

205+
/// Determines if the given contents are likely an A2UI adjacency list.
206+
bool _isAdjacencyList(List<Object?> contents) {
207+
if (contents.isEmpty) return false;
208+
for (final item in contents) {
209+
if (item is! Map) return false;
210+
if (!item.containsKey('key')) return false;
211+
212+
if (!_a2uiValueKeys.any(item.containsKey)) return false;
213+
}
214+
return true;
215+
}
216+
191217
/// Retrieves a static, one-time value from the data model at the
192218
/// specified absolute path without creating a subscription.
193219
T? getValue<T>(DataPath absolutePath) {
@@ -211,13 +237,7 @@ class DataModel {
211237
Object? value;
212238
var valueCount = 0;
213239

214-
const valueKeys = [
215-
'valueString',
216-
'valueNumber',
217-
'valueBoolean',
218-
'valueMap',
219-
];
220-
for (final valueKey in valueKeys) {
240+
for (final String valueKey in _a2uiValueKeys) {
221241
if (item.containsKey(valueKey)) {
222242
if (valueCount == 0) {
223243
if (valueKey == 'valueMap') {

0 commit comments

Comments
 (0)