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