Line data Source code
1 : import 'package:flutter/foundation.dart'; 2 : 3 : import 'destination.dart'; 4 : import 'exceptions.dart'; 5 : 6 : /// A base destination parser. 7 : /// 8 : /// [DestinationParser] is used to parse the destination object from the 9 : /// given URI string, and to generate the URI for the destination. 10 : /// 11 : /// When subclassed, the certain type of destination parameters must be provided. 12 : /// 13 : /// There are two methods, that must be implemented in the specific parser: 14 : /// [toDestinationParameters] and [toMap]. 15 : /// 16 : /// If typed parameters are not required, the [DefaultDestinationParser] is used. 17 : /// 18 : /// See also: 19 : /// - [Destination] 20 : /// - [DestinationParameters] 21 : /// - [DefaultDestinationParameters] 22 : /// - [DefaultDestinationParser] 23 : /// 24 : abstract class DestinationParser<T extends DestinationParameters> { 25 : /// Creates destination parser. 26 : /// 27 16 : const DestinationParser(); 28 : 29 : /// Creates a destination parameters object of type [T] from the given map. 30 : /// 31 : /// The key of the map entry is a parameter name, and the value is serialized 32 : /// parameter's value. 33 : /// This method is used by [parseParameters()] to generate the destination object 34 : /// from the given URI string. 35 : /// 36 : Future<T> toDestinationParameters(Map<String, String> map); 37 : 38 : /// Converts destination [parameters] object of type [T] to a map. 39 : /// 40 : /// The key of the map entry is a parameter name, and the value is serialized 41 : /// parameter's value. 42 : /// This method is used by [uri()] to generate the destination's URI string. 43 : /// 44 : Map<String, String> toMap(T parameters); 45 : 46 : /// Check if the provided [uri] string match the base [destination] 47 : /// 48 8 : bool isMatch(String uri, Destination<T> destination) { 49 16 : if ((uri == '/' || uri.isEmpty)) { 50 3 : return destination.isHome; 51 : } 52 : 53 16 : final destinationUri = Uri.parse(destination.path); 54 8 : final sourceUri = Uri.parse(uri); 55 8 : final destinationSegments = destinationUri.pathSegments; 56 8 : final sourceSegments = sourceUri.pathSegments; 57 : 58 24 : if (destinationSegments.length < sourceSegments.length) { 59 : return false; 60 : } 61 24 : final lengthDifference = destinationSegments.length - sourceSegments.length; 62 8 : if (lengthDifference > 1 || 63 20 : lengthDifference == 1 && !isPathParameter(destinationSegments.last)) { 64 : return false; 65 : } 66 32 : for (var i = 0; i < destinationSegments.length - lengthDifference; i++) { 67 24 : if (destinationSegments[i] != sourceSegments[i] && 68 16 : !isPathParameter(destinationSegments[i])) { 69 : return false; 70 : } 71 : } 72 : return true; 73 : } 74 : 75 : /// Check if the path segment string is a valid path parameter placeholder. 76 : /// 77 : /// The default path parameter format is '{parameterName}'. 78 : /// 79 8 : bool isPathParameter(String pathSegment) { 80 15 : return pathSegment.startsWith('{') && pathSegment.endsWith('}'); 81 : } 82 : 83 : /// Extract parameter name from the path segment string. 84 : /// 85 : /// See [isPathParameter] for default path parameter format. 86 : /// 87 7 : String parsePathParameterName(String pathSegment) { 88 7 : if (!isPathParameter(pathSegment)) { 89 : // TODO: Implement custom exception 90 0 : throw Exception('$pathSegment is not a valid path parameter'); 91 : } 92 21 : return pathSegment.substring(1, pathSegment.length - 1); 93 : } 94 : 95 : /// Parses parameter values from the specified URI for matched destination. 96 : /// 97 : /// Returns the copy of [matchedDestination] with actual parameter values parsed from the [uri]. 98 : /// Uses [toDestinationParameters] implementation to create parameters object of 99 : /// type [T]. 100 : /// 101 : /// Also it ensures that raw parameters value in [DestinationParameters.map] are valid. 102 : /// 103 : /// Throws [DestinationNotMatchException] if the URI does mot match the destination. 104 : /// 105 5 : Future<Destination<T>> parseParameters( 106 : String uri, Destination<T> matchedDestination) async { 107 : // TODO: Is this check really needed here? 108 5 : if (!isMatch(uri, matchedDestination)) { 109 2 : throw DestinationNotMatchException(uri, matchedDestination); 110 : } 111 5 : final parsedUri = Uri.parse(uri); 112 5 : final parametersMap = <String, String>{} 113 10 : ..addAll(_parsePathParameters(parsedUri, matchedDestination)) 114 10 : ..addAll(parsedUri.queryParameters); 115 10 : final T parameters = await toDestinationParameters(parametersMap); 116 5 : final rawParameters = toMap(parameters); 117 : parameters 118 10 : ..map.clear() 119 10 : ..map.addAll(rawParameters); 120 5 : return matchedDestination.withParameters(parameters); 121 : } 122 : 123 : /// Returns URI string for the destination 124 : /// 125 : /// The [Destination.path] is used for building the URI path segment. 126 : /// The URI query segment is built using [Destination.parameters] converted 127 : /// by [toMap] implementation from the [Destination.parser]. 128 : /// 129 8 : String uri(Destination destination) { 130 : late final Map<String, String> parametersMap; 131 8 : if (destination.parameters == null) { 132 : parametersMap = const <String, String>{}; 133 : } else { 134 18 : parametersMap = destination.parser.toMap(destination.parameters!); 135 : } 136 16 : final pathParameters = _getPathParameters(destination.path, parametersMap); 137 8 : final queryParameters = _getQueryParameters(pathParameters, parametersMap); 138 16 : final path = _fillPathParameters(destination.path, pathParameters); 139 8 : return Uri( 140 : path: path, 141 8 : queryParameters: queryParameters.isNotEmpty ? queryParameters : null, 142 8 : ).toString(); 143 : } 144 : 145 5 : Map<String, String> _parsePathParameters( 146 : Uri uri, Destination<T> baseDestination) { 147 5 : final result = <String, String>{}; 148 10 : final baseDestinationUri = Uri.parse(baseDestination.path); 149 19 : for (int i = 0; i < uri.pathSegments.length; i++) { 150 8 : final pathSegment = uri.pathSegments[i]; 151 8 : final baseDestinationPathSegment = baseDestinationUri.pathSegments[i]; 152 4 : if (isPathParameter(baseDestinationPathSegment)) { 153 : final parameterName = 154 4 : parsePathParameterName(baseDestinationPathSegment); 155 4 : result[parameterName] = pathSegment; 156 : } 157 : } 158 : return result; 159 : } 160 : 161 8 : Map<String, String> _getPathParameters( 162 : String path, Map<String, String> parameters) { 163 8 : final result = <String, String>{}; 164 8 : final pathUri = Uri.parse(path); 165 16 : for (var pathSegment in pathUri.pathSegments) { 166 8 : if (isPathParameter(pathSegment)) { 167 7 : final parameterName = parsePathParameterName(pathSegment); 168 7 : final value = parameters[parameterName]; 169 : if (value != null) { 170 4 : result[parameterName] = value; 171 : } 172 : } 173 : } 174 : return result; 175 : } 176 : 177 8 : Map<String, String> _getQueryParameters( 178 : Map<String, String> pathParameters, Map<String, String> parameters) { 179 8 : final result = <String, String>{}; 180 12 : for (MapEntry entry in parameters.entries) { 181 12 : if (pathParameters.keys.contains(entry.key)) { 182 : continue; 183 : } 184 9 : result[entry.key] = entry.value; 185 : } 186 : return result; 187 : } 188 : 189 8 : String _fillPathParameters(String path, Map<String, String> parameters) { 190 8 : final pathUri = Uri.parse(path); 191 8 : final filledPathSegments = <String>[]; 192 16 : for (var pathSegment in pathUri.pathSegments) { 193 8 : if (isPathParameter(pathSegment)) { 194 7 : final parameterName = parsePathParameterName(pathSegment); 195 7 : final value = parameters[parameterName]; 196 : if (value != null) { 197 4 : filledPathSegments.add(value); 198 : } else { 199 18 : if (pathUri.pathSegments.last != pathSegment) { 200 4 : filledPathSegments.add(pathSegment); 201 : } 202 : } 203 : } else { 204 8 : filledPathSegments.add(pathSegment); 205 : } 206 : } 207 16 : final result = Uri(pathSegments: filledPathSegments).toString(); 208 16 : return '${path.startsWith('/') ? "/" : ""}$result'; 209 : } 210 : } 211 : 212 : /// A default implementation of [DestinationParser]. 213 : /// 214 : class DefaultDestinationParser 215 : extends DestinationParser<DestinationParameters> { 216 : /// Creates default destination parser. 217 : /// 218 11 : const DefaultDestinationParser() : super(); 219 : 220 5 : @override 221 : Future<DestinationParameters> toDestinationParameters( 222 : Map<String, String> map) => 223 10 : SynchronousFuture(DestinationParameters(map)); 224 : 225 6 : @override 226 12 : Map<String, String> toMap(DestinationParameters parameters) => Map.of(parameters.map); 227 : }