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