diff --git a/AUTHORS b/AUTHORS index 673c3f2fb551..c5b332610c80 100644 --- a/AUTHORS +++ b/AUTHORS @@ -80,3 +80,4 @@ LinXunFeng Hashir Shoaib Ricardo Dalarme Andrei Kabylin +Ernesto Ramirez diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index c09cc5874309..9ace42f1ed79 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,6 +1,11 @@ +## 4.1.0 + +- Adds support for classes that support fromJson/toJson. [#117261](https://github.com/flutter/flutter/issues/117261) +- Adds annotation that enable custom string encoder/decoder [#110781](https://github.com/flutter/flutter/issues/110781) + ## 4.0.1 -- Fixes unnecessary whitespace in generated `RelativeGoRouteData`. +- Fixes unnecessary whitespace in generated `RelativeGoRouteData`. ## 4.0.0 diff --git a/packages/go_router_builder/example/lib/custom_encoder_example.dart b/packages/go_router_builder/example/lib/custom_encoder_example.dart new file mode 100644 index 000000000000..a2d488c63bee --- /dev/null +++ b/packages/go_router_builder/example/lib/custom_encoder_example.dart @@ -0,0 +1,94 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs, unreachable_from_main + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +part 'custom_encoder_example.g.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({super.key}); + + @override + Widget build(BuildContext context) => + MaterialApp.router(routerConfig: _router, title: _appTitle); + + final GoRouter _router = GoRouter(routes: $appRoutes); +} + +@TypedGoRoute( + path: '/', + name: 'Home', + routes: >[ + TypedGoRoute(path: 'encoded'), + ], +) +class HomeRoute extends GoRouteData with $HomeRoute { + const HomeRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); +} + +class EncodedRoute extends GoRouteData with $EncodedRoute { + const EncodedRoute(this.token); + + @CustomParameterCodec(encode: toBase64, decode: fromBase64) + final String token; + + @override + Widget build(BuildContext context, GoRouterState state) => + EncodedScreen(token: token); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(_appTitle)), + body: ListView( + children: [ + ListTile( + title: const Text('Base64Token'), + onTap: () => const EncodedRoute('Base64Token').go(context), + ), + ListTile( + title: const Text('from url only'), + // like in deep links + onTap: () => context.go('/encoded?token=ZW5jb2RlZCBpbmZvIQ'), + ), + ], + ), + ); +} + +class EncodedScreen extends StatelessWidget { + const EncodedScreen({super.key, required this.token}); + final String token; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Base64Token')), + body: Center(child: Text(token)), + ); +} + +String fromBase64(String value) { + return const Utf8Decoder().convert( + base64Url.decode(base64Url.normalize(value)), + ); +} + +String toBase64(String value) { + return base64Url.encode(const Utf8Encoder().convert(value)); +} + +const String _appTitle = 'GoRouter Example: custom encoder'; diff --git a/packages/go_router_builder/example/lib/custom_encoder_example.g.dart b/packages/go_router_builder/example/lib/custom_encoder_example.g.dart new file mode 100644 index 000000000000..ab002c500782 --- /dev/null +++ b/packages/go_router_builder/example/lib/custom_encoder_example.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'custom_encoder_example.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [$homeRoute]; + +RouteBase get $homeRoute => GoRouteData.$route( + path: '/', + name: 'Home', + factory: $HomeRoute._fromState, + routes: [ + GoRouteData.$route(path: 'encoded', factory: $EncodedRoute._fromState), + ], +); + +mixin $HomeRoute on GoRouteData { + static HomeRoute _fromState(GoRouterState state) => const HomeRoute(); + + @override + String get location => GoRouteData.$location('/'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $EncodedRoute on GoRouteData { + static EncodedRoute _fromState(GoRouterState state) => + EncodedRoute(fromBase64(state.uri.queryParameters['token']!)); + + EncodedRoute get _self => this as EncodedRoute; + + @override + String get location => GoRouteData.$location( + '/encoded', + queryParams: {'token': toBase64(_self.token)}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} diff --git a/packages/go_router_builder/example/lib/json_example.dart b/packages/go_router_builder/example/lib/json_example.dart new file mode 100644 index 000000000000..4033b9a08468 --- /dev/null +++ b/packages/go_router_builder/example/lib/json_example.dart @@ -0,0 +1,82 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs, unreachable_from_main + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'shared/json_example.dart'; + +part 'json_example.g.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({super.key}); + + @override + Widget build(BuildContext context) => + MaterialApp.router(routerConfig: _router, title: _appTitle); + + final GoRouter _router = GoRouter(routes: $appRoutes); +} + +@TypedGoRoute( + path: '/', + name: 'Home', + routes: >[TypedGoRoute(path: 'json')], +) +class HomeRoute extends GoRouteData with $HomeRoute { + const HomeRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); +} + +class JsonRoute extends GoRouteData with $JsonRoute { + const JsonRoute(this.json); + + final JsonExample json; + + @override + Widget build(BuildContext context, GoRouterState state) => + JsonScreen(json: json); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(_appTitle)), + body: ListView( + children: [ + for (final JsonExample json in jsonData) + ListTile( + title: Text(json.name), + onTap: () => JsonRoute(json).go(context), + ), + ], + ), + ); +} + +class JsonScreen extends StatelessWidget { + const JsonScreen({required this.json, super.key}); + final JsonExample json; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(json.name)), + body: ListView( + key: ValueKey(json.id), + children: [Text(json.id), Text(json.name)], + ), + ); +} + +const String _appTitle = 'GoRouter Example: builder'; diff --git a/packages/go_router_builder/example/lib/json_example.g.dart b/packages/go_router_builder/example/lib/json_example.g.dart new file mode 100644 index 000000000000..99600db150ef --- /dev/null +++ b/packages/go_router_builder/example/lib/json_example.g.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'json_example.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [$homeRoute]; + +RouteBase get $homeRoute => GoRouteData.$route( + path: '/', + name: 'Home', + factory: $HomeRoute._fromState, + routes: [GoRouteData.$route(path: 'json', factory: $JsonRoute._fromState)], +); + +mixin $HomeRoute on GoRouteData { + static HomeRoute _fromState(GoRouterState state) => const HomeRoute(); + + @override + String get location => GoRouteData.$location('/'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $JsonRoute on GoRouteData { + static JsonRoute _fromState(GoRouterState state) => JsonRoute((String json0) { + return JsonExample.fromJson(jsonDecode(json0) as Map); + }(state.uri.queryParameters['json']!)); + + JsonRoute get _self => this as JsonRoute; + + @override + String get location => GoRouteData.$location( + '/json', + queryParams: {'json': jsonEncode(_self.json.toJson())}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} diff --git a/packages/go_router_builder/example/lib/json_nested_example.dart b/packages/go_router_builder/example/lib/json_nested_example.dart new file mode 100644 index 000000000000..16d0b5a45fce --- /dev/null +++ b/packages/go_router_builder/example/lib/json_nested_example.dart @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs, unreachable_from_main + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'shared/json_example.dart'; + +part 'json_nested_example.g.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({super.key}); + + @override + Widget build(BuildContext context) => + MaterialApp.router(routerConfig: _router, title: _appTitle); + + final GoRouter _router = GoRouter(routes: $appRoutes); +} + +@TypedGoRoute( + path: '/', + name: 'Home', + routes: >[TypedGoRoute(path: 'json')], +) +class HomeRoute extends GoRouteData with $HomeRoute { + const HomeRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); +} + +class JsonRoute extends GoRouteData with $JsonRoute { + const JsonRoute(this.json); + + final JsonExampleNested json; + + @override + Widget build(BuildContext context, GoRouterState state) => + JsonScreen(json: json.child); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(_appTitle)), + body: ListView( + children: [ + for (final JsonExample json in jsonData) + ListTile( + title: Text(json.name), + onTap: + () => JsonRoute( + JsonExampleNested(child: json), + ).go(context), + ), + ], + ), + ); +} + +class JsonScreen extends StatelessWidget { + const JsonScreen({required this.json, super.key}); + final JsonExample json; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(json.name)), + body: ListView( + key: ValueKey(json.id), + children: [Text(json.id), Text(json.name)], + ), + ); +} + +const String _appTitle = 'GoRouter Example: builder'; diff --git a/packages/go_router_builder/example/lib/json_nested_example.g.dart b/packages/go_router_builder/example/lib/json_nested_example.g.dart new file mode 100644 index 000000000000..18411ebc9a7c --- /dev/null +++ b/packages/go_router_builder/example/lib/json_nested_example.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'json_nested_example.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [$homeRoute]; + +RouteBase get $homeRoute => GoRouteData.$route( + path: '/', + name: 'Home', + factory: $HomeRoute._fromState, + routes: [GoRouteData.$route(path: 'json', factory: $JsonRoute._fromState)], +); + +mixin $HomeRoute on GoRouteData { + static HomeRoute _fromState(GoRouterState state) => const HomeRoute(); + + @override + String get location => GoRouteData.$location('/'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $JsonRoute on GoRouteData { + static JsonRoute _fromState(GoRouterState state) => JsonRoute((String json0) { + return JsonExampleNested.fromJson( + jsonDecode(json0) as Map, + (Object? json1) { + return JsonExample.fromJson(json1 as Map); + }, + ); + }(state.uri.queryParameters['json']!)); + + JsonRoute get _self => this as JsonRoute; + + @override + String get location => GoRouteData.$location( + '/json', + queryParams: {'json': jsonEncode(_self.json.toJson())}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} diff --git a/packages/go_router_builder/example/lib/shared/json_example.dart b/packages/go_router_builder/example/lib/shared/json_example.dart new file mode 100644 index 000000000000..7d20e88e4c50 --- /dev/null +++ b/packages/go_router_builder/example/lib/shared/json_example.dart @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// json example +class JsonExample { + /// json example + const JsonExample({required this.id, required this.name}); + + /// fromJson decoder + factory JsonExample.fromJson(Map json) { + return JsonExample(id: json['id'] as String, name: json['name'] as String); + } + + /// toJson encoder + Map toJson() { + return {'id': id, 'name': name}; + } + + /// id + final String id; + + /// name + final String name; +} + +/// example info +const List jsonData = [ + JsonExample(id: '1', name: 'people'), + JsonExample(id: '2', name: 'animals'), +]; + +/// Json Nested Example +class JsonExampleNested { + /// Json Nested Example + const JsonExampleNested({required this.child}); + + /// toJson decoder + factory JsonExampleNested.fromJson( + Map json, + T Function(Object? json) fromJsonT, + ) { + return JsonExampleNested(child: fromJsonT(json['child'])); + } + + /// toJson encoder + Map toJson() { + return {'child': child}; + } + + /// child + final T child; +} diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index b1eab450219b..8cf6e72de0e9 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -273,7 +273,14 @@ mixin _GoRouteMixin on RouteBaseConfig { ); } } - final String fromStateExpression = decodeParameter(element, _pathParams); + final List? metadata = _fieldMetadata( + element.displayName, + ); + final String fromStateExpression = decodeParameter( + element, + _pathParams, + metadata, + ); if (element.isPositional) { return '$fromStateExpression,'; @@ -298,7 +305,8 @@ mixin _GoRouteMixin on RouteBaseConfig { ); } - return encodeField(field); + final List? metadata = _fieldMetadata(fieldName); + return encodeField(field, metadata); } String get _locationQueryParams { @@ -473,19 +481,19 @@ mixin $_mixinName on $routeDataClassName { $_castedSelf @override String get location => GoRouteData.\$location($_locationArgs,$_locationQueryParams); - + @override void go(BuildContext context) => context.go(location${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); - + @override Future push(BuildContext context) => context.push(location${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); - + @override void pushReplacement(BuildContext context) => context.pushReplacement(location${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); - + @override void replace(BuildContext context) => context.replace(location${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); @@ -548,7 +556,7 @@ mixin $_mixinName on $routeDataClassName { $_castedSelf @override String get subLocation => RelativeGoRouteData.\$location($_locationArgs,$_locationQueryParams); - + @override String get relativeLocation => './\$subLocation'; @@ -559,11 +567,11 @@ mixin $_mixinName on $routeDataClassName { @override Future pushRelative(BuildContext context) => context.push(relativeLocation${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); - + @override void pushReplacementRelative(BuildContext context) => context.pushReplacement(relativeLocation${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); - + @override void replaceRelative(BuildContext context) => context.replace(relativeLocation${_extraParam != null ? ', extra: $selfFieldName.$extraFieldName' : ''}); @@ -901,6 +909,14 @@ $routeDataClassName.$dataConvertionFunctionName( PropertyAccessorElement2? _field(String name) => routeDataClass.getGetter2(name); + List? _fieldMetadata(String name) => + routeDataClass.fields2 + .firstWhereOrNull( + (FieldElement2 element) => element.displayName == name, + ) + ?.metadata2 + .annotations; + /// The name of `RouteData` subclass this configuration represents. @protected String get routeDataClassName; diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index 294e1d325dda..0bced8c2d4fb 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -5,8 +5,10 @@ import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/session.dart'; import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element2.dart'; import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; import 'package:source_gen/source_gen.dart'; import 'package:source_helper/source_helper.dart'; @@ -49,14 +51,41 @@ const List<_TypeHelper> _helpers = <_TypeHelper>[ _TypeHelperString(), _TypeHelperUri(), _TypeHelperIterable(), + _TypeHelperJson(), ]; +/// Checks if has a function that converts string to string, such as encode and decode. +bool _isStringToStringFunction( + ExecutableElement2? executableElement, + String name, +) { + if (executableElement == null) { + return false; + } + final List parameters = + executableElement.formalParameters; + return parameters.length == 1 && + parameters.first.type.isDartCoreString && + executableElement.returnType.isDartCoreString; +} + +/// Returns the custom codec for the annotation. +String? _getCustomCodec(ElementAnnotation annotation, String name) { + final ExecutableElement2? executableElement = + annotation.computeConstantValue()?.getField(name)?.toFunctionValue2(); + if (_isStringToStringFunction(executableElement, name)) { + return executableElement!.displayName; + } + return null; +} + /// Returns the decoded [String] value for [element], if its type is supported. /// /// Otherwise, throws an [InvalidGenerationSourceError]. String decodeParameter( FormalParameterElement element, Set pathParameters, + List? metadata, ) { if (element.isExtraField) { return 'state.${_stateValueAccess(element, pathParameters)}'; @@ -65,13 +94,37 @@ String decodeParameter( final DartType paramType = element.type; for (final _TypeHelper helper in _helpers) { if (helper._matchesType(paramType)) { - String decoded = helper._decode(element, pathParameters); + String? decoder; + + final ElementAnnotation? annotation = metadata?.firstWhereOrNull(( + ElementAnnotation annotation, + ) { + return annotation.computeConstantValue()?.type?.getDisplayString() == + 'CustomParameterCodec'; + }); + if (annotation != null) { + final String? decode = _getCustomCodec(annotation, 'decode'); + final String? encode = _getCustomCodec(annotation, 'encode'); + if (decode != null && encode != null) { + decoder = decode; + } else { + throw InvalidGenerationSourceError( + 'The parameter type ' + '`${paramType.getDisplayString(withNullability: false)}` not have a well defined CustomParameterCodec decorator.', + element: element, + ); + } + } + String decoded = helper._decode(element, pathParameters, decoder); if (element.isOptional && element.hasDefaultValue) { if (element.type.isNullableType) { throw NullableDefaultValueError(element); } decoded += ' ?? ${element.defaultValueCode!}'; } + if (helper is _TypeHelperString && decoder != null) { + return _fieldWithEncoder(decoded, decoder); + } return decoded; } } @@ -86,13 +139,38 @@ String decodeParameter( /// Returns the encoded [String] value for [element], if its type is supported. /// /// Otherwise, throws an [InvalidGenerationSourceError]. -String encodeField(PropertyAccessorElement2 element) { +String encodeField( + PropertyAccessorElement2 element, + List? metadata, +) { for (final _TypeHelper helper in _helpers) { if (helper._matchesType(element.returnType)) { - return helper._encode( + String? encoder; + final ElementAnnotation? annotation = metadata?.firstWhereOrNull(( + ElementAnnotation annotation, + ) { + final DartObject? constant = annotation.computeConstantValue(); + return constant?.type?.getDisplayString() == 'CustomParameterCodec'; + }); + if (annotation != null) { + final String? decode = _getCustomCodec(annotation, 'decode'); + final String? encode = _getCustomCodec(annotation, 'encode'); + if (decode != null && encode != null) { + encoder = encode; + } else { + throw InvalidGenerationSourceError( + 'The parameter type ' + '`${element.type.getDisplayString(withNullability: false)}` not have a well defined CustomParameterCodec decorator.', + element: element, + ); + } + } + final String encoded = helper._encode( '$selfFieldName.${element.displayName}', element.returnType, + encoder, ); + return encoded; } } @@ -102,7 +180,7 @@ String encodeField(PropertyAccessorElement2 element) { ); } -/// Returns an AstNode type from a InterfaceElement. +/// Returns an AstNode type from a InterfaceElement2. T? getNodeDeclaration(InterfaceElement2 element) { final AnalysisSession? session = element.session; if (session == null) { @@ -145,6 +223,9 @@ String compareField( /// Gets the name of the `const` map generated to help encode [Enum] types. String enumMapName(InterfaceType type) => '_\$${type.element.name}EnumMap'; +/// Gets the name of the `const` map generated to help encode [Json] types. +String jsonMapName(InterfaceType type) => type.element.name; + String _stateValueAccess( FormalParameterElement element, Set pathParameters, @@ -177,6 +258,10 @@ String withoutNullability(String type) { return _isNullableType(type) ? type.substring(0, type.length - 1) : type; } +String _fieldWithEncoder(String field, String? customEncoder) { + return customEncoder != null ? '$customEncoder($field)' : field; +} + abstract class _TypeHelper { const _TypeHelper(); @@ -184,10 +269,11 @@ abstract class _TypeHelper { String _decode( FormalParameterElement parameterElement, Set pathParameters, + String? customDecoder, ); /// Encodes the value from its string representation in the URL. - String _encode(String fieldName, DartType type); + String _encode(String fieldName, DartType type, String? customEncoder); bool _matchesType(DartType type); @@ -206,8 +292,11 @@ class _TypeHelperBigInt extends _TypeHelperWithHelper { } @override - String _encode(String fieldName, DartType type) => - '$fieldName${type.ensureNotNull}.toString()'; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + '$fieldName${type.ensureNotNull}.toString()', + customEncoder, + ); @override bool _matchesType(DartType type) => @@ -221,8 +310,11 @@ class _TypeHelperBool extends _TypeHelperWithHelper { String helperName(DartType paramType) => boolConverterHelperName; @override - String _encode(String fieldName, DartType type) => - '$fieldName${type.ensureNotNull}.toString()'; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + '$fieldName${type.ensureNotNull}.toString()', + customEncoder, + ); @override bool _matchesType(DartType type) => type.isDartCoreBool; @@ -240,8 +332,11 @@ class _TypeHelperDateTime extends _TypeHelperWithHelper { } @override - String _encode(String fieldName, DartType type) => - '$fieldName${type.ensureNotNull}.toString()'; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + '$fieldName${type.ensureNotNull}.toString()', + customEncoder, + ); @override bool _matchesType(DartType type) => @@ -260,8 +355,11 @@ class _TypeHelperDouble extends _TypeHelperWithHelper { } @override - String _encode(String fieldName, DartType type) => - '$fieldName${type.ensureNotNull}.toString()'; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + '$fieldName${type.ensureNotNull}.toString()', + customEncoder, + ); @override bool _matchesType(DartType type) => type.isDartCoreDouble; @@ -275,8 +373,14 @@ class _TypeHelperEnum extends _TypeHelperWithHelper { '${enumMapName(paramType as InterfaceType)}.$enumExtensionHelperName'; @override - String _encode(String fieldName, DartType type) => - '${enumMapName(type as InterfaceType)}[$fieldName${type.ensureNotNull}]'; + String _encode( + String fieldName, + DartType type, + String? customEncoder, + ) => _fieldWithEncoder( + '${enumMapName(type as InterfaceType)}[$fieldName${type.ensureNotNull}]', + customEncoder, + ); @override bool _matchesType(DartType type) => type.isEnum; @@ -300,6 +404,7 @@ class _TypeHelperExtensionType extends _TypeHelper { String _decode( FormalParameterElement parameterElement, Set pathParameters, + String? customDecoder, ) { final DartType paramType = parameterElement.type; if (paramType.isNullableType && parameterElement.hasDefaultValue) { @@ -336,7 +441,7 @@ class _TypeHelperExtensionType extends _TypeHelper { } @override - String _encode(String fieldName, DartType type) { + String _encode(String fieldName, DartType type, String? customDecoder) { final DartType representationType = type.extensionTypeErasure; if (representationType.isDartCoreString) { return '$fieldName${type.ensureNotNull} as String'; @@ -388,8 +493,11 @@ class _TypeHelperInt extends _TypeHelperWithHelper { } @override - String _encode(String fieldName, DartType type) => - '$fieldName${type.ensureNotNull}.toString()'; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + '$fieldName${type.ensureNotNull}.toString()', + customEncoder, + ); @override bool _matchesType(DartType type) => type.isDartCoreInt; @@ -407,8 +515,11 @@ class _TypeHelperNum extends _TypeHelperWithHelper { } @override - String _encode(String fieldName, DartType type) => - '$fieldName${type.ensureNotNull}.toString()'; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + '$fieldName${type.ensureNotNull}.toString()', + customEncoder, + ); @override bool _matchesType(DartType type) => type.isDartCoreNum; @@ -421,10 +532,12 @@ class _TypeHelperString extends _TypeHelper { String _decode( FormalParameterElement parameterElement, Set pathParameters, + String? customDecoder, ) => 'state.${_stateValueAccess(parameterElement, pathParameters)}'; @override - String _encode(String fieldName, DartType type) => fieldName; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder(fieldName, customEncoder); @override bool _matchesType(DartType type) => type.isDartCoreString; @@ -442,8 +555,11 @@ class _TypeHelperUri extends _TypeHelperWithHelper { } @override - String _encode(String fieldName, DartType type) => - '$fieldName${type.ensureNotNull}.toString()'; + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + '$fieldName${type.ensureNotNull}.toString()', + customEncoder, + ); @override bool _matchesType(DartType type) => @@ -460,6 +576,7 @@ class _TypeHelperIterable extends _TypeHelperWithHelper { String _decode( FormalParameterElement parameterElement, Set pathParameters, + String? customDecoder, ) { if (parameterElement.type is ParameterizedType) { final DartType iterableType = @@ -476,6 +593,10 @@ class _TypeHelperIterable extends _TypeHelperWithHelper { convertToNotNull = '.cast<$iterableType>()'; } entriesTypeDecoder = helper.helperName(iterableType); + if (customDecoder != null) { + entriesTypeDecoder = + '(e) => $entriesTypeDecoder($customDecoder(e))'; + } } } @@ -510,7 +631,7 @@ state.uri.queryParametersAll[${escapeDartString(parameterElement.displayName.keb } @override - String _encode(String fieldName, DartType type) { + String _encode(String fieldName, DartType type, String? customEncoder) { final String nullAwareAccess = type.isNullableType ? '?' : ''; if (type is ParameterizedType) { final DartType iterableType = type.typeArguments.first; @@ -520,7 +641,7 @@ state.uri.queryParametersAll[${escapeDartString(parameterElement.displayName.keb for (final _TypeHelper helper in _helpers) { if (helper._matchesType(iterableType)) { entriesTypeEncoder = ''' -$nullAwareAccess.map((e) => ${helper._encode('e', iterableType)}).toList()'''; +$nullAwareAccess.map((e) => ${helper._encode('e', iterableType, customEncoder)}).toList()'''; } } return ''' @@ -540,6 +661,135 @@ $fieldName$nullAwareAccess.map((e) => e.toString()).toList()'''; '!$iterablesEqualHelperName($value1, $value2)'; } +class _TypeHelperJson extends _TypeHelperWithHelper { + const _TypeHelperJson(); + + @override + String helperName(DartType paramType) { + return _helperNameN(paramType, 0); + } + + @override + String _encode(String fieldName, DartType type, String? customEncoder) => + _fieldWithEncoder( + 'jsonEncode($fieldName${type.ensureNotNull}.toJson())', + customEncoder, + ); + + @override + bool _matchesType(DartType type) { + if (type is! InterfaceType) { + return false; + } + + final MethodElement2? toJsonMethod = type.lookUpMethod3( + 'toJson', + type.element3.library2, + ); + if (toJsonMethod == null || + !toJsonMethod.isPublic || + toJsonMethod.formalParameters.isNotEmpty) { + return false; + } + + // test for template + if (_isNestedTemplate(type)) { + // check for deep compatibility + return _matchesType(type.typeArguments.first); + } + + final ConstructorElement2? fromJsonMethod = type.element3 + .getNamedConstructor2('fromJson'); + + if (fromJsonMethod == null || + !fromJsonMethod.isPublic || + fromJsonMethod.formalParameters.length != 1 || + fromJsonMethod.formalParameters.first.type.getDisplayString( + withNullability: false, + ) != + 'Map') { + throw InvalidGenerationSourceError( + 'The parameter type ' + '`${type.getDisplayString(withNullability: false)}` not have a supported fromJson definition.', + element: type.element3, + ); + } + + return true; + } + + String _helperNameN(DartType paramType, int index) { + final String mainType = index == 0 ? 'String' : 'Object?'; + final String mainDecoder = + index == 0 + ? 'jsonDecode(json$index) as Map' + : 'json$index as Map'; + if (_isNestedTemplate(paramType as InterfaceType)) { + return ''' +($mainType json$index) { + return ${jsonMapName(paramType)}.fromJson( + $mainDecoder, + ${_helperNameN(paramType.typeArguments.first, index + 1)}, + ); +}'''; + } + return ''' +($mainType json$index) { + return ${jsonMapName(paramType)}.fromJson($mainDecoder); +}'''; + } + + bool _isNestedTemplate(InterfaceType type) { + // check if has fromJson constructor + final ConstructorElement2? fromJsonMethod = type.element3 + .getNamedConstructor2('fromJson'); + if (fromJsonMethod == null || !fromJsonMethod.isPublic) { + return false; + } + + if (type.typeArguments.length != 1) { + return false; + } + + // check if fromJson method receive two parameters + final List parameters = + fromJsonMethod.formalParameters; + if (parameters.length != 2) { + return false; + } + + final FormalParameterElement firstParam = parameters[0]; + if (firstParam.type.getDisplayString(withNullability: false) != + 'Map') { + throw InvalidGenerationSourceError( + 'The parameter type ' + '`${type.getDisplayString(withNullability: false)}` not have a supported fromJson definition.', + element: type.element3, + ); + } + + // Test for (T Function(Object? json)). + final FormalParameterElement secondParam = parameters[1]; + if (secondParam.type is! FunctionType) { + return false; + } + + final FunctionType functionType = secondParam.type as FunctionType; + if (functionType.parameters.length != 1 || + functionType.returnType.getDisplayString() != + type.element.typeParameters.first.getDisplayString() || + functionType.parameters[0].type.getDisplayString() != 'Object?') { + throw InvalidGenerationSourceError( + 'The parameter type ' + '`${type.getDisplayString(withNullability: false)}` not have a supported fromJson definition.', + element: type.element3, + ); + } + + return true; + } +} + abstract class _TypeHelperWithHelper extends _TypeHelper { const _TypeHelperWithHelper(); @@ -549,6 +799,7 @@ abstract class _TypeHelperWithHelper extends _TypeHelper { String _decode( FormalParameterElement parameterElement, Set pathParameters, + String? customDecoder, ) { final DartType paramType = parameterElement.type; final String parameterName = parameterElement.displayName; @@ -560,15 +811,17 @@ abstract class _TypeHelperWithHelper extends _TypeHelper { 'state.uri.queryParameters, ' '${helperName(paramType)})'; } - final String nullableSuffix = paramType.isNullableType || (paramType.isEnum && !paramType.isNullableType) ? '!' : ''; - return '${helperName(paramType)}' - '(state.${_stateValueAccess(parameterElement, pathParameters)} ${!parameterElement.isRequired ? " ?? '' " : ''})$nullableSuffix'; + final String decode = _fieldWithEncoder( + 'state.${_stateValueAccess(parameterElement, pathParameters)} ${!parameterElement.isRequired ? " ?? '' " : ''}', + customDecoder, + ); + return '${helperName(paramType)}($decode)$nullableSuffix'; } } diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 25e088a51c72..f25a19ef95f4 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: 4.0.1 +version: 4.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 diff --git a/packages/go_router_builder/test_inputs/bad_json.dart b/packages/go_router_builder/test_inputs/bad_json.dart new file mode 100644 index 000000000000..8d43c6bfd7b0 --- /dev/null +++ b/packages/go_router_builder/test_inputs/bad_json.dart @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:go_router/go_router.dart'; + +mixin $BadJson {} + +@TypedGoRoute(path: '/') +class BadJson extends GoRouteData with $BadJson { + const BadJson({required this.id}); + + final JsonExample id; +} + +class JsonExample { + const JsonExample({required this.id}); + + // json parameter is not a Map + factory JsonExample.fromJson(/*Map*/ dynamic json) { + return JsonExample(id: (json as Map)['a'] as String); + } + + Map toJson() { + return {'id': id}; + } + + final String id; +} diff --git a/packages/go_router_builder/test_inputs/bad_json.dart.expect b/packages/go_router_builder/test_inputs/bad_json.dart.expect new file mode 100644 index 000000000000..fc70f3af316d --- /dev/null +++ b/packages/go_router_builder/test_inputs/bad_json.dart.expect @@ -0,0 +1 @@ +The parameter type `JsonExample` not have a supported fromJson definition. diff --git a/packages/go_router_builder/test_inputs/bad_json_template.dart b/packages/go_router_builder/test_inputs/bad_json_template.dart new file mode 100644 index 000000000000..fd1656b61fd7 --- /dev/null +++ b/packages/go_router_builder/test_inputs/bad_json_template.dart @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:go_router/go_router.dart'; + +mixin $BadJsonTemplate {} + +@TypedGoRoute(path: '/') +class BadJsonTemplate extends GoRouteData with $BadJsonTemplate { + const BadJsonTemplate({required this.nested, this.deepNested}); + + final JsonExampleNested nested; + final JsonExampleNested>? deepNested; +} + +class JsonExample { + const JsonExample({required this.id}); + + factory JsonExample.fromJson(Map json) { + return JsonExample(id: json['id'] as String); + } + + Map toJson() { + return {'id': id}; + } + + final String id; +} + +class JsonExampleNested { + const JsonExampleNested({required this.child}); + + // from fromJson is not well formed + factory JsonExampleNested.fromJson( + // ignore: avoid_unused_constructor_parameters + Map json, + // ignore: avoid_unused_constructor_parameters + void Function(Object? json) fromJsonT, + /*T Function(Object? json) fromJsonT,*/ + ) { + return JsonExampleNested(child: /*fromJsonT(json['child'])*/ null); + } + + Map toJson() { + return {'child': child}; + } + + final T? child; +} diff --git a/packages/go_router_builder/test_inputs/bad_json_template.dart.expect b/packages/go_router_builder/test_inputs/bad_json_template.dart.expect new file mode 100644 index 000000000000..3302facea2c2 --- /dev/null +++ b/packages/go_router_builder/test_inputs/bad_json_template.dart.expect @@ -0,0 +1 @@ +The parameter type `JsonExampleNested` not have a supported fromJson definition. diff --git a/packages/go_router_builder/test_inputs/custom_encoder.dart b/packages/go_router_builder/test_inputs/custom_encoder.dart new file mode 100644 index 000000000000..a0e0d7d04361 --- /dev/null +++ b/packages/go_router_builder/test_inputs/custom_encoder.dart @@ -0,0 +1,59 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:go_router/go_router.dart'; + +String fromBase64(String value) { + return const Utf8Decoder().convert( + base64Url.decode(base64Url.normalize(value)), + ); +} + +String toBase64(String value) { + return base64Url.encode(const Utf8Encoder().convert(value)); +} + +mixin $CustomParameterRoute {} + +@TypedGoRoute(path: '/default-value-route') +class CustomParameterRoute extends GoRouteData with $CustomParameterRoute { + CustomParameterRoute({required this.param}); + + @CustomParameterCodec(encode: toBase64, decode: fromBase64) + final int param; +} + +mixin $CustomParameterComplexRoute {} + +@TypedGoRoute(path: '/:id/') +class CustomParameterComplexRoute extends GoRouteData + with $CustomParameterComplexRoute { + CustomParameterComplexRoute({ + required this.id, + this.dir = '', + this.list = const [], + required this.enumTest, + }); + + @CustomParameterCodec(encode: toBase64, decode: fromBase64) + final int id; + + @CustomParameterCodec(encode: toBase64, decode: fromBase64) + final String dir; + @CustomParameterCodec(encode: toBase64, decode: fromBase64) + final List list; + @CustomParameterCodec(encode: toBase64, decode: fromBase64) + final EnumTest enumTest; +} + +enum EnumTest { + a(1), + b(3), + c(5); + + const EnumTest(this.x); + final int x; +} diff --git a/packages/go_router_builder/test_inputs/custom_encoder.dart.expect b/packages/go_router_builder/test_inputs/custom_encoder.dart.expect new file mode 100644 index 000000000000..d37b9cdc6a17 --- /dev/null +++ b/packages/go_router_builder/test_inputs/custom_encoder.dart.expect @@ -0,0 +1,102 @@ +RouteBase get $customParameterRoute => GoRouteData.$route( + path: '/default-value-route', + factory: $CustomParameterRoute._fromState, +); + +mixin $CustomParameterRoute on GoRouteData { + static CustomParameterRoute _fromState(GoRouterState state) => + CustomParameterRoute( + param: int.parse(fromBase64(state.uri.queryParameters['param']!)), + ); + + CustomParameterRoute get _self => this as CustomParameterRoute; + + @override + String get location => GoRouteData.$location( + '/default-value-route', + queryParams: {'param': toBase64(_self.param.toString())}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $customParameterComplexRoute => GoRouteData.$route( + path: '/:id/', + factory: $CustomParameterComplexRoute._fromState, +); + +mixin $CustomParameterComplexRoute on GoRouteData { + static CustomParameterComplexRoute _fromState(GoRouterState state) => + CustomParameterComplexRoute( + id: int.parse(fromBase64(state.pathParameters['id']!)), + dir: fromBase64(state.uri.queryParameters['dir'] ?? ''), + list: + state.uri.queryParametersAll['list'] + ?.map((e) => Uri.parse(fromBase64(e))) + .cast() + .toList() ?? + const [], + enumTest: + _$EnumTestEnumMap._$fromName( + fromBase64(state.uri.queryParameters['enum-test']!), + )!, + ); + + CustomParameterComplexRoute get _self => this as CustomParameterComplexRoute; + + @override + String get location => GoRouteData.$location( + '/${Uri.encodeComponent(toBase64(_self.id.toString()))}/', + queryParams: { + if (_self.dir != '') 'dir': toBase64(_self.dir), + if (!_$iterablesEqual(_self.list, const [])) + 'list': _self.list.map((e) => toBase64(e.toString())).toList(), + 'enum-test': toBase64(_$EnumTestEnumMap[_self.enumTest]), + }, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +const _$EnumTestEnumMap = {EnumTest.a: 'a', EnumTest.b: 'b', EnumTest.c: 'c'}; + +extension on Map { + T? _$fromName(String? value) => + entries.where((element) => element.value == value).firstOrNull?.key; +} + +bool _$iterablesEqual(Iterable? iterable1, Iterable? iterable2) { + if (identical(iterable1, iterable2)) return true; + if (iterable1 == null || iterable2 == null) return false; + final iterator1 = iterable1.iterator; + final iterator2 = iterable2.iterator; + while (true) { + final hasNext1 = iterator1.moveNext(); + final hasNext2 = iterator2.moveNext(); + if (hasNext1 != hasNext2) return false; + if (!hasNext1) return true; + if (iterator1.current != iterator2.current) return false; + } +} diff --git a/packages/go_router_builder/test_inputs/json.dart b/packages/go_router_builder/test_inputs/json.dart new file mode 100644 index 000000000000..90026d35049d --- /dev/null +++ b/packages/go_router_builder/test_inputs/json.dart @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:go_router/go_router.dart'; + +mixin $GoodJson {} + +@TypedGoRoute(path: '/') +class GoodJson extends GoRouteData with $GoodJson { + const GoodJson({required this.id, this.optionalField}); + + final JsonExample id; + final JsonExample? optionalField; +} + +class JsonExample { + const JsonExample({required this.id}); + + factory JsonExample.fromJson(Map json) { + return JsonExample(id: json['id'] as String); + } + + final String id; + + Map toJson() { + return {'id': id}; + } +} diff --git a/packages/go_router_builder/test_inputs/json.dart.expect b/packages/go_router_builder/test_inputs/json.dart.expect new file mode 100644 index 000000000000..56e0bf4814a4 --- /dev/null +++ b/packages/go_router_builder/test_inputs/json.dart.expect @@ -0,0 +1,51 @@ +RouteBase get $goodJson => + GoRouteData.$route(path: '/', factory: $GoodJson._fromState); + +mixin $GoodJson on GoRouteData { + static GoodJson _fromState(GoRouterState state) => GoodJson( + id: (String json0) { + return JsonExample.fromJson(jsonDecode(json0) as Map); + }(state.uri.queryParameters['id']!), + optionalField: _$convertMapValue( + 'optional-field', + state.uri.queryParameters, + (String json0) { + return JsonExample.fromJson(jsonDecode(json0) as Map); + }, + ), + ); + + GoodJson get _self => this as GoodJson; + + @override + String get location => GoRouteData.$location( + '/', + queryParams: { + 'id': jsonEncode(_self.id.toJson()), + if (_self.optionalField != null) + 'optional-field': jsonEncode(_self.optionalField!.toJson()), + }, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +T? _$convertMapValue( + String key, + Map map, + T? Function(String) converter, +) { + final value = map[key]; + return value == null ? null : converter(value); +} diff --git a/packages/go_router_builder/test_inputs/json_template.dart b/packages/go_router_builder/test_inputs/json_template.dart new file mode 100644 index 000000000000..c4cc509a4c82 --- /dev/null +++ b/packages/go_router_builder/test_inputs/json_template.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:go_router/go_router.dart'; + +mixin $JsonTemplateRoute {} + +@TypedGoRoute(path: '/') +class JsonTemplateRoute extends GoRouteData with $JsonTemplateRoute { + const JsonTemplateRoute({required this.nested, this.deepNested}); + + final JsonExampleNested nested; + final JsonExampleNested>? deepNested; +} + +class JsonExample { + const JsonExample({required this.id}); + + factory JsonExample.fromJson(Map json) { + return JsonExample(id: json['id'] as String); + } + + Map toJson() { + return {'id': id}; + } + + final String id; +} + +class JsonExampleNested { + const JsonExampleNested({required this.child}); + + factory JsonExampleNested.fromJson( + Map json, + T Function(Object? json) fromJsonT, + ) { + return JsonExampleNested(child: fromJsonT(json['child'])); + } + + Map toJson() { + return {'child': child}; + } + + final T child; +} diff --git a/packages/go_router_builder/test_inputs/json_template.dart.expect b/packages/go_router_builder/test_inputs/json_template.dart.expect new file mode 100644 index 000000000000..a2c04c4392ba --- /dev/null +++ b/packages/go_router_builder/test_inputs/json_template.dart.expect @@ -0,0 +1,65 @@ +RouteBase get $jsonTemplateRoute => GoRouteData.$route( + path: '/', + factory: $JsonTemplateRoute._fromState, + ); + +mixin $JsonTemplateRoute on GoRouteData { + static JsonTemplateRoute _fromState(GoRouterState state) => JsonTemplateRoute( + nested: (String json0) { + return JsonExampleNested.fromJson( + jsonDecode(json0) as Map, + (Object? json1) { + return JsonExample.fromJson(json1 as Map); + }, + ); + }(state.uri.queryParameters['nested']!), + deepNested: _$convertMapValue('deep-nested', state.uri.queryParameters, + (String json0) { + return JsonExampleNested.fromJson( + jsonDecode(json0) as Map, + (Object? json1) { + return JsonExampleNested.fromJson( + json1 as Map, + (Object? json2) { + return JsonExample.fromJson(json2 as Map); + }, + ); + }, + ); + }), + ); + + JsonTemplateRoute get _self => this as JsonTemplateRoute; + + @override + String get location => GoRouteData.$location( + '/', + queryParams: { + 'nested': jsonEncode(_self.nested.toJson()), + if (_self.deepNested != null) + 'deep-nested': jsonEncode(_self.deepNested!.toJson()), + }, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +T? _$convertMapValue( + String key, + Map map, + T? Function(String) converter, +) { + final value = map[key]; + return value == null ? null : converter(value); +}