diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index f34047da732..6b864002673 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.1 + +- Fixes an issue when navigate to router with invalid params + ## 2.8.0 - Adds support for passing `preload` parameter to `StatefulShellBranchData`. diff --git a/packages/go_router_builder/example/lib/all_types.g.dart b/packages/go_router_builder/example/lib/all_types.g.dart index 001ff5e46b7..a30f03f62c6 100644 --- a/packages/go_router_builder/example/lib/all_types.g.dart +++ b/packages/go_router_builder/example/lib/all_types.g.dart @@ -92,9 +92,9 @@ extension $AllTypesBaseRouteExtension on AllTypesBaseRoute { extension $BigIntRouteExtension on BigIntRoute { static BigIntRoute _fromState(GoRouterState state) => BigIntRoute( requiredBigIntField: - BigInt.parse(state.pathParameters['requiredBigIntField']!), + BigInt.parse(state.pathParameters['requiredBigIntField']!)!, bigIntField: _$convertMapValue( - 'big-int-field', state.uri.queryParameters, BigInt.parse), + 'big-int-field', state.uri.queryParameters, BigInt.tryParse), ); String get location => GoRouteData.$location( @@ -117,7 +117,7 @@ extension $BigIntRouteExtension on BigIntRoute { extension $BoolRouteExtension on BoolRoute { static BoolRoute _fromState(GoRouterState state) => BoolRoute( requiredBoolField: - _$boolConverter(state.pathParameters['requiredBoolField']!), + _$boolConverter(state.pathParameters['requiredBoolField']!)!, boolField: _$convertMapValue( 'bool-field', state.uri.queryParameters, _$boolConverter), boolFieldWithDefaultValue: _$convertMapValue( @@ -150,9 +150,9 @@ extension $BoolRouteExtension on BoolRoute { extension $DateTimeRouteExtension on DateTimeRoute { static DateTimeRoute _fromState(GoRouterState state) => DateTimeRoute( requiredDateTimeField: - DateTime.parse(state.pathParameters['requiredDateTimeField']!), + DateTime.parse(state.pathParameters['requiredDateTimeField']!)!, dateTimeField: _$convertMapValue( - 'date-time-field', state.uri.queryParameters, DateTime.parse), + 'date-time-field', state.uri.queryParameters, DateTime.tryParse), ); String get location => GoRouteData.$location( @@ -176,9 +176,9 @@ extension $DateTimeRouteExtension on DateTimeRoute { extension $DoubleRouteExtension on DoubleRoute { static DoubleRoute _fromState(GoRouterState state) => DoubleRoute( requiredDoubleField: - double.parse(state.pathParameters['requiredDoubleField']!), + double.parse(state.pathParameters['requiredDoubleField']!)!, doubleField: _$convertMapValue( - 'double-field', state.uri.queryParameters, double.parse), + 'double-field', state.uri.queryParameters, double.tryParse), doubleFieldWithDefaultValue: _$convertMapValue( 'double-field-with-default-value', state.uri.queryParameters, @@ -208,9 +208,9 @@ extension $DoubleRouteExtension on DoubleRoute { extension $IntRouteExtension on IntRoute { static IntRoute _fromState(GoRouterState state) => IntRoute( - requiredIntField: int.parse(state.pathParameters['requiredIntField']!), + requiredIntField: int.parse(state.pathParameters['requiredIntField']!)!, intField: _$convertMapValue( - 'int-field', state.uri.queryParameters, int.parse), + 'int-field', state.uri.queryParameters, int.tryParse), intFieldWithDefaultValue: _$convertMapValue( 'int-field-with-default-value', state.uri.queryParameters, @@ -239,9 +239,9 @@ extension $IntRouteExtension on IntRoute { extension $NumRouteExtension on NumRoute { static NumRoute _fromState(GoRouterState state) => NumRoute( - requiredNumField: num.parse(state.pathParameters['requiredNumField']!), + requiredNumField: num.parse(state.pathParameters['requiredNumField']!)!, numField: _$convertMapValue( - 'num-field', state.uri.queryParameters, num.parse), + 'num-field', state.uri.queryParameters, num.tryParse), numFieldWithDefaultValue: _$convertMapValue( 'num-field-with-default-value', state.uri.queryParameters, @@ -271,7 +271,7 @@ extension $NumRouteExtension on NumRoute { extension $EnumRouteExtension on EnumRoute { static EnumRoute _fromState(GoRouterState state) => EnumRoute( requiredEnumField: _$PersonDetailsEnumMap - ._$fromName(state.pathParameters['requiredEnumField']!), + ._$fromName(state.pathParameters['requiredEnumField']!)!, enumField: _$convertMapValue('enum-field', state.uri.queryParameters, _$PersonDetailsEnumMap._$fromName), enumFieldWithDefaultValue: _$convertMapValue( @@ -311,7 +311,7 @@ const _$PersonDetailsEnumMap = { extension $EnhancedEnumRouteExtension on EnhancedEnumRoute { static EnhancedEnumRoute _fromState(GoRouterState state) => EnhancedEnumRoute( requiredEnumField: _$SportDetailsEnumMap - ._$fromName(state.pathParameters['requiredEnumField']!), + ._$fromName(state.pathParameters['requiredEnumField']!)!, enumField: _$convertMapValue('enum-field', state.uri.queryParameters, _$SportDetailsEnumMap._$fromName), enumFieldWithDefaultValue: _$convertMapValue( @@ -379,9 +379,9 @@ extension $StringRouteExtension on StringRoute { extension $UriRouteExtension on UriRoute { static UriRoute _fromState(GoRouterState state) => UriRoute( - requiredUriField: Uri.parse(state.pathParameters['requiredUriField']!), + requiredUriField: Uri.parse(state.pathParameters['requiredUriField']!)!, uriField: _$convertMapValue( - 'uri-field', state.uri.queryParameters, Uri.parse), + 'uri-field', state.uri.queryParameters, Uri.tryParse), ); String get location => GoRouteData.$location( @@ -403,59 +403,82 @@ extension $UriRouteExtension on UriRoute { extension $IterableRouteExtension on IterableRoute { static IterableRoute _fromState(GoRouterState state) => IterableRoute( - intIterableField: - state.uri.queryParametersAll['int-iterable-field']?.map(int.parse), - doubleIterableField: state - .uri.queryParametersAll['double-iterable-field'] - ?.map(double.parse), - stringIterableField: state - .uri.queryParametersAll['string-iterable-field'] - ?.map((e) => e), - boolIterableField: state.uri.queryParametersAll['bool-iterable-field'] - ?.map(_$boolConverter), - enumIterableField: state.uri.queryParametersAll['enum-iterable-field'] - ?.map(_$SportDetailsEnumMap._$fromName), - enumOnlyInIterableField: state - .uri.queryParametersAll['enum-only-in-iterable-field'] - ?.map(_$CookingRecipeEnumMap._$fromName), - intListField: state.uri.queryParametersAll['int-list-field'] - ?.map(int.parse) - .toList(), - doubleListField: state.uri.queryParametersAll['double-list-field'] - ?.map(double.parse) - .toList(), - stringListField: state.uri.queryParametersAll['string-list-field'] - ?.map((e) => e) - .toList(), - boolListField: state.uri.queryParametersAll['bool-list-field'] - ?.map(_$boolConverter) - .toList(), - enumListField: state.uri.queryParametersAll['enum-list-field'] - ?.map(_$SportDetailsEnumMap._$fromName) - .toList(), - enumOnlyInListField: state - .uri.queryParametersAll['enum-only-in-list-field'] - ?.map(_$CookingRecipeEnumMap._$fromName) - .toList(), - intSetField: state.uri.queryParametersAll['int-set-field'] + intIterableField: (state.uri.queryParametersAll['int-iterable-field'] ?.map(int.parse) - .toSet(), - doubleSetField: state.uri.queryParametersAll['double-set-field'] + .cast() as Iterable?), + doubleIterableField: (state + .uri.queryParametersAll['double-iterable-field'] ?.map(double.parse) - .toSet(), - stringSetField: state.uri.queryParametersAll['string-set-field'] - ?.map((e) => e) - .toSet(), - boolSetField: state.uri.queryParametersAll['bool-set-field'] + .cast() as Iterable?), + stringIterableField: (state + .uri.queryParametersAll['string-iterable-field'] + ?.map((e) => e)), + boolIterableField: (state.uri.queryParametersAll['bool-iterable-field'] ?.map(_$boolConverter) - .toSet(), - enumSetField: state.uri.queryParametersAll['enum-set-field'] + .cast() as Iterable?), + enumIterableField: (state.uri.queryParametersAll['enum-iterable-field'] ?.map(_$SportDetailsEnumMap._$fromName) - .toSet(), - enumOnlyInSetField: state - .uri.queryParametersAll['enum-only-in-set-field'] + .cast() as Iterable?), + enumOnlyInIterableField: (state + .uri.queryParametersAll['enum-only-in-iterable-field'] ?.map(_$CookingRecipeEnumMap._$fromName) - .toSet(), + .cast() as Iterable?), + intListField: (state.uri.queryParametersAll['int-list-field'] + ?.map(int.parse) + .cast() + ?.toList() as List?) + ?.toList(), + doubleListField: (state.uri.queryParametersAll['double-list-field'] + ?.map(double.parse) + .cast() + ?.toList() as List?) + ?.toList(), + stringListField: (state.uri.queryParametersAll['string-list-field'] + ?.map((e) => e))?.toList(), + boolListField: (state.uri.queryParametersAll['bool-list-field'] + ?.map(_$boolConverter) + .cast() + ?.toList() as List?) + ?.toList(), + enumListField: (state.uri.queryParametersAll['enum-list-field'] + ?.map(_$SportDetailsEnumMap._$fromName) + .cast() + ?.toList() as List?) + ?.toList(), + enumOnlyInListField: (state + .uri.queryParametersAll['enum-only-in-list-field'] + ?.map(_$CookingRecipeEnumMap._$fromName) + .cast() + ?.toList() as List?) + ?.toList(), + intSetField: (state.uri.queryParametersAll['int-set-field'] + ?.map(int.parse) + .cast() + ?.toSet() as Set?) + ?.toSet(), + doubleSetField: (state.uri.queryParametersAll['double-set-field'] + ?.map(double.parse) + .cast() + ?.toSet() as Set?) + ?.toSet(), + stringSetField: (state.uri.queryParametersAll['string-set-field'] + ?.map((e) => e))?.toSet(), + boolSetField: (state.uri.queryParametersAll['bool-set-field'] + ?.map(_$boolConverter) + .cast() + ?.toSet() as Set?) + ?.toSet(), + enumSetField: (state.uri.queryParametersAll['enum-set-field'] + ?.map(_$SportDetailsEnumMap._$fromName) + .cast() + ?.toSet() as Set?) + ?.toSet(), + enumOnlyInSetField: (state + .uri.queryParametersAll['enum-only-in-set-field'] + ?.map(_$CookingRecipeEnumMap._$fromName) + .cast() + ?.toSet() as Set?) + ?.toSet(), ); String get location => GoRouteData.$location( @@ -536,62 +559,80 @@ extension $IterableRouteWithDefaultValuesExtension on IterableRouteWithDefaultValues { static IterableRouteWithDefaultValues _fromState(GoRouterState state) => IterableRouteWithDefaultValues( - intIterableField: state.uri.queryParametersAll['int-iterable-field'] - ?.map(int.parse) ?? + intIterableField: (state.uri.queryParametersAll['int-iterable-field'] + ?.map(int.parse) + .cast() as Iterable?) ?? const [0], - doubleIterableField: state + doubleIterableField: (state .uri.queryParametersAll['double-iterable-field'] - ?.map(double.parse) ?? + ?.map(double.parse) + .cast() as Iterable?) ?? const [0, 1, 2], - stringIterableField: state + stringIterableField: (state .uri.queryParametersAll['string-iterable-field'] - ?.map((e) => e) ?? + ?.map((e) => e)) ?? const ['defaultValue'], - boolIterableField: state.uri.queryParametersAll['bool-iterable-field'] - ?.map(_$boolConverter) ?? + boolIterableField: (state.uri.queryParametersAll['bool-iterable-field'] + ?.map(_$boolConverter) + .cast() as Iterable?) ?? const [false], - enumIterableField: state.uri.queryParametersAll['enum-iterable-field'] - ?.map(_$SportDetailsEnumMap._$fromName) ?? + enumIterableField: (state.uri.queryParametersAll['enum-iterable-field'] + ?.map(_$SportDetailsEnumMap._$fromName) + .cast() as Iterable?) ?? const [SportDetails.tennis, SportDetails.hockey], - intListField: state.uri.queryParametersAll['int-list-field'] - ?.map(int.parse) - .toList() ?? + intListField: (state.uri.queryParametersAll['int-list-field'] + ?.map(int.parse) + .cast() + ?.toList() as List?) + ?.toList() ?? const [0], - doubleListField: state.uri.queryParametersAll['double-list-field'] - ?.map(double.parse) - .toList() ?? + doubleListField: (state.uri.queryParametersAll['double-list-field'] + ?.map(double.parse) + .cast() + ?.toList() as List?) + ?.toList() ?? const [1, 2, 3], - stringListField: state.uri.queryParametersAll['string-list-field'] - ?.map((e) => e) - .toList() ?? + stringListField: (state.uri.queryParametersAll['string-list-field'] + ?.map((e) => e))?.toList() ?? const ['defaultValue0', 'defaultValue1'], - boolListField: state.uri.queryParametersAll['bool-list-field'] - ?.map(_$boolConverter) - .toList() ?? + boolListField: (state.uri.queryParametersAll['bool-list-field'] + ?.map(_$boolConverter) + .cast() + ?.toList() as List?) + ?.toList() ?? const [true], - enumListField: state.uri.queryParametersAll['enum-list-field'] - ?.map(_$SportDetailsEnumMap._$fromName) - .toList() ?? + enumListField: (state.uri.queryParametersAll['enum-list-field'] + ?.map(_$SportDetailsEnumMap._$fromName) + .cast() + ?.toList() as List?) + ?.toList() ?? const [SportDetails.football], - intSetField: state.uri.queryParametersAll['int-set-field'] - ?.map(int.parse) - .toSet() ?? + intSetField: (state.uri.queryParametersAll['int-set-field'] + ?.map(int.parse) + .cast() + ?.toSet() as Set?) + ?.toSet() ?? const {0, 1}, - doubleSetField: state.uri.queryParametersAll['double-set-field'] - ?.map(double.parse) - .toSet() ?? + doubleSetField: (state.uri.queryParametersAll['double-set-field'] + ?.map(double.parse) + .cast() + ?.toSet() as Set?) + ?.toSet() ?? const {}, - stringSetField: state.uri.queryParametersAll['string-set-field'] - ?.map((e) => e) - .toSet() ?? + stringSetField: (state.uri.queryParametersAll['string-set-field'] + ?.map((e) => e))?.toSet() ?? const {'defaultValue'}, - boolSetField: state.uri.queryParametersAll['bool-set-field'] - ?.map(_$boolConverter) - .toSet() ?? + boolSetField: (state.uri.queryParametersAll['bool-set-field'] + ?.map(_$boolConverter) + .cast() + ?.toSet() as Set?) + ?.toSet() ?? const {true, false}, - enumSetField: state.uri.queryParametersAll['enum-set-field'] - ?.map(_$SportDetailsEnumMap._$fromName) - .toSet() ?? + enumSetField: (state.uri.queryParametersAll['enum-set-field'] + ?.map(_$SportDetailsEnumMap._$fromName) + .cast() + ?.toSet() as Set?) + ?.toSet() ?? const {SportDetails.hockey}, ); @@ -657,7 +698,7 @@ extension $IterableRouteWithDefaultValuesExtension T? _$convertMapValue( String key, Map map, - T Function(String) converter, + T? Function(String) converter, ) { final value = map[key]; return value == null ? null : converter(value); @@ -675,8 +716,8 @@ bool _$boolConverter(String value) { } extension on Map { - T _$fromName(String value) => - entries.singleWhere((element) => element.value == value).key; + T? _$fromName(String value) => + entries.where((element) => element.value == value).firstOrNull?.key; } bool _$iterablesEqual(Iterable? iterable1, Iterable? iterable2) { diff --git a/packages/go_router_builder/example/lib/main.g.dart b/packages/go_router_builder/example/lib/main.g.dart index 88c4f0ac84f..e489f48ad7d 100644 --- a/packages/go_router_builder/example/lib/main.g.dart +++ b/packages/go_router_builder/example/lib/main.g.dart @@ -79,7 +79,7 @@ extension $FamilyRouteExtension on FamilyRoute { extension $PersonRouteExtension on PersonRoute { static PersonRoute _fromState(GoRouterState state) => PersonRoute( state.pathParameters['fid']!, - int.parse(state.pathParameters['pid']!), + int.parse(state.pathParameters['pid']!)!, ); String get location => GoRouteData.$location( @@ -100,8 +100,8 @@ extension $PersonDetailsRouteExtension on PersonDetailsRoute { static PersonDetailsRoute _fromState(GoRouterState state) => PersonDetailsRoute( state.pathParameters['fid']!, - int.parse(state.pathParameters['pid']!), - _$PersonDetailsEnumMap._$fromName(state.pathParameters['details']!), + int.parse(state.pathParameters['pid']!)!, + _$PersonDetailsEnumMap._$fromName(state.pathParameters['details']!)!, $extra: state.extra as int?, ); @@ -129,7 +129,7 @@ const _$PersonDetailsEnumMap = { extension $FamilyCountRouteExtension on FamilyCountRoute { static FamilyCountRoute _fromState(GoRouterState state) => FamilyCountRoute( - int.parse(state.pathParameters['count']!), + int.parse(state.pathParameters['count']!)!, ); String get location => GoRouteData.$location( @@ -147,8 +147,8 @@ extension $FamilyCountRouteExtension on FamilyCountRoute { } extension on Map { - T _$fromName(String value) => - entries.singleWhere((element) => element.value == value).key; + T? _$fromName(String value) => + entries.where((element) => element.value == value).firstOrNull?.key; } RouteBase get $loginRoute => GoRouteData.$route( diff --git a/packages/go_router_builder/example/lib/readme_excerpts.g.dart b/packages/go_router_builder/example/lib/readme_excerpts.g.dart index f6ec99f9d2c..f6719844df6 100644 --- a/packages/go_router_builder/example/lib/readme_excerpts.g.dart +++ b/packages/go_router_builder/example/lib/readme_excerpts.g.dart @@ -46,11 +46,11 @@ extension $HomeRouteExtension on HomeRoute { extension $FamilyRouteExtension on FamilyRoute { static FamilyRoute _fromState(GoRouterState state) => FamilyRoute( - fid: state.pathParameters['fid']!, + fid: state.pathParameters['fid'], ); String get location => GoRouteData.$location( - '/family/${Uri.encodeComponent(fid)}', + '/family/${Uri.encodeComponent(fid ?? '')}', ); void go(BuildContext context) => context.go(location); @@ -154,7 +154,7 @@ RouteBase get $hotdogRouteWithEverything => GoRouteData.$route( extension $HotdogRouteWithEverythingExtension on HotdogRouteWithEverything { static HotdogRouteWithEverything _fromState(GoRouterState state) => HotdogRouteWithEverything( - _$boolConverter(state.pathParameters['ketchup']!), + _$boolConverter(state.pathParameters['ketchup']!)!, state.uri.queryParameters['mustard'], state.extra as Sauce, ); diff --git a/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart b/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart index 6151889582d..4a4cb4ff9a2 100644 --- a/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart +++ b/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart @@ -76,7 +76,7 @@ extension $UsersRouteDataExtension on UsersRouteData { extension $UserRouteDataExtension on UserRouteData { static UserRouteData _fromState(GoRouterState state) => UserRouteData( - id: int.parse(state.pathParameters['id']!), + id: int.parse(state.pathParameters['id']!)!, ); String get location => GoRouteData.$location( diff --git a/packages/go_router_builder/example/lib/shell_route_with_observers_example.g.dart b/packages/go_router_builder/example/lib/shell_route_with_observers_example.g.dart index 5737eb472a3..52d3c58f1cb 100644 --- a/packages/go_router_builder/example/lib/shell_route_with_observers_example.g.dart +++ b/packages/go_router_builder/example/lib/shell_route_with_observers_example.g.dart @@ -75,7 +75,7 @@ extension $UsersRouteDataExtension on UsersRouteData { extension $UserRouteDataExtension on UserRouteData { static UserRouteData _fromState(GoRouterState state) => UserRouteData( - id: int.parse(state.pathParameters['id']!), + id: int.parse(state.pathParameters['id']!)!, ); String get location => GoRouteData.$location( diff --git a/packages/go_router_builder/example/lib/simple_example.dart b/packages/go_router_builder/example/lib/simple_example.dart index b977a296cb4..bfef272b186 100644 --- a/packages/go_router_builder/example/lib/simple_example.dart +++ b/packages/go_router_builder/example/lib/simple_example.dart @@ -45,8 +45,9 @@ class FamilyRoute extends GoRouteData { final String familyId; @override - Widget build(BuildContext context, GoRouterState state) => - FamilyScreen(family: familyById(familyId)); + Widget build(BuildContext context, GoRouterState state) { + return FamilyScreen(family: familyById(familyId)); + } } class HomeScreen extends StatelessWidget { diff --git a/packages/go_router_builder/example/lib/stateful_shell_route_initial_location_example.g.dart b/packages/go_router_builder/example/lib/stateful_shell_route_initial_location_example.g.dart index 5b398e07990..11aa3efae3c 100644 --- a/packages/go_router_builder/example/lib/stateful_shell_route_initial_location_example.g.dart +++ b/packages/go_router_builder/example/lib/stateful_shell_route_initial_location_example.g.dart @@ -69,7 +69,7 @@ extension $NotificationsRouteDataExtension on NotificationsRouteData { static NotificationsRouteData _fromState(GoRouterState state) => NotificationsRouteData( section: _$NotificationsPageSectionEnumMap - ._$fromName(state.pathParameters['section']!), + ._$fromName(state.pathParameters['section']!)!, ); String get location => GoRouteData.$location( @@ -111,6 +111,6 @@ extension $OrdersRouteDataExtension on OrdersRouteData { } extension on Map { - T _$fromName(String value) => - entries.singleWhere((element) => element.value == value).key; + T? _$fromName(String value) => + entries.where((element) => element.value == value).firstOrNull?.key; } diff --git a/packages/go_router_builder/example/test/all_types_test.dart b/packages/go_router_builder/example/test/all_types_test.dart index eafcdeefda0..89774f7f623 100644 --- a/packages/go_router_builder/example/test/all_types_test.dart +++ b/packages/go_router_builder/example/test/all_types_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:go_router_builder_example/all_types.dart'; import 'package:go_router_builder_example/shared/data.dart'; @@ -221,4 +222,74 @@ void main() { ); expect(find.text('/iterable-route-with-default-values'), findsOneWidget); }); + + testWidgets( + 'Test navigation with invalid query and path parameters using Uri.parse', + (WidgetTester tester) async { + await tester.pumpWidget(AllTypesApp()); + + final ScaffoldState scaffoldState = + tester.firstState(find.byType(Scaffold)); + + // Test invalid BigInt parameter + scaffoldState.context + .go(Uri.parse('/big-int-route/4?bigIntField=invalid').toString()); + await tester.pumpAndSettle(); + expect(find.text('BigIntRoute'), findsOneWidget); + expect(find.text('Param: 4'), findsOneWidget); + expect(find.text('Query param: null'), findsOneWidget); + + // Test invalid DateTime parameter + scaffoldState.context.go(Uri.parse( + '/date-time-route/2021-01-01T00:00:00.000?dateTimeField=invalid-date') + .toString()); + await tester.pumpAndSettle(); + expect(find.text('DateTimeRoute'), findsOneWidget); + expect(find.text('Param: 2021-01-01 00:00:00.000'), findsOneWidget); + expect(find.text('Query param: null'), findsOneWidget); + + // Test invalid Double parameter + scaffoldState.context + .go(Uri.parse('/double-route/3.14?doubleField=invalid').toString()); + await tester.pumpAndSettle(); + expect(find.text('DoubleRoute'), findsOneWidget); + expect(find.text('Param: 3.14'), findsOneWidget); + expect(find.text('Query param: null'), findsOneWidget); + expect(find.text('Query param with default value: 1.0'), findsOneWidget); + + // Test invalid Int parameter + scaffoldState.context + .go(Uri.parse('/int-route/65?intField=invalid').toString()); + await tester.pumpAndSettle(); + expect(find.text('IntRoute'), findsOneWidget); + expect(find.text('Param: 65'), findsOneWidget); + expect(find.text('Query param: null'), findsOneWidget); + expect(find.text('Query param with default value: 1'), findsOneWidget); + + // Test invalid Uri parameter + scaffoldState.context.go( + Uri.parse('/uri-route/https%3A%2F%2Fdart.dev?uriField=invalid-uri') + .toString()); + await tester.pumpAndSettle(); + expect(find.text('UriRoute'), findsOneWidget); + expect(find.text('Param: https://dart.dev'), findsOneWidget); + expect(find.text('Query param: null'), findsOneWidget); + + // Test invalid Enum parameter + scaffoldState.context.go( + Uri.parse('/enum-route/favorite-food?enum-field=invalid').toString()); + await tester.pumpAndSettle(); + expect(find.text('EnumRoute'), findsOneWidget); + expect(find.text('Query param: null'), findsOneWidget); + expect( + find.text('Query param with default value: PersonDetails.favoriteFood'), + findsOneWidget); + + // Test invalid Iterable parameter + scaffoldState.context + .go(Uri.parse('/iterable-route?intListField=invalid').toString()); + await tester.pumpAndSettle(); + expect(find.text('IterableRoute'), findsOneWidget); + expect(find.text('/iterable-route'), findsOneWidget); + }); } diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 94212051ff0..d272335db28 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -231,8 +231,9 @@ class GoRouteConfig extends RouteBaseConfig { // Enum types are encoded using a map, so we need a nullability check // here to ensure it matches Uri.encodeComponent nullability final DartType? type = _field(pathParameter)?.returnType; + final String value = - '\${Uri.encodeComponent(${_encodeFor(pathParameter)}${type?.isEnum ?? false ? '!' : ''})}'; + '\${Uri.encodeComponent(${_encodeFor(pathParameter)}${(type?.isEnum ?? false) ? '!' : (type?.isNullableType ?? false) ? "?? ''" : ''})}'; return MapEntry(pathParameter, value); }), ); @@ -752,7 +753,7 @@ const String _convertMapValueHelper = ''' T? $convertMapValueHelperName( String key, Map map, - T Function(String) converter, + T? Function(String) converter, ) { final value = map[key]; return value == null ? null : converter(value); @@ -774,8 +775,8 @@ bool $boolConverterHelperName(String value) { const String _enumConverterHelper = ''' extension on Map { - T $enumExtensionHelperName(String value) => - entries.singleWhere((element) => element.value == value).key; + T? $enumExtensionHelperName(String value) => + entries.where((element) => element.value == value).firstOrNull?.key; }'''; const String _iterableEqualsHelper = ''' diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index a822ea86280..6a13c146930 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -116,14 +116,11 @@ String _stateValueAccess(ParameterElement element, Set pathParameters) { late String access; if (pathParameters.contains(element.name)) { - access = 'pathParameters[${escapeDartString(element.name)}]'; + access = + 'pathParameters[${escapeDartString(element.name)}]${element.isRequired ? '!' : ''}'; } else { access = 'uri.queryParameters[${escapeDartString(element.name.kebab)}]'; } - if (pathParameters.contains(element.name) || - (!element.type.isNullableType && !element.hasDefaultValue)) { - access += '!'; - } return access; } @@ -146,7 +143,12 @@ class _TypeHelperBigInt extends _TypeHelperWithHelper { const _TypeHelperBigInt(); @override - String helperName(DartType paramType) => 'BigInt.parse'; + String helperName(DartType paramType) { + if (paramType.isNullableType) { + return 'BigInt.tryParse'; + } + return 'BigInt.parse'; + } @override String _encode(String fieldName, DartType type) => @@ -175,7 +177,12 @@ class _TypeHelperDateTime extends _TypeHelperWithHelper { const _TypeHelperDateTime(); @override - String helperName(DartType paramType) => 'DateTime.parse'; + String helperName(DartType paramType) { + if (paramType.isNullableType) { + return 'DateTime.tryParse'; + } + return 'DateTime.parse'; + } @override String _encode(String fieldName, DartType type) => @@ -190,7 +197,12 @@ class _TypeHelperDouble extends _TypeHelperWithHelper { const _TypeHelperDouble(); @override - String helperName(DartType paramType) => 'double.parse'; + String helperName(DartType paramType) { + if (paramType.isNullableType) { + return 'double.tryParse'; + } + return 'double.parse'; + } @override String _encode(String fieldName, DartType type) => @@ -219,7 +231,12 @@ class _TypeHelperInt extends _TypeHelperWithHelper { const _TypeHelperInt(); @override - String helperName(DartType paramType) => 'int.parse'; + String helperName(DartType paramType) { + if (paramType.isNullableType) { + return 'int.tryParse'; + } + return 'int.parse'; + } @override String _encode(String fieldName, DartType type) => @@ -233,7 +250,12 @@ class _TypeHelperNum extends _TypeHelperWithHelper { const _TypeHelperNum(); @override - String helperName(DartType paramType) => 'num.parse'; + String helperName(DartType paramType) { + if (paramType.isNullableType) { + return 'num.tryParse'; + } + return 'num.parse'; + } @override String _encode(String fieldName, DartType type) => @@ -262,7 +284,12 @@ class _TypeHelperUri extends _TypeHelperWithHelper { const _TypeHelperUri(); @override - String helperName(DartType paramType) => 'Uri.parse'; + String helperName(DartType paramType) { + if (paramType.isNullableType) { + return 'Uri.tryParse'; + } + return 'Uri.parse'; + } @override String _encode(String fieldName, DartType type) => @@ -288,9 +315,26 @@ class _TypeHelperIterable extends _TypeHelperWithHelper { // get a type converter for values in iterable String entriesTypeDecoder = '(e) => e'; + String convertToNotNull = ''; + String formatIterableType = ''; + String asParameterType = ' as ${parameterElement.type}'; + + if (parameterElement.hasDefaultValue) { + asParameterType += '?'; + } + for (final _TypeHelper helper in _helpers) { if (helper._matchesType(iterableType) && helper is _TypeHelperWithHelper) { + if (!iterableType.isNullableType) { + if (parameterElement.type.isDartCoreList) { + formatIterableType = '?.toList()'; + } else if (parameterElement.type.isDartCoreSet) { + formatIterableType = '?.toSet()'; + } + convertToNotNull = + '.cast<$iterableType>()$formatIterableType$asParameterType'; + } entriesTypeDecoder = helper.helperName(iterableType); } } @@ -300,14 +344,14 @@ class _TypeHelperIterable extends _TypeHelperWithHelper { String fallBack = ''; if (const TypeChecker.fromRuntime(List) .isAssignableFromType(parameterElement.type)) { - iterableCaster = '.toList()'; + iterableCaster += '?.toList()'; if (!parameterElement.type.isNullableType && !parameterElement.hasDefaultValue) { fallBack = '?? const []'; } } else if (const TypeChecker.fromRuntime(Set) .isAssignableFromType(parameterElement.type)) { - iterableCaster = '.toSet()'; + iterableCaster += '?.toSet()'; if (!parameterElement.type.isNullableType && !parameterElement.hasDefaultValue) { fallBack = '?? const {}'; @@ -315,9 +359,9 @@ class _TypeHelperIterable extends _TypeHelperWithHelper { } return ''' -state.uri.queryParametersAll[ +(state.uri.queryParametersAll[ ${escapeDartString(parameterElement.name.kebab)}] - ?.map($entriesTypeDecoder)$iterableCaster$fallBack'''; + ?.map($entriesTypeDecoder)$convertToNotNull)$iterableCaster$fallBack'''; } return ''' state.uri.queryParametersAll[${escapeDartString(parameterElement.name.kebab)}]'''; @@ -373,7 +417,7 @@ abstract class _TypeHelperWithHelper extends _TypeHelper { '${helperName(paramType)})'; } return '${helperName(paramType)}' - '(state.${_stateValueAccess(parameterElement, pathParameters)})'; + '(state.${_stateValueAccess(parameterElement, pathParameters)} ${!parameterElement.isRequired ? " ?? '' " : ''})!'; } } diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 113a88619f1..6ef52a5819b 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 2.8.0 +version: 2.8.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 diff --git a/packages/go_router_builder/test_inputs/default_value.dart.expect b/packages/go_router_builder/test_inputs/default_value.dart.expect index af5ce08b341..9d9d5e8ec83 100644 --- a/packages/go_router_builder/test_inputs/default_value.dart.expect +++ b/packages/go_router_builder/test_inputs/default_value.dart.expect @@ -30,7 +30,7 @@ extension $DefaultValueRouteExtension on DefaultValueRoute { T? _$convertMapValue( String key, Map map, - T Function(String) converter, + T? Function(String) converter, ) { final value = map[key]; return value == null ? null : converter(value); diff --git a/packages/go_router_builder/test_inputs/enum_parameter.dart.expect b/packages/go_router_builder/test_inputs/enum_parameter.dart.expect index c7dcba7146d..97c63224b35 100644 --- a/packages/go_router_builder/test_inputs/enum_parameter.dart.expect +++ b/packages/go_router_builder/test_inputs/enum_parameter.dart.expect @@ -5,7 +5,7 @@ RouteBase get $enumParam => GoRouteData.$route( extension $EnumParamExtension on EnumParam { static EnumParam _fromState(GoRouterState state) => EnumParam( - y: _$EnumTestEnumMap._$fromName(state.pathParameters['y']!), + y: _$EnumTestEnumMap._$fromName(state.pathParameters['y']!)!, ); String get location => GoRouteData.$location( @@ -29,6 +29,6 @@ const _$EnumTestEnumMap = { }; extension on Map { - T _$fromName(String value) => - entries.singleWhere((element) => element.value == value).key; + T? _$fromName(String value) => + entries.where((element) => element.value == value).firstOrNull?.key; } diff --git a/packages/go_router_builder/test_inputs/extra_value.dart.expect b/packages/go_router_builder/test_inputs/extra_value.dart.expect index a3c02e4cbf9..fccced1ce01 100644 --- a/packages/go_router_builder/test_inputs/extra_value.dart.expect +++ b/packages/go_router_builder/test_inputs/extra_value.dart.expect @@ -33,7 +33,7 @@ extension $ExtraValueRouteExtension on ExtraValueRoute { T? _$convertMapValue( String key, Map map, - T Function(String) converter, + T? Function(String) converter, ) { final value = map[key]; return value == null ? null : converter(value); diff --git a/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect b/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect index 625e92c0984..683a64ec7ca 100644 --- a/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect +++ b/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect @@ -6,7 +6,9 @@ RouteBase get $iterableDefaultValueRoute => GoRouteData.$route( extension $IterableDefaultValueRouteExtension on IterableDefaultValueRoute { static IterableDefaultValueRoute _fromState(GoRouterState state) => IterableDefaultValueRoute( - param: state.uri.queryParametersAll['param']?.map(int.parse) ?? + param: (state.uri.queryParametersAll['param'] + ?.map(int.parse) + .cast() as Iterable?) ?? const [0], ); diff --git a/packages/go_router_builder/test_inputs/iterable_with_enum.dart.expect b/packages/go_router_builder/test_inputs/iterable_with_enum.dart.expect index 048909fbff1..8d20676242f 100644 --- a/packages/go_router_builder/test_inputs/iterable_with_enum.dart.expect +++ b/packages/go_router_builder/test_inputs/iterable_with_enum.dart.expect @@ -6,8 +6,10 @@ RouteBase get $iterableWithEnumRoute => GoRouteData.$route( extension $IterableWithEnumRouteExtension on IterableWithEnumRoute { static IterableWithEnumRoute _fromState(GoRouterState state) => IterableWithEnumRoute( - param: state.uri.queryParametersAll['param'] - ?.map(_$EnumOnlyUsedInIterableEnumMap._$fromName), + param: (state.uri.queryParametersAll['param'] + ?.map(_$EnumOnlyUsedInIterableEnumMap._$fromName) + .cast() + as Iterable?), ); String get location => GoRouteData.$location( @@ -36,6 +38,6 @@ const _$EnumOnlyUsedInIterableEnumMap = { }; extension on Map { - T _$fromName(String value) => - entries.singleWhere((element) => element.value == value).key; + T? _$fromName(String value) => + entries.where((element) => element.value == value).firstOrNull?.key; } diff --git a/packages/go_router_builder/test_inputs/list.dart.expect b/packages/go_router_builder/test_inputs/list.dart.expect index 3c1e5a726fb..bd756314184 100644 --- a/packages/go_router_builder/test_inputs/list.dart.expect +++ b/packages/go_router_builder/test_inputs/list.dart.expect @@ -5,15 +5,23 @@ RouteBase get $listRoute => GoRouteData.$route( extension $ListRouteExtension on ListRoute { static ListRoute _fromState(GoRouterState state) => ListRoute( - ids: state.uri.queryParametersAll['ids']?.map(int.parse).toList() ?? + ids: (state.uri.queryParametersAll['ids'] + ?.map(int.parse) + .cast() + ?.toList() as List) + ?.toList() ?? const [], - nullableIds: state.uri.queryParametersAll['nullable-ids'] - ?.map(int.parse) - .toList(), - idsWithDefaultValue: state - .uri.queryParametersAll['ids-with-default-value'] + nullableIds: (state.uri.queryParametersAll['nullable-ids'] ?.map(int.parse) - .toList() ?? + .cast() + ?.toList() as List?) + ?.toList(), + idsWithDefaultValue: (state + .uri.queryParametersAll['ids-with-default-value'] + ?.map(int.parse) + .cast() + ?.toList() as List?) + ?.toList() ?? const [0], ); diff --git a/packages/go_router_builder/test_inputs/required_parameters_not_in_path_can_be_null.dart.expect b/packages/go_router_builder/test_inputs/required_parameters_not_in_path_can_be_null.dart.expect index 0e27dc9e5ff..26ab06e90c2 100644 --- a/packages/go_router_builder/test_inputs/required_parameters_not_in_path_can_be_null.dart.expect +++ b/packages/go_router_builder/test_inputs/required_parameters_not_in_path_can_be_null.dart.expect @@ -7,7 +7,7 @@ extension $NullableRequiredParamNotInPathExtension on NullableRequiredParamNotInPath { static NullableRequiredParamNotInPath _fromState(GoRouterState state) => NullableRequiredParamNotInPath( - id: _$convertMapValue('id', state.uri.queryParameters, int.parse), + id: _$convertMapValue('id', state.uri.queryParameters, int.tryParse), ); String get location => GoRouteData.$location( @@ -30,7 +30,7 @@ extension $NullableRequiredParamNotInPathExtension T? _$convertMapValue( String key, Map map, - T Function(String) converter, + T? Function(String) converter, ) { final value = map[key]; return value == null ? null : converter(value); diff --git a/packages/go_router_builder/test_inputs/required_query_parameter.dart.expect b/packages/go_router_builder/test_inputs/required_query_parameter.dart.expect index 09d957cb075..1b62bee0e52 100644 --- a/packages/go_router_builder/test_inputs/required_query_parameter.dart.expect +++ b/packages/go_router_builder/test_inputs/required_query_parameter.dart.expect @@ -7,7 +7,7 @@ extension $NonNullableRequiredParamNotInPathExtension on NonNullableRequiredParamNotInPath { static NonNullableRequiredParamNotInPath _fromState(GoRouterState state) => NonNullableRequiredParamNotInPath( - id: int.parse(state.uri.queryParameters['id']!), + id: int.parse(state.uri.queryParameters['id'])!, ); String get location => GoRouteData.$location( diff --git a/packages/go_router_builder/test_inputs/set.dart.expect b/packages/go_router_builder/test_inputs/set.dart.expect index d33aa7e7c67..a8f577bdb00 100644 --- a/packages/go_router_builder/test_inputs/set.dart.expect +++ b/packages/go_router_builder/test_inputs/set.dart.expect @@ -5,15 +5,23 @@ RouteBase get $setRoute => GoRouteData.$route( extension $SetRouteExtension on SetRoute { static SetRoute _fromState(GoRouterState state) => SetRoute( - ids: state.uri.queryParametersAll['ids']?.map(int.parse).toSet() ?? + ids: (state.uri.queryParametersAll['ids'] + ?.map(int.parse) + .cast() + ?.toSet() as Set) + ?.toSet() ?? const {}, - nullableIds: state.uri.queryParametersAll['nullable-ids'] - ?.map(int.parse) - .toSet(), - idsWithDefaultValue: state - .uri.queryParametersAll['ids-with-default-value'] + nullableIds: (state.uri.queryParametersAll['nullable-ids'] ?.map(int.parse) - .toSet() ?? + .cast() + ?.toSet() as Set?) + ?.toSet(), + idsWithDefaultValue: (state + .uri.queryParametersAll['ids-with-default-value'] + ?.map(int.parse) + .cast() + ?.toSet() as Set?) + ?.toSet() ?? const {0}, ); diff --git a/packages/go_router_builder/test_inputs/shell_route_data.dart.expect b/packages/go_router_builder/test_inputs/shell_route_data.dart.expect index a5778302090..106211100ed 100644 --- a/packages/go_router_builder/test_inputs/shell_route_data.dart.expect +++ b/packages/go_router_builder/test_inputs/shell_route_data.dart.expect @@ -27,4 +27,4 @@ extension $ShellRouteWithRestorationScopeIdExtension on ShellRouteWithRestorationScopeId { static ShellRouteWithRestorationScopeId _fromState(GoRouterState state) => const ShellRouteWithRestorationScopeId(); -} \ No newline at end of file +}