Jonatas

clean route parser implementation and add test to dynamic routes

## [3.25.0]
## [3.24.0]
- GetWidget has been completely redesigned.
Throughout its lifetime, GetWidget has always been mentioned in the documentation as "something you shouldn't use unless you're sure you need it", and it had a very small use case. A short time ago we realized that it could have some unexpected behaviors, when compared to GetView, so we decided to rebuild it from scratch, creating a really useful widget for the ecosystem.
... ...
export 'get_connect/connect.dart';
... ...
... ... @@ -895,9 +895,7 @@ extension GetNavigation on GetInterface {
routeTree = ParseRouteTree();
}
for (final element in getPages) {
routeTree.addRoute(element);
}
routeTree.addRoutes(getPages);
}
}
... ...
import 'package:flutter/widgets.dart';
import '../../get_navigation.dart';
import '../routes/get_route.dart';
class ParseRouteTree {
final List<_ParseRouteTreeNode> _nodes = <_ParseRouteTreeNode>[];
// bool _hasDefaultRoute = false;
void addRoute(GetPage route) {
var path = route.name;
class RouteDecoder {
final GetPage route;
final Map<String, String> parameters;
const RouteDecoder(this.route, this.parameters);
}
if (path == Navigator.defaultRouteName) {
// if (_hasDefaultRoute) {
// throw ("Default route was already defined");
// }
var node = _ParseRouteTreeNode(path, _ParseRouteTreeNodeType.component);
node.routes = [route];
_nodes.add(node);
// _hasDefaultRoute = true;
return;
}
if (path.startsWith("/")) {
path = path.substring(1);
class ParseRouteTree {
final _routes = <GetPage>[];
RouteDecoder matchRoute(String name) {
final uri = Uri.parse(name);
final route = _findRoute(uri.path);
final params = Map<String, String>.from(uri.queryParameters);
final parsedParams = _parseParams(name, route?.path);
if (parsedParams != null && parsedParams.isNotEmpty) {
params.addAll(parsedParams);
}
var pathComponents = path.split('/');
_ParseRouteTreeNode parent;
for (var i = 0; i < pathComponents.length; i++) {
var component = pathComponents[i];
var node = _nodeForComponent(component, parent);
if (node == null) {
var type = _typeForComponent(component);
node = _ParseRouteTreeNode(component, type);
node.parent = parent;
if (parent == null) {
_nodes.add(node);
} else {
parent.nodes.add(node);
}
}
if (i == pathComponents.length - 1) {
if (node.routes == null) {
node.routes = [route];
} else {
node.routes.add(route);
}
}
parent = node;
return RouteDecoder(route, params);
}
void addRoutes(List<GetPage> getPages) {
for (final route in getPages) {
addRoute(route);
}
}
void addRoute(GetPage route) {
_routes.add(route);
// Add Page children.
for (var page in _flattenPage(route)) {
... ... @@ -97,178 +80,37 @@ class ParseRouteTree {
middlewares: middlewares,
);
_GetPageMatch matchRoute(String path) {
var usePath = path;
if (usePath.startsWith("/")) {
usePath = path.substring(1);
}
// should take off url parameters first..
final uri = Uri.tryParse(usePath);
// List<String> components = usePath.split("/");
var components = uri.pathSegments;
if (path == Navigator.defaultRouteName) {
components = ["/"];
}
var nodeMatches = <_ParseRouteTreeNode, _ParseRouteTreeNodeMatch>{};
var nodesToCheck = _nodes;
for (final checkComponent in components) {
final currentMatches = <_ParseRouteTreeNode, _ParseRouteTreeNodeMatch>{};
final nextNodes = <_ParseRouteTreeNode>[];
for (final node in nodesToCheck) {
var pathPart = checkComponent;
var queryMap = <String, String>{};
if (checkComponent.contains("?") && !checkComponent.contains("=")) {
var splitParam = checkComponent.split("?");
pathPart = splitParam[0];
queryMap = {pathPart: splitParam[1]};
} else if (checkComponent.contains("?")) {
var splitParam = checkComponent.split("?");
var splitParam2 = splitParam[1].split("=");
if (!splitParam2[1].contains("&")) {
pathPart = splitParam[0];
queryMap = {splitParam2[0]: splitParam2[1]};
} else {
pathPart = splitParam[0];
final second = splitParam[1];
var other = second.split(RegExp(r"[&,=]"));
for (var i = 0; i < (other.length - 1); i++) {
var isOdd = (i % 2 == 0);
if (isOdd) {
queryMap.addAll({other[0 + i]: other[1 + i]});
}
}
}
}
final isMatch = (node.part == pathPart || node.isParameter());
if (isMatch) {
final parentMatch = nodeMatches[node.parent];
final match = _ParseRouteTreeNodeMatch.fromMatch(parentMatch, node);
// TODO: find a way to clean this implementation.
match.parameters.addAll(uri.queryParameters);
if (node.isParameter()) {
final paramKey = node.part.substring(1);
match.parameters[paramKey] = pathPart;
}
if (queryMap != null) {
match.parameters.addAll(queryMap);
}
currentMatches[node] = match;
if (node.nodes != null) {
nextNodes.addAll(node.nodes);
}
}
}
nodeMatches = currentMatches;
nodesToCheck = nextNodes;
if (currentMatches.values.length == 0) {
return null;
}
}
var matches = nodeMatches.values.toList();
if (matches.length > 0) {
var match = matches.first;
var nodeToUse = match.node;
if (nodeToUse != null &&
nodeToUse.routes != null &&
nodeToUse.routes.length > 0) {
var routes = nodeToUse.routes;
var routeMatch = _GetPageMatch(routes[0]);
routeMatch.parameters = match.parameters;
return routeMatch;
}
}
return null;
GetPage _findRoute(String name) {
final route = _routes.firstWhere(
(route) {
return _match(
name,
route.path['regex'] as RegExp,
);
},
orElse: () => null,
);
return route;
}
_ParseRouteTreeNode _nodeForComponent(
String component,
_ParseRouteTreeNode parent,
) {
var nodes = _nodes;
if (parent != null) {
nodes = parent.nodes;
}
for (var node in nodes) {
if (node.part == component) {
return node;
Map<String, String> _parseParams(
String path, Map<String, dynamic> routePath) {
final params = <String, String>{};
Match paramsMatch = (routePath['regex'] as RegExp).firstMatch(path);
for (var i = 0; i < (routePath['keys'].length as int); i++) {
String param;
try {
param = Uri.decodeQueryComponent(paramsMatch[i + 1]);
} on Exception catch (_) {
param = paramsMatch[i + 1];
}
}
return null;
}
_ParseRouteTreeNodeType _typeForComponent(String component) {
var type = _ParseRouteTreeNodeType.component;
if (_isParameterComponent(component)) {
type = _ParseRouteTreeNodeType.parameter;
}
return type;
}
bool _isParameterComponent(String component) {
return component.startsWith(":");
}
Map<String, String> parseQueryString(String query) {
var search = RegExp('([^&=]+)=?([^&]*)');
var params = <String, String>{};
if (query.startsWith('?')) query = query.substring(1);
decode(String s) => Uri.decodeComponent(s.replaceAll('+', ' '));
for (Match match in search.allMatches(query)) {
var key = decode(match.group(1));
final value = decode(match.group(2));
params[key] = value;
params[routePath['keys'][i] as String] = param;
}
return params;
}
}
class _ParseRouteTreeNodeMatch {
_ParseRouteTreeNodeMatch(this.node);
_ParseRouteTreeNodeMatch.fromMatch(
_ParseRouteTreeNodeMatch match, this.node) {
parameters = <String, String>{};
if (match != null) {
parameters.addAll(match.parameters);
}
}
_ParseRouteTreeNode node;
Map<String, String> parameters = <String, String>{};
}
class _ParseRouteTreeNode {
_ParseRouteTreeNode(this.part, this.type);
String part;
_ParseRouteTreeNodeType type;
List<GetPage> routes = <GetPage>[];
List<_ParseRouteTreeNode> nodes = <_ParseRouteTreeNode>[];
_ParseRouteTreeNode parent;
bool isParameter() {
return type == _ParseRouteTreeNodeType.parameter;
bool _match(String name, RegExp path) {
return path.hasMatch(name);
}
}
class _GetPageMatch {
_GetPageMatch(this.route);
GetPage route;
Map<String, String> parameters = <String, String>{};
}
enum _ParseRouteTreeNodeType {
component,
parameter,
}
... ...
... ... @@ -24,8 +24,9 @@ class GetPage {
final RouteSettings settings;
final List<GetPage> children;
final List<GetMiddleware> middlewares;
const GetPage({
final Map<String, dynamic> path;
final List<String> keys;
GetPage({
@required this.name,
@required this.page,
this.title,
... ... @@ -43,12 +44,40 @@ class GetPage {
this.customTransition,
this.fullscreenDialog = false,
this.children,
this.keys,
this.middlewares,
}) : assert(page != null),
}) : path = normalize(name, keysList: keys),
assert(page != null),
assert(name != null),
assert(maintainState != null),
assert(fullscreenDialog != null);
static Map<String, dynamic> normalize(
String path, {
List<String> keysList,
}) {
var keys = List<String>.from(keysList ?? const <String>[]);
var stringPath =
'$path/?'.replaceAllMapped(RegExp(r'(\.)?:(\w+)(\?)?'), (placeholder) {
var replace = StringBuffer('(?:');
if (placeholder[1] != null) {
replace.write('\.');
}
replace.write('([\\w%+-._~!\$&\'()*,;=:@]+))');
if (placeholder[3] != null) {
replace.write('?');
}
keys.add(placeholder[2]);
return replace.toString();
}).replaceAll('//', '/');
return {'regex': RegExp('^$stringPath\$'), 'keys': keys};
}
GetPage copyWith({
String name,
GetPageBuilder page,
... ... @@ -66,6 +95,7 @@ class GetPage {
Duration transitionDuration,
bool fullscreenDialog,
RouteSettings settings,
List<String> keys,
List<GetPage> children,
List<GetMiddleware> middlewares,
}) {
... ... @@ -86,6 +116,7 @@ class GetPage {
transitionDuration: transitionDuration ?? this.transitionDuration,
fullscreenDialog: fullscreenDialog ?? this.fullscreenDialog,
settings: settings ?? this.settings,
keys: keys ?? this.keys,
children: children ?? this.children,
middlewares: middlewares ?? this.middlewares,
);
... ...
... ... @@ -102,6 +102,8 @@ class _GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
VoidCallback remove;
Object _selector;
List<VoidCallback> _removeToOthers = <VoidCallback>[];
@override
void initState() {
super.initState();
... ...
... ... @@ -50,4 +50,41 @@ void main() {
expect(match, isNotNull);
expect(match.route.name, searchRoute);
});
testWidgets(
'test params from dynamic route',
(tester) async {
await tester.pumpWidget(GetMaterialApp(
initialRoute: '/first/juan',
getPages: [
GetPage(page: () => Container(), name: '/first/:name'),
GetPage(page: () => Container(), name: '/second/:id'),
GetPage(page: () => Container(), name: '/third'),
GetPage(page: () => Container(), name: '/last/:id/:name/profile')
],
));
expect(Get.parameters['name'], 'juan');
Get.toNamed('/second/1234');
await tester.pumpAndSettle();
expect(Get.parameters['id'], '1234');
Get.toNamed('/third?name=jonny&job=dev');
await tester.pumpAndSettle();
expect(Get.parameters['name'], 'jonny');
expect(Get.parameters['job'], 'dev');
Get.toNamed('/last/1234/ana/profile');
await tester.pumpAndSettle();
expect(Get.parameters['id'], '1234');
expect(Get.parameters['name'], 'ana');
},
);
}
... ...