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 17 : 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 : /// 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 17 : 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 8 : String parsePathParameterName(String pathSegment) { 91 8 : if (!isPathParameter(pathSegment)) { 92 : // TODO: Implement custom exception 93 0 : throw Exception('$pathSegment is not a valid path parameter'); 94 : } 95 24 : 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 [toDestinationParameters] 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 5 : Future<Destination<T>> parseParameters( 109 : String uri, Destination<T> matchedDestination) async { 110 : // TODO: Is this check really needed here? 111 5 : if (!isMatch(uri, matchedDestination)) { 112 2 : throw DestinationNotMatchException(uri, matchedDestination); 113 : } 114 5 : final parsedUri = Uri.parse(uri); 115 5 : final parametersMap = <String, String>{} 116 10 : ..addAll(_parsePathParameters(parsedUri, matchedDestination)) 117 10 : ..addAll(parsedUri.queryParameters); 118 10 : final T parameters = await toDestinationParameters(parametersMap); 119 5 : final rawParameters = toMap(parameters); 120 : parameters 121 10 : ..map.clear() 122 10 : ..map.addAll(rawParameters); 123 5 : return matchedDestination.withParameters(parameters); 124 : } 125 : 126 : /// Returns URI string for the destination 127 : /// 128 : /// The [Destination.path] is used for building the URI path segment. 129 : /// The URI query segment is built using [Destination.parameters] converted 130 : /// by [toMap] implementation from the [Destination.parser]. 131 : /// 132 9 : String uri(Destination destination) { 133 : late final Map<String, String> parametersMap; 134 9 : if (destination.parameters == null) { 135 : parametersMap = const <String, String>{}; 136 : } else { 137 18 : parametersMap = destination.parser.toMap(destination.parameters!); 138 : } 139 18 : final pathParameters = _getPathParameters(destination.path, parametersMap); 140 9 : final queryParameters = _getQueryParameters(pathParameters, parametersMap); 141 18 : final path = _fillPathParameters(destination.path, pathParameters); 142 9 : return Uri( 143 : path: path, 144 9 : queryParameters: queryParameters.isNotEmpty ? queryParameters : null, 145 9 : ).toString(); 146 : } 147 : 148 5 : Map<String, String> _parsePathParameters( 149 : Uri uri, Destination<T> baseDestination) { 150 5 : final result = <String, String>{}; 151 10 : final baseDestinationUri = Uri.parse(baseDestination.path); 152 19 : for (int i = 0; i < uri.pathSegments.length; i++) { 153 8 : final pathSegment = uri.pathSegments[i]; 154 8 : final baseDestinationPathSegment = baseDestinationUri.pathSegments[i]; 155 4 : if (isPathParameter(baseDestinationPathSegment)) { 156 : final parameterName = 157 4 : parsePathParameterName(baseDestinationPathSegment); 158 4 : result[parameterName] = pathSegment; 159 : } 160 : } 161 : return result; 162 : } 163 : 164 9 : Map<String, String> _getPathParameters( 165 : String path, Map<String, String> parameters) { 166 9 : final result = <String, String>{}; 167 9 : final pathUri = Uri.parse(path); 168 18 : for (var pathSegment in pathUri.pathSegments) { 169 9 : if (isPathParameter(pathSegment)) { 170 8 : final parameterName = parsePathParameterName(pathSegment); 171 8 : final value = parameters[parameterName]; 172 : if (value != null) { 173 4 : result[parameterName] = value; 174 : } 175 : } 176 : } 177 : return result; 178 : } 179 : 180 9 : Map<String, String> _getQueryParameters( 181 : Map<String, String> pathParameters, Map<String, String> parameters) { 182 9 : final result = <String, String>{}; 183 13 : for (MapEntry entry in parameters.entries) { 184 12 : if (pathParameters.keys.contains(entry.key)) { 185 : continue; 186 : } 187 9 : result[entry.key] = entry.value; 188 : } 189 : return result; 190 : } 191 : 192 9 : String _fillPathParameters(String path, Map<String, String> parameters) { 193 9 : final pathUri = Uri.parse(path); 194 9 : final filledPathSegments = <String>[]; 195 18 : for (var pathSegment in pathUri.pathSegments) { 196 9 : if (isPathParameter(pathSegment)) { 197 8 : final parameterName = parsePathParameterName(pathSegment); 198 8 : final value = parameters[parameterName]; 199 : if (value != null) { 200 4 : filledPathSegments.add(value); 201 : } else { 202 21 : if (pathUri.pathSegments.last != pathSegment) { 203 4 : filledPathSegments.add(pathSegment); 204 : } 205 : } 206 : } else { 207 9 : filledPathSegments.add(pathSegment); 208 : } 209 : } 210 18 : final result = Uri(pathSegments: filledPathSegments).toString(); 211 18 : return '${path.startsWith('/') ? "/" : ""}$result'; 212 : } 213 : } 214 : 215 : /// A default implementation of [DestinationParser]. 216 : /// 217 : class DefaultDestinationParser 218 : extends DestinationParser<DestinationParameters> { 219 : /// Creates default destination parser. 220 : /// 221 12 : const DefaultDestinationParser() : super(); 222 : 223 5 : @override 224 : Future<DestinationParameters> toDestinationParameters( 225 : Map<String, String> map) => 226 10 : SynchronousFuture(DestinationParameters(map)); 227 : 228 6 : @override 229 : Map<String, String> toMap(DestinationParameters parameters) => 230 12 : Map.of(parameters.map); 231 : }