Line data Source code
1 : part of '../main.dart';
2 :
3 : /// If the [VRouteElement] contains a path, it should extend this class
4 : ///
5 : /// What is does is:
6 : /// - Requiring attributes [path], [name], [aliases]
7 : /// - Computing attributes [pathRegExp], [aliasesRegExp], [pathParametersKeys] and [aliasesPathParametersKeys]
8 : /// - implementing a default [buildRoute] and [getPathFromName] methods for them
9 : @immutable
10 : class VPath extends VRouteElement with VoidVGuard, VoidVPopHandler {
11 : /// The path (relative or absolute) or this [VRouteElement]
12 : ///
13 : /// If the path of a subroute is exactly matched, this will be used in
14 : /// the route but might be covered by another [VRouteElement.widget]
15 : /// The value of the path ca have three form:
16 : /// * starting with '/': The path will be treated as a route path,
17 : /// this is useful to take full advantage of nested routes while
18 : /// conserving the freedom of path naming
19 : /// * not starting with '/': The path corresponding to this route
20 : /// will be the path of the parent route + this path. If this is used
21 : /// directly in the [VRouter] routes, a '/' will be added anyway
22 : /// * be null: In this case this path will match the parent path
23 : ///
24 : /// Note we use the package [path_to_regexp](https://pub.dev/packages/path_to_regexp)
25 : /// so you can use naming such as /user/:id to get the id (see [VRouteElementData.pathParameters]
26 : /// You can also use more advance technique using regexp directly in your path, for example
27 : /// '.*' will match any route, '/user/:id(\d+)' will match any route starting with user
28 : /// and followed by a digit. Here is a recap:
29 : /// | pattern | matched path | [VRouter.pathParameters]
30 : /// | /user/:username | /user/evan | { username: 'evan' }
31 : /// | /user/:id(\d+) | /user/123 | { id: '123' }
32 : /// | .* | every path | -
33 : final String? path;
34 :
35 : /// A name for the route which will allow you to easily navigate to it
36 : /// using [VRouter.of(context).pushNamed]
37 : ///
38 : /// Note that [name] should be unique w.r.t every [VRouteElement]
39 : final String? name;
40 :
41 : /// Alternative paths that will be matched to this route
42 : ///
43 : /// Note that path is match first, then every aliases in order
44 : final List<String> aliases;
45 :
46 : /// A boolean to indicate whether this can be a valid [VRouteElement] of the [VRoute] if no
47 : /// [VRouteElement] in its [stackedRoute] is matched
48 : ///
49 : /// This is mainly useful for [VRouteElement]s which are NOT [VRouteElementWithPage]
50 : final bool mustMatchStackedRoute;
51 :
52 : /// The routes which should be included if the constraint on this path are met
53 : final List<VRouteElement> stackedRoutes;
54 :
55 14 : VPath({
56 : required this.path,
57 : this.name,
58 : this.aliases = const [],
59 : this.mustMatchStackedRoute = false,
60 : required this.stackedRoutes,
61 : }) {
62 56 : pathRegExp = (path != null) ? pathToRegExp(path!, prefix: true) : null;
63 46 : aliasesRegExp = [for (var alias in aliases) pathToRegExp(alias, prefix: true)];
64 28 : pathParametersKeys = <String>[];
65 64 : aliasesPathParametersKeys = List<List<String>>.generate(aliases.length, (_) => []);
66 :
67 : // Get local parameters
68 14 : if (path != null) {
69 63 : final localPath = path!.startsWith('/') ? path!.substring(1) : path!;
70 28 : pathToRegExp(localPath, parameters: pathParametersKeys);
71 : }
72 :
73 46 : for (var i = 0; i < aliases.length; i++) {
74 8 : final alias = aliases[i];
75 12 : final localPath = alias[i].startsWith('/') ? alias.substring(1) : alias;
76 12 : pathToRegExp(localPath, parameters: aliasesPathParametersKeys[i]);
77 : }
78 : }
79 :
80 : /// RegExp version of the path
81 : /// It is created automatically
82 : /// If the path starts with '/', it is removed from
83 : /// this regExp.
84 : late final RegExp? pathRegExp;
85 :
86 : /// RegExp version of the aliases
87 : /// It is created automatically
88 : /// If an alias starts with '/', it is removed from
89 : /// this regExp.
90 : late final List<RegExp> aliasesRegExp;
91 :
92 : /// Parameters of the path
93 : /// It is created automatically
94 : late final List<String> pathParametersKeys;
95 :
96 : /// Parameters of the aliases if any
97 : /// It is created automatically
98 : late final List<List<String>> aliasesPathParametersKeys;
99 :
100 : /// What this [buildRoute] does is look if any path or alias can give a valid [VRoute]
101 : /// considering this and the stackedRoutes
102 : ///
103 : /// For more about buildRoute, see [VRouteElement.buildRoute]
104 12 : @override
105 : VRoute? buildRoute(
106 : VPathRequestData vPathRequestData, {
107 : required VPathMatch parentVPathMatch,
108 : }) {
109 : // This will hold the GetPathMatchResult for the path so that we compute it only once
110 : late final VPathMatch pathMatch;
111 :
112 : // This will hold every GetPathMatchResult for the aliases so that we compute them only once
113 12 : List<VPathMatch> aliasesMatch = [];
114 :
115 : // Try to find valid VRoute from stackedRoutes
116 :
117 : // Check for the path
118 12 : pathMatch = getPathMatch(
119 12 : entirePath: vPathRequestData.path,
120 12 : selfPath: path,
121 12 : selfPathRegExp: pathRegExp,
122 12 : selfPathParametersKeys: pathParametersKeys,
123 : parentVPathMatch: parentVPathMatch,
124 : );
125 12 : final VRoute? stackedRouteVRoute = getVRouteFromRoutes(
126 : vPathRequestData,
127 12 : routes: stackedRoutes,
128 0 : vPathMatch: pathMatch,
129 : );
130 : if (stackedRouteVRoute != null) {
131 12 : return VRoute(
132 12 : vRouteElementNode: VRouteElementNode(
133 : this,
134 12 : localPath: pathMatch.localPath,
135 12 : stackedVRouteElementNode: stackedRouteVRoute.vRouteElementNode,
136 : ),
137 12 : pages: stackedRouteVRoute.pages,
138 12 : pathParameters: stackedRouteVRoute.pathParameters,
139 36 : vRouteElements: <VRouteElement>[this] + stackedRouteVRoute.vRouteElements,
140 : );
141 : }
142 :
143 : // Check for the aliases
144 38 : for (var i = 0; i < aliases.length; i++) {
145 2 : aliasesMatch.add(
146 2 : getPathMatch(
147 2 : entirePath: vPathRequestData.path,
148 4 : selfPath: aliases[i],
149 4 : selfPathRegExp: aliasesRegExp[i],
150 4 : selfPathParametersKeys: aliasesPathParametersKeys[i],
151 : parentVPathMatch: parentVPathMatch,
152 : ),
153 : );
154 2 : final VRoute? stackedRouteVRoute = getVRouteFromRoutes(
155 : vPathRequestData,
156 2 : routes: stackedRoutes,
157 2 : vPathMatch: aliasesMatch[i],
158 : );
159 : if (stackedRouteVRoute != null) {
160 2 : return VRoute(
161 2 : vRouteElementNode: VRouteElementNode(
162 : this,
163 2 : localPath: pathMatch.localPath,
164 2 : stackedVRouteElementNode: stackedRouteVRoute.vRouteElementNode,
165 : ),
166 2 : pages: stackedRouteVRoute.pages,
167 2 : pathParameters: stackedRouteVRoute.pathParameters,
168 6 : vRouteElements: <VRouteElement>[this] + stackedRouteVRoute.vRouteElements,
169 : );
170 : }
171 : }
172 :
173 : // Else, if no subroute is valid
174 :
175 : // check if this is an exact match with path
176 12 : final vRoute = getVRouteFromSelf(
177 : vPathRequestData,
178 0 : vPathMatch: pathMatch,
179 : );
180 : if (vRoute != null) {
181 : return vRoute;
182 : }
183 :
184 : // Check exact match for the aliases
185 38 : for (var i = 0; i < aliases.length; i++) {
186 2 : final vRoute = getVRouteFromSelf(
187 : vPathRequestData,
188 2 : vPathMatch: aliasesMatch[i],
189 : );
190 : if (vRoute != null) {
191 : return vRoute;
192 : }
193 : }
194 :
195 : // Else return null
196 : return null;
197 : }
198 :
199 : /// Searches for a valid [VRoute] by asking [VRouteElement]s is [routes] if they can form a valid [VRoute]
200 12 : VRoute? getVRouteFromRoutes(
201 : VPathRequestData vPathRequestData, {
202 : required List<VRouteElement> routes,
203 : required VPathMatch vPathMatch,
204 : }) {
205 24 : for (var vRouteElement in routes) {
206 12 : final childVRoute = vRouteElement.buildRoute(
207 : vPathRequestData,
208 : parentVPathMatch: vPathMatch,
209 : );
210 : if (childVRoute != null) return childVRoute;
211 : }
212 : }
213 :
214 : /// Try to form a [VRoute] where this [VRouteElement] is the last [VRouteElement]
215 : /// This is possible is:
216 : /// - [mustMatchStackedRoute] is false
217 : /// - There is a match of the path and it is exact
218 12 : VRoute? getVRouteFromSelf(
219 : VPathRequestData vPathRequestData, {
220 : required VPathMatch vPathMatch,
221 : }) {
222 12 : if (!mustMatchStackedRoute &&
223 12 : vPathMatch is ValidVPathMatch &&
224 14 : (vPathMatch.remainingPath.isEmpty)) {
225 3 : return VRoute(
226 6 : vRouteElementNode: VRouteElementNode(this, localPath: vPathMatch.localPath),
227 3 : pages: [],
228 3 : pathParameters: vPathMatch.pathParameters,
229 3 : vRouteElements: <VRouteElement>[this],
230 : );
231 : }
232 : }
233 :
234 : /// Returns path information given a local path.
235 : ///
236 : /// [entirePath] is the whole path, useful when [selfPathRegExp] is absolute
237 : /// [remainingPathFromParent] is the path that remain after removing the parent paths, useful when [selfPathRegExp] relative
238 : /// [selfPathRegExp] the RegExp corresponding to the path that should be tested
239 : ///
240 : /// Returns a [VPathMatch] which holds two information:
241 : /// - The remaining path, after having removed the [selfPathRegExp] (null if there is no match)
242 : /// - The path parameters gotten from [selfPathRegExp] and the path, added to the parentPathParameters if relative path
243 12 : VPathMatch getPathMatch(
244 : {required String entirePath,
245 : required String? selfPath,
246 : required RegExp? selfPathRegExp,
247 : required List<String> selfPathParametersKeys,
248 : required VPathMatch parentVPathMatch}) {
249 :
250 : if (selfPath == null) {
251 : return parentVPathMatch;
252 : }
253 :
254 : // if selfPath is not null, neither should selfPathRegExp be
255 0 : assert(selfPathRegExp != null);
256 :
257 : // If our path starts with '/', this is an absolute path
258 12 : if ((selfPath.startsWith('/'))) {
259 12 : final match = selfPathRegExp!.matchAsPrefix(entirePath);
260 :
261 : if (match == null) {
262 24 : return InvalidVPathMatch(localPath: getConstantLocalPath());
263 : }
264 :
265 24 : var remainingPath = entirePath.substring(match.end);
266 36 : final localPath = entirePath.substring(match.start, match.end);
267 12 : final pathParameters = extract(selfPathParametersKeys, match)
268 20 : ..updateAll((key, value) => Uri.decodeComponent(value));
269 :
270 : // Remove the trailing '/' in remainingPath if needed
271 15 : if (remainingPath.startsWith('/')) remainingPath = remainingPath.replaceFirst('/', '');
272 :
273 12 : return ValidVPathMatch(
274 : remainingPath: remainingPath,
275 : pathParameters: pathParameters,
276 : localPath: localPath,
277 : );
278 : }
279 :
280 : // Else our path is relative
281 5 : final String? thisConstantLocalPath = getConstantLocalPath();
282 : final String? constantLocalPath =
283 5 : (parentVPathMatch.localPath == null && thisConstantLocalPath == null)
284 : ? null
285 5 : : (parentVPathMatch.localPath == null)
286 : ? thisConstantLocalPath
287 : : (thisConstantLocalPath == null)
288 0 : ? parentVPathMatch.localPath
289 0 : : parentVPathMatch.localPath! +
290 0 : (parentVPathMatch.localPath!.endsWith('/') ? '' : '/') +
291 : thisConstantLocalPath;
292 :
293 5 : if (parentVPathMatch is ValidVPathMatch) {
294 : // We try to remove this part of the path from the remainingPathFromParent
295 10 : final match = selfPathRegExp!.matchAsPrefix(parentVPathMatch.remainingPath);
296 :
297 : if (match == null) {
298 5 : return InvalidVPathMatch(localPath: constantLocalPath);
299 : }
300 :
301 15 : var remainingPath = parentVPathMatch.remainingPath.substring(match.end);
302 20 : final localPath = parentVPathMatch.remainingPath.substring(match.start, match.end);
303 5 : final pathParameters = {
304 5 : ...parentVPathMatch.pathParameters,
305 5 : ...extract(selfPathParametersKeys, match)
306 11 : ..updateAll((key, value) => Uri.decodeComponent(value)),
307 : };
308 :
309 : // Remove the trailing '/' in remainingPath if needed
310 5 : if (remainingPath.startsWith('/')) remainingPath = remainingPath.replaceFirst('/', '');
311 :
312 5 : return ValidVPathMatch(
313 : remainingPath: remainingPath,
314 : pathParameters: pathParameters,
315 : localPath: localPath,
316 : );
317 : }
318 :
319 2 : return InvalidVPathMatch(localPath: constantLocalPath);
320 : }
321 :
322 : /// Tries to a path from a name
323 : ///
324 : /// This first asks its stackedRoutes if they have a match
325 : /// Else is tries to see if this [VRouteElement] matches the name
326 : /// Else return null
327 : ///
328 : /// Note that not only the name must match but the path parameters must be able to form a
329 : /// valid path afterward too
330 7 : GetPathFromNameResult getPathFromName(
331 : String nameToMatch, {
332 : required Map<String, String> pathParameters,
333 : required GetNewParentPathResult parentPathResult,
334 : required Map<String, String> remainingPathParameters,
335 : }) {
336 : // A variable to store the new parentPath from the path and aliases
337 7 : final List<GetNewParentPathResult> newParentPathResults = [];
338 7 : final List<Map<String, String>> newRemainingPathParameters = [];
339 :
340 7 : final List<GetPathFromNameResult> nameErrorResults = [];
341 :
342 : // Check if any subroute matches the name using path
343 :
344 : // Get the new parent path by taking this path into account
345 7 : newParentPathResults.add(
346 7 : getNewParentPath(
347 : parentPathResult,
348 7 : thisPath: path,
349 7 : thisPathParametersKeys: pathParametersKeys,
350 : pathParameters: pathParameters,
351 : ),
352 : );
353 :
354 7 : newRemainingPathParameters.add(
355 21 : (path != null && path!.startsWith('/'))
356 7 : ? Map<String, String>.from(pathParameters)
357 4 : : Map<String, String>.from(remainingPathParameters)
358 19 : ..removeWhere((key, value) => pathParametersKeys.contains(key)),
359 : );
360 :
361 14 : for (var vRouteElement in stackedRoutes) {
362 7 : GetPathFromNameResult childPathFromNameResult = vRouteElement.getPathFromName(
363 : nameToMatch,
364 : pathParameters: pathParameters,
365 7 : parentPathResult: newParentPathResults.last,
366 7 : remainingPathParameters: newRemainingPathParameters.last,
367 : );
368 7 : if (childPathFromNameResult is ValidNameResult) {
369 : return childPathFromNameResult;
370 : } else {
371 7 : nameErrorResults.add(childPathFromNameResult);
372 : }
373 : }
374 :
375 : // Check if any subroute matches the name using aliases
376 :
377 24 : for (var i = 0; i < aliases.length; i++) {
378 : // Get the new parent path by taking this alias into account
379 6 : newParentPathResults.add(getNewParentPath(
380 : parentPathResult,
381 6 : thisPath: aliases[i],
382 6 : thisPathParametersKeys: aliasesPathParametersKeys[i],
383 : pathParameters: pathParameters,
384 : ));
385 3 : newRemainingPathParameters.add(
386 9 : (aliases[i].startsWith('/'))
387 3 : ? Map<String, String>.from(pathParameters)
388 0 : : Map<String, String>.from(remainingPathParameters)
389 7 : ..removeWhere((key, value) => aliasesPathParametersKeys[i].contains(key)),
390 : );
391 6 : for (var vRouteElement in stackedRoutes) {
392 3 : GetPathFromNameResult childPathFromNameResult = vRouteElement.getPathFromName(
393 : nameToMatch,
394 : pathParameters: pathParameters,
395 3 : parentPathResult: newParentPathResults.last,
396 3 : remainingPathParameters: newRemainingPathParameters.last,
397 : );
398 3 : if (childPathFromNameResult is ValidNameResult) {
399 : return childPathFromNameResult;
400 : } else {
401 3 : nameErrorResults.add(childPathFromNameResult);
402 : }
403 : }
404 : }
405 :
406 : // If no subroute matches the name, try to match this name
407 14 : if (name == nameToMatch) {
408 : // If path or any alias is valid considering the given path parameters, return this
409 18 : for (int i = 0; i < newParentPathResults.length; i++) {
410 7 : var newParentPathResult = newParentPathResults[i];
411 7 : if (newParentPathResult is ValidParentPathResult) {
412 7 : if (newParentPathResult.path == null) {
413 : // If this path is null, we add a NullPathErrorNameResult
414 2 : nameErrorResults.add(NullPathErrorNameResult(name: nameToMatch));
415 : } else {
416 7 : final newRemainingPathParameter = newRemainingPathParameters[i];
417 7 : if (newRemainingPathParameter.isNotEmpty) {
418 : // If there are path parameters remaining, wee add a PathParamsErrorsNameResult
419 2 : nameErrorResults.add(
420 2 : PathParamsErrorsNameResult(
421 : name: nameToMatch,
422 2 : values: [
423 2 : OverlyPathParamsError(
424 4 : pathParams: pathParameters.keys.toList(),
425 6 : expectedPathParams: newParentPathResult.pathParameters.keys.toList(),
426 : ),
427 : ],
428 : ),
429 : );
430 : } else {
431 : // Else the result is valid
432 12 : return ValidNameResult(path: newParentPathResult.path!);
433 : }
434 : }
435 : } else {
436 4 : assert(newParentPathResult is PathParamsErrorNewParentPath);
437 4 : nameErrorResults.add(
438 4 : PathParamsErrorsNameResult(
439 : name: nameToMatch,
440 4 : values: [
441 4 : MissingPathParamsError(
442 8 : pathParams: pathParameters.keys.toList(),
443 : missingPathParams:
444 4 : (newParentPathResult as PathParamsErrorNewParentPath).pathParameters,
445 : ),
446 : ],
447 : ),
448 : );
449 : }
450 : }
451 : }
452 :
453 : // If we don't have any valid result
454 :
455 : // If some stackedRoute returned PathParamsPopError, aggregate them
456 5 : final pathParamsNameErrors = PathParamsErrorsNameResult(
457 : name: nameToMatch,
458 5 : values: nameErrorResults.fold<List<PathParamsError>>(
459 5 : <PathParamsError>[],
460 5 : (previousValue, element) {
461 5 : return previousValue +
462 12 : ((element is PathParamsErrorsNameResult) ? element.values : []);
463 : },
464 : ),
465 : );
466 :
467 : // If there was any PathParamsPopError, we have some pathParamsPopErrors.values
468 : // and therefore should return this
469 10 : if (pathParamsNameErrors.values.isNotEmpty) {
470 : return pathParamsNameErrors;
471 : }
472 :
473 : // Else try to find a NullPathError
474 : if (nameErrorResults
475 20 : .indexWhere((childNameResult) => childNameResult is NullPathErrorNameResult) !=
476 5 : -1) {
477 1 : return NullPathErrorNameResult(name: nameToMatch);
478 : }
479 :
480 : // Else return a NotFoundError
481 5 : return NotFoundErrorNameResult(name: nameToMatch);
482 : }
483 :
484 : /// The goal is that, considering [thisPath] and [parentPath] we can form a new parentPath
485 : ///
486 : /// For more details, see [GetNewParentPathResult]
487 10 : GetNewParentPathResult getNewParentPath(
488 : GetNewParentPathResult parentPathResult, {
489 : required String? thisPath,
490 : required List<String> thisPathParametersKeys,
491 : required Map<String, String> pathParameters,
492 : }) {
493 : // First check that we have the path parameters needed to have this path
494 : final missingPathParameters =
495 34 : thisPathParametersKeys.where((key) => !pathParameters.containsKey(key)).toList();
496 :
497 10 : if (missingPathParameters.isNotEmpty) {
498 5 : if (thisPath!.startsWith('/')) {
499 4 : return PathParamsErrorNewParentPath(pathParameters: missingPathParameters);
500 : } else {
501 3 : return PathParamsErrorNewParentPath(
502 3 : pathParameters: [
503 3 : if (parentPathResult is PathParamsErrorNewParentPath)
504 0 : ...parentPathResult.pathParameters,
505 3 : ...missingPathParameters
506 : ],
507 : );
508 : }
509 : }
510 :
511 : if (thisPath == null) {
512 : // If the path is null, the new parent path is the same as the previous one
513 : return parentPathResult;
514 : }
515 :
516 20 : final localPath = pathToFunction(thisPath)(pathParameters);
517 10 : final thisPathParameters = Map<String, String>.from(pathParameters)
518 24 : ..removeWhere((key, value) => !thisPathParametersKeys.contains(key));
519 :
520 : // If the path is absolute
521 10 : if (thisPath.startsWith('/')) {
522 10 : return ValidParentPathResult(
523 : path: localPath,
524 : pathParameters: thisPathParameters,
525 : );
526 : }
527 :
528 : // Else the path is relative
529 :
530 : // If the path is relative and the parent path is invalid, then this path is invalid
531 4 : if (parentPathResult is PathParamsErrorNewParentPath) {
532 : return parentPathResult;
533 : }
534 :
535 : // Else this path is valid
536 4 : final parentPathValue = (parentPathResult as ValidParentPathResult).path ?? '';
537 4 : return ValidParentPathResult(
538 12 : path: parentPathValue + (!parentPathValue.endsWith('/') ? '/' : '') + localPath,
539 12 : pathParameters: {...parentPathResult.pathParameters, ...thisPathParameters},
540 : );
541 : }
542 :
543 9 : GetPathFromPopResult getPathFromPop(
544 : VRouteElement elementToPop, {
545 : required Map<String, String> pathParameters,
546 : required GetNewParentPathResult parentPathResult,
547 : }) {
548 : // If vRouteElement is this, then this is the element to pop so we return null
549 9 : if (elementToPop == this) {
550 0 : if (parentPathResult is ValidParentPathResult) {
551 0 : return ValidPopResult(
552 0 : path: parentPathResult.path, didPop: true, poppedVRouteElements: [this]);
553 : } else {
554 0 : assert(parentPathResult is PathParamsErrorNewParentPath);
555 0 : return PathParamsPopErrors(
556 0 : values: [
557 0 : MissingPathParamsError(
558 0 : pathParams: pathParameters.keys.toList(),
559 0 : missingPathParams: (parentPathResult as PathParamsErrorNewParentPath).pathParameters,
560 : ),
561 : ],
562 : );
563 : }
564 : }
565 :
566 9 : final List<GetPathFromPopResult> popErrorResults = [];
567 :
568 : // Try to match the path given the path parameters
569 9 : final newParentPathResult = getNewParentPath(
570 : parentPathResult,
571 9 : thisPath: path,
572 9 : thisPathParametersKeys: pathParametersKeys,
573 : pathParameters: pathParameters,
574 : );
575 :
576 : // If the path matched and produced a non null newParentPath, try to pop from the stackedRoutes
577 18 : for (var vRouteElement in stackedRoutes) {
578 9 : final childPopResult = vRouteElement.getPathFromPop(
579 : elementToPop,
580 : pathParameters: pathParameters,
581 : parentPathResult: newParentPathResult,
582 : );
583 9 : if (childPopResult is ValidPopResult) {
584 9 : if (childPopResult.didPop) {
585 : // if the nestedRoute popped, we should pop too
586 9 : if (parentPathResult is ValidParentPathResult) {
587 8 : return ValidPopResult(
588 8 : path: parentPathResult.path,
589 : didPop: true,
590 : poppedVRouteElements:
591 24 : <VRouteElement>[this] + childPopResult.poppedVRouteElements,
592 : );
593 : } else {
594 4 : assert(parentPathResult is PathParamsErrorNewParentPath);
595 4 : popErrorResults.add(
596 4 : PathParamsPopErrors(
597 4 : values: [
598 4 : MissingPathParamsError(
599 8 : pathParams: pathParameters.keys.toList(),
600 : missingPathParams:
601 4 : (parentPathResult as PathParamsErrorNewParentPath).pathParameters,
602 : ),
603 : ],
604 : ),
605 : );
606 : }
607 : } else {
608 8 : return ValidPopResult(
609 8 : path: childPopResult.path,
610 : didPop: false,
611 8 : poppedVRouteElements: childPopResult.poppedVRouteElements,
612 : );
613 : }
614 : } else {
615 4 : popErrorResults.add(childPopResult);
616 : }
617 : }
618 :
619 : // Try to match the aliases given the path parameters
620 14 : for (var i = 0; i < aliases.length; i++) {
621 3 : final newParentPathResultFromAlias = getNewParentPath(
622 : parentPathResult,
623 6 : thisPath: aliases[i],
624 6 : thisPathParametersKeys: aliasesPathParametersKeys[i],
625 : pathParameters: pathParameters,
626 : );
627 :
628 : // If an alias matched and produced a non null newParentPath, try to pop from the stackedRoutes
629 : // Try to pop from the stackedRoutes
630 6 : for (var vRouteElement in stackedRoutes) {
631 3 : final childPopResult = vRouteElement.getPathFromPop(
632 : elementToPop,
633 : pathParameters: pathParameters,
634 : parentPathResult: newParentPathResultFromAlias,
635 : );
636 3 : if (childPopResult is ValidPopResult) {
637 3 : if (childPopResult.didPop) {
638 : // if the nestedRoute popped, we should pop too
639 2 : if (parentPathResult is ValidParentPathResult) {
640 2 : return ValidPopResult(
641 2 : path: parentPathResult.path,
642 : didPop: true,
643 : poppedVRouteElements:
644 6 : <VRouteElement>[this] + childPopResult.poppedVRouteElements,
645 : );
646 : } else {
647 0 : assert(parentPathResult is PathParamsErrorNewParentPath);
648 0 : popErrorResults.add(
649 0 : PathParamsPopErrors(
650 0 : values: [
651 0 : MissingPathParamsError(
652 0 : pathParams: pathParameters.keys.toList(),
653 : missingPathParams:
654 0 : (parentPathResult as PathParamsErrorNewParentPath).pathParameters,
655 : ),
656 : ],
657 : ),
658 : );
659 : }
660 : } else {
661 3 : return ValidPopResult(
662 3 : path: childPopResult.path,
663 : didPop: false,
664 3 : poppedVRouteElements: childPopResult.poppedVRouteElements,
665 : );
666 : }
667 : } else {
668 2 : popErrorResults.add(childPopResult);
669 : }
670 : }
671 : }
672 :
673 : // If we don't have any valid result
674 :
675 : // If some stackedRoute returned PathParamsPopError, aggregate them
676 4 : final pathParamsPopErrors = PathParamsPopErrors(
677 4 : values: popErrorResults.fold<List<MissingPathParamsError>>(
678 4 : <MissingPathParamsError>[],
679 4 : (previousValue, element) {
680 15 : return previousValue + ((element is PathParamsPopErrors) ? element.values : []);
681 : },
682 : ),
683 : );
684 :
685 : // If there was any PathParamsPopError, we have some pathParamsPopErrors.values
686 : // and therefore should return this
687 8 : if (pathParamsPopErrors.values.isNotEmpty) {
688 : return pathParamsPopErrors;
689 : }
690 :
691 : // If none of the stackedRoutes popped, this did not pop, and there was no path parameters issue, return not found
692 3 : return ErrorNotFoundGetPathFromPopResult();
693 : }
694 :
695 : /// If this [VRouteElement] is in the route but its localPath is null
696 : /// we try to find a local path in [path, ...aliases]
697 : ///
698 : /// This is used in [buildPage] to form the LocalKey
699 : /// Note that
700 : /// - We can't use this because animation won't play if path parameters change for example
701 : /// - Using null is not ideal because if we pop from a absolute path, this won't animate as expected
702 12 : String? getConstantLocalPath() {
703 24 : if (pathParametersKeys.isEmpty) {
704 11 : return path;
705 : }
706 18 : for (var i = 0; i < aliasesPathParametersKeys.length; i++) {
707 6 : if (aliasesPathParametersKeys[i].isEmpty) {
708 4 : return aliases[i];
709 : }
710 : }
711 : return null;
712 : }
713 : }
714 :
715 : /// The value of the new parentPath in [VRouteElement.getPathFromPop] and [VRouteElement.getPathFromName]
716 : /// If this path is invalid:
717 : /// - return [ValidGetNewParentPathResult(value: parentPathParameter)]
718 : /// If this path starts with '/':
719 : /// - Either the path parameters from [pathParameters] include those of this path and
720 : /// we return the corresponding path
721 : /// - Or we return [InvalidGetNewParentPathResult(missingPathParameters: this missing path parameters)]
722 : /// If this path does not start with '/':
723 : /// - If the parent path is invalid:
724 : /// _ * [InvalidGetNewParentPathResult(missingPathParameters: parentPathParameterResult.missingPathParameters + this missingPathParameters)]
725 : /// - If the parent path is not invalid:
726 : /// _ * Either the path parameters from [pathParameters] include those of this path and
727 : /// we return [ValidGetNewParentPathResult(the parent path + this path)]
728 : /// _ * Or we return [InvalidGetNewParentPathResult(missingPathParameters: this missing path parameters)]
729 : abstract class GetNewParentPathResult {}
730 :
731 : class ValidParentPathResult extends GetNewParentPathResult {
732 : /// Null is a valid value, it just means that this path is null and the parent one was as well
733 : final String? path;
734 :
735 : final Map<String, String> pathParameters;
736 :
737 10 : ValidParentPathResult({required this.path, required this.pathParameters});
738 : }
739 :
740 : class PathParamsErrorNewParentPath extends GetNewParentPathResult {
741 : /// The missing path parameters that prevents us from creating the path
742 : final List<String> pathParameters;
743 :
744 5 : PathParamsErrorNewParentPath({required this.pathParameters});
745 : }
|