blob: 6135cff1c1cba68a110021cdf387c5267aac6c2e [file] [log] [blame]
// 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 'configuration.dart';
import 'match.dart';
import 'path_utils.dart';
/// Converts a location into a list of [RouteMatch] objects.
class RouteMatcher {
/// [RouteMatcher] constructor.
RouteMatcher(this.configuration);
/// The route configuration.
final RouteConfiguration configuration;
/// Finds the routes that matched the given URL.
RouteMatchList findMatch(String location, {Object? extra}) {
final String canonicalLocation = canonicalUri(location);
final List<RouteMatch> matches =
_getLocRouteMatches(canonicalLocation, extra);
return RouteMatchList(matches);
}
List<RouteMatch> _getLocRouteMatches(String location, Object? extra) {
final Uri uri = Uri.parse(location);
final List<RouteMatch> result = _getLocRouteRecursively(
loc: uri.path,
restLoc: uri.path,
routes: configuration.routes,
parentFullpath: '',
parentSubloc: '',
queryParams: uri.queryParameters,
extra: extra,
);
if (result.isEmpty) {
throw MatcherError('no routes for location', location);
}
return result;
}
}
/// The list of [RouteMatch] objects.
class RouteMatchList {
/// RouteMatchList constructor.
RouteMatchList(this._matches);
/// Constructs an empty matches object.
factory RouteMatchList.empty() => RouteMatchList(<RouteMatch>[]);
final List<RouteMatch> _matches;
/// Returns true if there are no matches.
bool get isEmpty => _matches.isEmpty;
/// Returns true if there are matches.
bool get isNotEmpty => _matches.isNotEmpty;
/// The original URL that was matched.
Uri get location =>
_matches.isEmpty ? Uri() : Uri.parse(_matches.last.fullUriString);
/// Pushes a match onto the list of matches.
void push(RouteMatch match) {
_matches.add(match);
}
/// Removes the last match.
void pop() {
_matches.removeLast();
assert(
_matches.isNotEmpty,
'You have popped the last page off of the stack,'
' there are no pages left to show');
}
/// Returns true if [pop] can safely be called.
bool canPop() {
return _matches.length > 1;
}
/// An optional object provided by the app during navigation.
Object? get extra => _matches.last.extra;
/// The last matching route.
RouteMatch get last => _matches.last;
/// The route matches.
List<RouteMatch> get matches => _matches;
/// Returns true if the current match intends to display an error screen.
bool get isError => matches.length == 1 && matches.first.error != null;
/// Returns the error that this match intends to display.
Exception? get error => matches.first.error;
}
/// An error that occurred during matching.
class MatcherError extends Error {
/// Constructs a [MatcherError].
MatcherError(String message, this.location) : message = '$message: $location';
/// The error message.
final String message;
/// The location that failed to match.
final String location;
@override
String toString() {
return message;
}
}
List<RouteMatch> _getLocRouteRecursively({
required String loc,
required String restLoc,
required String parentSubloc,
required List<GoRoute> routes,
required String parentFullpath,
required Map<String, String> queryParams,
required Object? extra,
}) {
bool debugGatherAllMatches = false;
assert(() {
debugGatherAllMatches = true;
return true;
}());
final List<List<RouteMatch>> result = <List<RouteMatch>>[];
// find the set of matches at this level of the tree
for (final GoRoute route in routes) {
final String fullpath = concatenatePaths(parentFullpath, route.path);
final RouteMatch? match = RouteMatch.match(
route: route,
restLoc: restLoc,
parentSubloc: parentSubloc,
fullpath: fullpath,
queryParams: queryParams,
extra: extra,
);
if (match == null) {
continue;
}
if (match.subloc.toLowerCase() == loc.toLowerCase()) {
// If it is a complete match, then return the matched route
// NOTE: need a lower case match because subloc is canonicalized to match
// the path case whereas the location can be of any case and still match
result.add(<RouteMatch>[match]);
} else if (route.routes.isEmpty) {
// If it is partial match but no sub-routes, bail.
continue;
} else {
// Otherwise, recurse
final String childRestLoc =
loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1));
assert(loc.startsWith(match.subloc));
assert(restLoc.isNotEmpty);
final List<RouteMatch> subRouteMatch = _getLocRouteRecursively(
loc: loc,
restLoc: childRestLoc,
parentSubloc: match.subloc,
routes: route.routes,
parentFullpath: fullpath,
queryParams: queryParams,
extra: extra,
).toList();
// If there's no sub-route matches, there is no match for this location
if (subRouteMatch.isEmpty) {
continue;
}
result.add(<RouteMatch>[match, ...subRouteMatch]);
}
// Should only reach here if there is a match.
if (debugGatherAllMatches) {
continue;
} else {
break;
}
}
if (result.isEmpty) {
return <RouteMatch>[];
}
// If there are multiple routes that match the location, returning the first one.
// To make predefined routes to take precedence over dynamic routes eg. '/:id'
// consider adding the dynamic route at the end of the routes
return result.first;
}