Skip to content

[go_router_builder]: Handle invaild params #8405

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 26, 2025
Merged
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
4 changes: 4 additions & 0 deletions packages/go_router_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
253 changes: 147 additions & 106 deletions packages/go_router_builder/example/lib/all_types.g.dart

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions packages/go_router_builder/example/lib/main.g.dart

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

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

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

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

5 changes: 3 additions & 2 deletions packages/go_router_builder/example/lib/simple_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

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

71 changes: 71 additions & 0 deletions packages/go_router_builder/example/test/all_types_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
});
}
9 changes: 5 additions & 4 deletions packages/go_router_builder/lib/src/route_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>(pathParameter, value);
}),
);
Expand Down Expand Up @@ -752,7 +753,7 @@ const String _convertMapValueHelper = '''
T? $convertMapValueHelperName<T>(
String key,
Map<String, String> map,
T Function(String) converter,
T? Function(String) converter,
) {
final value = map[key];
return value == null ? null : converter(value);
Expand All @@ -774,8 +775,8 @@ bool $boolConverterHelperName(String value) {

const String _enumConverterHelper = '''
extension<T extends Enum> on Map<T, String> {
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 = '''
Expand Down
76 changes: 60 additions & 16 deletions packages/go_router_builder/lib/src/type_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,11 @@ String _stateValueAccess(ParameterElement element, Set<String> 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;
}
Expand All @@ -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) =>
Expand Down Expand Up @@ -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) =>
Expand All @@ -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) =>
Expand Down Expand Up @@ -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) =>
Expand All @@ -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) =>
Expand Down Expand Up @@ -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) =>
Expand All @@ -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);
}
}
Expand All @@ -300,24 +344,24 @@ 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 {}';
}
}

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)}]''';
Expand Down Expand Up @@ -373,7 +417,7 @@ abstract class _TypeHelperWithHelper extends _TypeHelper {
'${helperName(paramType)})';
}
return '${helperName(paramType)}'
'(state.${_stateValueAccess(parameterElement, pathParameters)})';
'(state.${_stateValueAccess(parameterElement, pathParameters)} ${!parameterElement.isRequired ? " ?? '' " : ''})!';
}
}

Expand Down
Loading