Skip to content
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.1.0

* Supports required/positional parameters that are not in the path.

## 2.0.2

* Fixes unawaited_futures violations.
Expand Down
8 changes: 4 additions & 4 deletions packages/go_router_builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ class HomeRoute extends GoRouteData {
}
```

Required parameters are pulled from the route's `path` defined in the route
tree.

## Route tree

The tree of routes is defined as an attribute on each of the top-level routes:
Expand Down Expand Up @@ -178,9 +175,10 @@ void _tap() async {

## Query parameters

Optional parameters (named or positional) indicate query parameters:
Parameters (named or positional) not listed in the path of `TypedGoRoute` indicate query parameters:

```dart
@TypedGoRoute(path: '/login')
class LoginRoute extends GoRouteData {
LoginRoute({this.from});
final String? from;
Expand All @@ -195,6 +193,7 @@ class LoginRoute extends GoRouteData {
For query parameters with a **non-nullable** type, you can define a default value:

```dart
@TypedGoRoute(path: '/my-route')
class MyRoute extends GoRouteData {
MyRoute({this.queryParameter = 'defaultValue'});
final String queryParameter;
Expand Down Expand Up @@ -237,6 +236,7 @@ recommended when targeting Flutter web.
You can, of course, combine the use of path, query and $extra parameters:

```dart
@TypedGoRoute<HotdogRouteWithEverything>(path: '/:ketchup')
class HotdogRouteWithEverything extends GoRouteData {
HotdogRouteWithEverything(this.ketchup, this.mustard, this.$extra);
final bool ketchup; // required path parameter
Expand Down
14 changes: 4 additions & 10 deletions packages/go_router_builder/lib/src/route_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -369,21 +369,15 @@ GoRouteData.\$route(

String _decodeFor(ParameterElement element) {
if (element.isRequired) {
if (element.type.nullabilitySuffix == NullabilitySuffix.question) {
if (element.type.nullabilitySuffix == NullabilitySuffix.question &&
_pathParams.contains(element.name)) {
throw InvalidGenerationSourceError(
'Required parameters cannot be nullable.',
element: element,
);
}

if (!_pathParams.contains(element.name) && !element.isExtraField) {
throw InvalidGenerationSourceError(
'Missing param `${element.name}` in path.',
'Required parameters in the path cannot be nullable.',
element: element,
);
}
}
final String fromStateExpression = decodeParameter(element);
final String fromStateExpression = decodeParameter(element, _pathParams);

if (element.isPositional) {
return '$fromStateExpression,';
Expand Down
47 changes: 26 additions & 21 deletions packages/go_router_builder/lib/src/type_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ const List<_TypeHelper> _helpers = <_TypeHelper>[
/// Returns the decoded [String] value for [element], if its type is supported.
///
/// Otherwise, throws an [InvalidGenerationSourceError].
String decodeParameter(ParameterElement element) {
String decodeParameter(ParameterElement element, Set<String> pathParameters) {
if (element.isExtraField) {
return 'state.${_stateValueAccess(element)}';
return 'state.${_stateValueAccess(element, pathParameters)}';
}

final DartType paramType = element.type;
for (final _TypeHelper helper in _helpers) {
if (helper._matchesType(paramType)) {
String decoded = helper._decode(element);
String decoded = helper._decode(element, pathParameters);
if (element.isOptional && element.hasDefaultValue) {
if (element.type.isNullableType) {
throw NullableDefaultValueError(element);
Expand Down Expand Up @@ -92,30 +92,30 @@ String encodeField(PropertyAccessorElement element) {
// ignore: deprecated_member_use
String enumMapName(InterfaceType type) => '_\$${type.element.name}EnumMap';

String _stateValueAccess(ParameterElement element) {
String _stateValueAccess(ParameterElement element, Set<String> pathParameters) {
if (element.isExtraField) {
return 'extra as ${element.type.getDisplayString(withNullability: element.isOptional)}';
}

if (element.isRequired) {
return 'pathParameters[${escapeDartString(element.name)}]!';
late String access;
if (pathParameters.contains(element.name)) {
access = 'pathParameters[${escapeDartString(element.name)}]';
} else {
access = 'queryParameters[${escapeDartString(element.name.kebab)}]';
}

if (element.isOptional) {
return 'queryParameters[${escapeDartString(element.name.kebab)}]';
if (pathParameters.contains(element.name) ||
(!element.type.isNullableType && !element.hasDefaultValue)) {
access += '!';
}

throw InvalidGenerationSourceError(
'$likelyIssueMessage (param not required or optional)',
element: element,
);
return access;
}

abstract class _TypeHelper {
const _TypeHelper();

/// Decodes the value from its string representation in the URL.
String _decode(ParameterElement parameterElement);
String _decode(ParameterElement parameterElement, Set<String> pathParameters);

/// Encodes the value from its string representation in the URL.
String _encode(String fieldName, DartType type);
Expand Down Expand Up @@ -228,8 +228,9 @@ class _TypeHelperString extends _TypeHelper {
const _TypeHelperString();

@override
String _decode(ParameterElement parameterElement) =>
'state.${_stateValueAccess(parameterElement)}';
String _decode(
ParameterElement parameterElement, Set<String> pathParameters) =>
'state.${_stateValueAccess(parameterElement, pathParameters)}';

@override
String _encode(String fieldName, DartType type) => fieldName;
Expand Down Expand Up @@ -257,7 +258,8 @@ class _TypeHelperIterable extends _TypeHelper {
const _TypeHelperIterable();

@override
String _decode(ParameterElement parameterElement) {
String _decode(
ParameterElement parameterElement, Set<String> pathParameters) {
if (parameterElement.type is ParameterizedType) {
final DartType iterableType =
(parameterElement.type as ParameterizedType).typeArguments.first;
Expand Down Expand Up @@ -323,17 +325,20 @@ abstract class _TypeHelperWithHelper extends _TypeHelper {
String helperName(DartType paramType);

@override
String _decode(ParameterElement parameterElement) {
String _decode(
ParameterElement parameterElement, Set<String> pathParameters) {
final DartType paramType = parameterElement.type;
final String parameterName = parameterElement.name;

if (!parameterElement.isRequired) {
if (!pathParameters.contains(parameterName) &&
(paramType.isNullableType || parameterElement.hasDefaultValue)) {
return '$convertMapValueHelperName('
'${escapeDartString(parameterElement.name.kebab)}, '
'${escapeDartString(parameterName.kebab)}, '
'state.queryParameters, '
'${helperName(paramType)})';
}
return '${helperName(paramType)}'
'(state.${_stateValueAccess(parameterElement)})';
'(state.${_stateValueAccess(parameterElement, pathParameters)})';
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/go_router_builder/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: go_router_builder
description: >-
A builder that supports generated strongly-typed route helpers for
package:go_router
version: 2.0.2
version: 2.1.0
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

Expand Down
5 changes: 3 additions & 2 deletions packages/go_router_builder/test/builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ const Set<String> _expectedAnnotatedTests = <String>{
'BadPathParam',
'ExtraValueRoute',
'RequiredExtraValueRoute',
'MissingPathParam',
'MissingPathValue',
'MissingTypeAnnotation',
'NullableRequiredParam',
'NullableRequiredParamInPath',
'NullableRequiredParamNotInPath',
'NonNullableRequiredParamNotInPath',
'UnsupportedType',
'theAnswer',
'EnumParam',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,83 @@ class UnsupportedType extends GoRouteData {
}

@ShouldThrow(
'Required parameters cannot be nullable.',
'Required parameters in the path cannot be nullable.',
)
@TypedGoRoute<NullableRequiredParam>(path: 'bob/:id')
class NullableRequiredParam extends GoRouteData {
NullableRequiredParam({required this.id});
@TypedGoRoute<NullableRequiredParamInPath>(path: 'bob/:id')
class NullableRequiredParamInPath extends GoRouteData {
NullableRequiredParamInPath({required this.id});
final int? id;
}

@ShouldThrow(
'Missing param `id` in path.',
)
@TypedGoRoute<MissingPathParam>(path: 'bob/')
class MissingPathParam extends GoRouteData {
MissingPathParam({required this.id});
final String id;
@ShouldGenerate(r'''
RouteBase get $nullableRequiredParamNotInPath => GoRouteData.$route(
path: 'bob',
factory: $NullableRequiredParamNotInPathExtension._fromState,
);

extension $NullableRequiredParamNotInPathExtension
on NullableRequiredParamNotInPath {
static NullableRequiredParamNotInPath _fromState(GoRouterState state) =>
NullableRequiredParamNotInPath(
id: _$convertMapValue('id', state.queryParameters, int.parse),
);

String get location => GoRouteData.$location(
'bob',
);

void go(BuildContext context) => context.go(location);

Future<T?> push<T>(BuildContext context) => context.push<T>(location);

void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
}

T? _$convertMapValue<T>(
String key,
Map<String, String> map,
T Function(String) converter,
) {
final value = map[key];
return value == null ? null : converter(value);
}
''')
@TypedGoRoute<NullableRequiredParamNotInPath>(path: 'bob')
class NullableRequiredParamNotInPath extends GoRouteData {
NullableRequiredParamNotInPath({required this.id});
final int? id;
}

@ShouldGenerate(r'''
RouteBase get $nonNullableRequiredParamNotInPath => GoRouteData.$route(
path: 'bob',
factory: $NonNullableRequiredParamNotInPathExtension._fromState,
);

extension $NonNullableRequiredParamNotInPathExtension
on NonNullableRequiredParamNotInPath {
static NonNullableRequiredParamNotInPath _fromState(GoRouterState state) =>
NonNullableRequiredParamNotInPath(
id: int.parse(state.queryParameters['id']!),
);

String get location => GoRouteData.$location(
'bob',
);

void go(BuildContext context) => context.go(location);

Future<T?> push<T>(BuildContext context) => context.push<T>(location);

void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
}
''')
@TypedGoRoute<NonNullableRequiredParamNotInPath>(path: 'bob')
class NonNullableRequiredParamNotInPath extends GoRouteData {
NonNullableRequiredParamNotInPath({required this.id});
final int id;
}

@ShouldGenerate(r'''
Expand Down