diff --git a/packages/genui/lib/genui.dart b/packages/genui/lib/genui.dart index 525d64ba4..290601909 100644 --- a/packages/genui/lib/genui.dart +++ b/packages/genui/lib/genui.dart @@ -29,6 +29,7 @@ export 'src/model/chat_message.dart'; export 'src/model/data_model.dart'; export 'src/model/generation_events.dart'; export 'src/model/ui_models.dart'; +export 'src/model/validation_helper.dart'; export 'src/primitives/cancellation.dart'; export 'src/primitives/constants.dart'; export 'src/primitives/logging.dart'; diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart index b1d719498..9c50c80b2 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart @@ -7,11 +7,11 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; +import '../../model/data_model.dart'; import '../../model/ui_models.dart'; +import '../../model/validation_helper.dart'; import '../../primitives/logging.dart'; import '../../primitives/simple_items.dart'; -import '../../utils/validation_helper.dart'; -import '../../widgets/widget_utilities.dart'; final _schema = S.object( description: 'An interactive button that triggers an action when pressed.', diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart index c2401d6c4..be314b50d 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart @@ -10,8 +10,8 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; import '../../model/data_model.dart'; +import '../../model/validation_helper.dart'; import '../../primitives/simple_items.dart'; -import '../../utils/validation_helper.dart'; import '../../widgets/widget_utilities.dart'; final _schema = S.object( diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/text_field.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/text_field.dart index 577ecb9ce..cad6e5c0c 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/text_field.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/text_field.dart @@ -11,8 +11,8 @@ import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; import '../../model/data_model.dart'; import '../../model/ui_models.dart'; +import '../../model/validation_helper.dart'; import '../../primitives/simple_items.dart'; -import '../../utils/validation_helper.dart'; import '../../widgets/widget_utilities.dart'; final _schema = S.object( diff --git a/packages/genui/lib/src/functions/format_string.dart b/packages/genui/lib/src/functions/format_string.dart index 51e44edc0..c1aaa9a00 100644 --- a/packages/genui/lib/src/functions/format_string.dart +++ b/packages/genui/lib/src/functions/format_string.dart @@ -4,12 +4,13 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import 'package:meta/meta.dart'; -import 'package:rxdart/rxdart.dart'; +import 'package:stream_transform/stream_transform.dart'; import '../model/client_function.dart' as cf; import '../model/data_model.dart'; import '../primitives/logging.dart'; import '../primitives/simple_items.dart'; +import '../utils/stream_extensions.dart'; /// Formats a value as a string. class FormatStringFunction implements cf.ClientFunction { @@ -173,7 +174,7 @@ class ExpressionParser { return Stream.value(val); }).toList(); - return CombineLatestStream.list(streams).switchMap((List values) { + return streams.combineLatestAll().switchMap((List values) { final Map combinedArgs = {}; for (var i = 0; i < keys.length; i++) { combinedArgs[keys[i]] = values[i]; @@ -249,7 +250,7 @@ class ExpressionParser { return Stream.value(part); }).toList(); - return CombineLatestStream.list(streams).map((List values) { + return streams.combineLatestAll().map((List values) { return values.map((e) => e?.toString() ?? '').join(''); }); } diff --git a/packages/genui/lib/src/model/data_model.dart b/packages/genui/lib/src/model/data_model.dart index 2aba001ab..cdd031110 100644 --- a/packages/genui/lib/src/model/data_model.dart +++ b/packages/genui/lib/src/model/data_model.dart @@ -6,10 +6,11 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; -import 'package:rxdart/rxdart.dart'; +import 'package:stream_transform/stream_transform.dart'; import '../primitives/logging.dart'; import '../primitives/simple_items.dart'; +import '../utils/stream_extensions.dart'; import 'client_function.dart' as cf; import 'data_path.dart'; @@ -155,7 +156,7 @@ class DataContext implements cf.ExecutionContext { final Stream> combinedStream = streams.isEmpty ? Stream.value([]) - : CombineLatestStream.list(streams); + : streams.combineLatestAll(); return combinedStream.switchMap((List values) { final Map combinedArgs = {}; @@ -180,6 +181,23 @@ class DataContext implements cf.ExecutionContext { } } +/// Resolves a context map definition against a [DataContext]. +/// +Future resolveContext( + DataContext dataContext, + JsonMap? contextDefinition, +) async { + final resolved = {}; + if (contextDefinition == null) return resolved; + + for (final MapEntry entry in contextDefinition.entries) { + final String key = entry.key; + final Object? value = entry.value; + resolved[key] = await dataContext.resolve(value).first; + } + return resolved; +} + /// Exception thrown when a value in the [DataModel] is not of the expected /// type. class DataModelTypeException implements Exception { diff --git a/packages/genui/lib/src/utils/validation_helper.dart b/packages/genui/lib/src/model/validation_helper.dart similarity index 94% rename from packages/genui/lib/src/utils/validation_helper.dart rename to packages/genui/lib/src/model/validation_helper.dart index eaf6262b7..731ce2e59 100644 --- a/packages/genui/lib/src/utils/validation_helper.dart +++ b/packages/genui/lib/src/model/validation_helper.dart @@ -2,11 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:rxdart/rxdart.dart'; - -import '../model/data_model.dart'; import '../primitives/simple_items.dart'; -import '../widgets/widget_utilities.dart'; +import '../utils/stream_extensions.dart'; +import 'data_model.dart'; /// A validation error with a message. class ValidationError { @@ -47,7 +45,7 @@ class ValidationHelper { ); } - return CombineLatestStream.list(streams).map((results) { + return streams.combineLatestAll().map((results) { for (final (isValid, msg) in results) { if (!isValid) return msg; } diff --git a/packages/genui/lib/src/utils/stream_extensions.dart b/packages/genui/lib/src/utils/stream_extensions.dart new file mode 100644 index 000000000..87e85d8e8 --- /dev/null +++ b/packages/genui/lib/src/utils/stream_extensions.dart @@ -0,0 +1,19 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:stream_transform/stream_transform.dart'; + +/// Extensions for [Iterable] of [Stream]s. +extension CombineLatestAll on Iterable> { + /// Combines all streams in this iterable into a single stream that emits a + /// list of the latest values from each stream. + /// + /// The resulting stream will not emit until every stream in the iterable has + /// emitted at least one value. + Stream> combineLatestAll() { + if (isEmpty) return Stream.value([]); + + return first.combineLatestAll(skip(1)); + } +} diff --git a/packages/genui/lib/src/widgets/widget_utilities.dart b/packages/genui/lib/src/widgets/widget_utilities.dart index b7ef26584..e356a785c 100644 --- a/packages/genui/lib/src/widgets/widget_utilities.dart +++ b/packages/genui/lib/src/widgets/widget_utilities.dart @@ -9,7 +9,8 @@ import 'package:flutter/material.dart'; import '../model/data_model.dart'; import '../primitives/logging.dart'; -import '../primitives/simple_items.dart'; + +export '../model/data_model.dart' show resolveContext; /// A builder widget that simplifies handling of nullable `ValueListenable`s. /// @@ -376,20 +377,3 @@ class _ToNumberNotifier extends ValueNotifier { super.dispose(); } } - -/// Resolves a context map definition against a [DataContext]. -/// -Future resolveContext( - DataContext dataContext, - JsonMap? contextDefinition, -) async { - final resolved = {}; - if (contextDefinition == null) return resolved; - - for (final MapEntry entry in contextDefinition.entries) { - final String key = entry.key; - final Object? value = entry.value; - resolved[key] = await dataContext.resolve(value).first; - } - return resolved; -} diff --git a/packages/genui/pubspec.yaml b/packages/genui/pubspec.yaml index 17141b3ea..79d7769fe 100644 --- a/packages/genui/pubspec.yaml +++ b/packages/genui/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: json_schema_builder: ^0.1.3 logging: ^1.3.0 meta: ^1.17.0 - rxdart: ^0.28.0 + stream_transform: ^2.1.1 url_launcher: ^6.3.2 uuid: ^4.4.0