LCOV - code coverage report
Current view: top level - src/vroute_elements - vroute_element_with_path.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 119 141 84.4 %
Date: 2021-03-18 15:42:40 Functions: 0 0 -

          Line data    Source code
       1             : part of '../main.dart';
       2             : 
       3             : /// If the VRouteElement does have a page to display, it should instantiate this class
       4             : ///
       5             : /// What is does is:
       6             : ///     - Requiring attributes [path], [name], [aliases], [widget] and [mustMatchStackedRoutes]
       7             : ///     - Computing attributes [pathRegExp], [aliasesRegExp], [pathParametersKeys],
       8             : ///                                                          [aliasesParameters] and [stateKey]
       9             : ///     - implementing [build] and [getPathFromName] methods for them
      10             : @immutable
      11             : abstract class VRouteElementWithPath extends VRouteElement {
      12             :   /// The path (local or absolute) or this [VRouteElement]
      13             :   ///
      14             :   /// If the path of a subroute is exactly matched, this will be used in
      15             :   /// the route but might be covered by another [VRouteElement.widget]
      16             :   /// The value of the path ca have three form:
      17             :   ///     * starting with '/': The path will be treated as a route path,
      18             :   ///       this is useful to take full advantage of nested routes while
      19             :   ///       conserving the freedom of path naming
      20             :   ///     * not starting with '/': The path corresponding to this route
      21             :   ///       will be the path of the parent route + this path. If this is used
      22             :   ///       directly in the [VRouter] routes, a '/' will be added anyway
      23             :   ///     * be null: In this case this path will match the parent path
      24             :   ///
      25             :   /// Note we use the package [path_to_regexp](https://pub.dev/packages/path_to_regexp)
      26             :   /// so you can use naming such as /user/:id to get the id (see [VRouteElementData.pathParameters]
      27             :   /// You can also use more advance technique using regexp directly in your path, for example
      28             :   /// '.*' will match any route, '/user/:id(\d+)' will match any route starting with user
      29             :   /// and followed by a digit. Here is a recap:
      30             :   /// |     pattern       | matched path |      [VRouteElementData.pathParameters]
      31             :   /// | /user/:username |  /user/evan  |            { username: 'evan' }
      32             :   /// | /user/:id(\d+)  |  /user/123   |                { id: '123' }
      33             :   /// |     .*          |  every path  |                 -
      34             :   // TODO: change example
      35             :   final String? path;
      36             : 
      37             :   /// A name for the route which will allow you to easily navigate to it
      38             :   /// using [VRouterData.of(context).pushNamed]
      39             :   ///
      40             :   /// Note that [name] should be unique w.r.t every [VRouteElement]
      41             :   final String? name;
      42             : 
      43             :   /// An alternative path that will be matched to this route
      44             :   final List<String> aliases;
      45             : 
      46             :   final bool mustMatchSubRoute;
      47             : 
      48             :   final List<VRouteElement> stackedRoutes;
      49             : 
      50          12 :   VRouteElementWithPath({
      51             :     required this.path,
      52             :     this.name,
      53             :     this.stackedRoutes = const [],
      54             :     this.aliases = const [],
      55             :     this.mustMatchSubRoute = false,
      56          12 :   })  : pathRegExp = (path != null) ? pathToRegExp(path, prefix: true) : null,
      57          27 :         aliasesRegExp = [for (var alias in aliases) pathToRegExp(alias, prefix: true)],
      58          12 :         pathParametersKeys = <String>[],
      59          30 :         aliasesPathParametersKeys = List<List<String>>.generate(aliases.length, (_) => []) {
      60             :     // Get local parameters
      61          12 :     if (path != null) {
      62          52 :       final localPath = path!.startsWith('/') ? path!.substring(1) : path!;
      63          24 :       pathToRegExp(localPath, parameters: pathParametersKeys);
      64             :     }
      65             : 
      66          39 :     for (var i = 0; i < aliases.length; i++) {
      67           6 :       final alias = aliases[i];
      68           9 :       final localPath = alias[i].startsWith('/') ? alias.substring(1) : alias;
      69           9 :       pathToRegExp(localPath, parameters: aliasesPathParametersKeys[i]);
      70             :     }
      71             :   }
      72             : 
      73             :   // /// A key for the [VBasePage] that will host the widget
      74             :   // /// You shouldn't care about using it unless you don't
      75             :   // /// specify a path.
      76             :   // LocalKey? get key;
      77             : 
      78             :   /// RegExp version of the path
      79             :   /// It is created automatically
      80             :   /// If the path starts with '/', it is removed from
      81             :   /// this regExp.
      82             :   final RegExp? pathRegExp;
      83             : 
      84             :   /// RegExp version of the aliases
      85             :   /// It is created automatically
      86             :   /// If an alias starts with '/', it is removed from
      87             :   /// this regExp.
      88             :   final List<RegExp> aliasesRegExp;
      89             : 
      90             :   /// Parameters of the path
      91             :   /// It is created automatically
      92             :   final List<String> pathParametersKeys;
      93             : 
      94             :   /// Parameters of the aliases if any
      95             :   /// It is created automatically
      96             :   final List<List<String>> aliasesPathParametersKeys;
      97             : 
      98             :   /// [entirePath] is the entire path given (in push for example)
      99             :   ///
     100             :   /// [parentRemainingPath] is the part of the path which is left to match
     101             :   /// after the parent [VRouteElement] matched the [entirePath]
     102             :   /// WARNING: [parentRemainingPath] is null if the parent did not match the path
     103             :   /// in which case only absolute path should be tested.
     104          10 :   VRoute? buildRoute(
     105             :     VPathRequestData vPathRequestData, {
     106             :     required String? parentRemainingPath,
     107             :     required Map<String, String> parentPathParameters,
     108             :   }) {
     109             :     // This will hold the GetPathMatchResult for the path so that we compute it only once
     110             :     late final GetPathMatchResult pathGetPathMatchResult;
     111             : 
     112             :     // This will hold every GetPathMatchResult for the aliases so that we compute them only once
     113          10 :     List<GetPathMatchResult> aliasesGetPathMatchResult = [];
     114             : 
     115             :     // Try to find valid VRoute from stackedRoutes
     116             : 
     117             :     // Check for the path
     118          10 :     pathGetPathMatchResult = getPathMatch(
     119          10 :       entirePath: vPathRequestData.path,
     120             :       remainingPathFromParent: parentRemainingPath,
     121          10 :       selfPath: path,
     122          10 :       selfPathRegExp: pathRegExp,
     123          10 :       selfPathParametersKeys: pathParametersKeys,
     124             :       parentPathParameters: parentPathParameters,
     125             :     );
     126          10 :     final VRoute? stackedRouteVRoute = getVRouteFromRoutes(
     127             :       vPathRequestData,
     128          10 :       routes: stackedRoutes,
     129           0 :       getPathMatchResult: pathGetPathMatchResult,
     130             :     );
     131             :     if (stackedRouteVRoute != null) {
     132          10 :       return VRoute(
     133          10 :         vRouteElementNode: VRouteElementNode(
     134             :           this,
     135          10 :           stackedVRouteElementNode: stackedRouteVRoute.vRouteElementNode,
     136             :         ),
     137          10 :         pages: stackedRouteVRoute.pages,
     138          10 :         pathParameters: stackedRouteVRoute.pathParameters,
     139          30 :         vRouteElements: <VRouteElement>[this] + stackedRouteVRoute.vRouteElements,
     140             :       );
     141             :     }
     142             : 
     143             :     // Check for the aliases
     144          31 :     for (var i = 0; i < aliases.length; i++) {
     145           1 :       aliasesGetPathMatchResult.add(
     146           1 :         getPathMatch(
     147           1 :           entirePath: vPathRequestData.path,
     148             :           remainingPathFromParent: parentRemainingPath,
     149           2 :           selfPath: aliases[i],
     150           2 :           selfPathRegExp: aliasesRegExp[i],
     151           2 :           selfPathParametersKeys: aliasesPathParametersKeys[i],
     152             :           parentPathParameters: parentPathParameters,
     153             :         ),
     154             :       );
     155           1 :       final VRoute? stackedRouteVRoute = getVRouteFromRoutes(
     156             :         vPathRequestData,
     157           1 :         routes: stackedRoutes,
     158           1 :         getPathMatchResult: aliasesGetPathMatchResult[i],
     159             :       );
     160             :       if (stackedRouteVRoute != null) {
     161           0 :         return VRoute(
     162           0 :           vRouteElementNode: VRouteElementNode(
     163             :             this,
     164           0 :             stackedVRouteElementNode: stackedRouteVRoute.vRouteElementNode,
     165             :           ),
     166           0 :           pages: stackedRouteVRoute.pages,
     167           0 :           pathParameters: stackedRouteVRoute.pathParameters,
     168           0 :           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          10 :     final vRoute = getVRouteFromSelf(
     177             :       vPathRequestData,
     178             :       parentPathParameters: parentPathParameters,
     179           0 :       getPathMatchResult: pathGetPathMatchResult,
     180             :     );
     181             :     if (vRoute != null) {
     182             :       return vRoute;
     183             :     }
     184             : 
     185             :     // Check exact match for the aliases
     186          28 :     for (var i = 0; i < aliases.length; i++) {
     187           1 :       final vRoute = getVRouteFromSelf(
     188             :         vPathRequestData,
     189             :         parentPathParameters: parentPathParameters,
     190           1 :         getPathMatchResult: aliasesGetPathMatchResult[i],
     191             :       );
     192             :       if (vRoute != null) {
     193             :         return vRoute;
     194             :       }
     195             :     }
     196             : 
     197             :     // Else return null
     198             :     return null;
     199             :   }
     200             : 
     201             :   /// Searches for a valid [VRoute] by asking [VRouteElement]s is [routes] if they can form a valid [VRoute]
     202          10 :   VRoute? getVRouteFromRoutes(
     203             :     VPathRequestData vPathRequestData, {
     204             :     required List<VRouteElement> routes,
     205             :     required GetPathMatchResult getPathMatchResult,
     206             :   }) {
     207          20 :     for (var vRouteElement in routes) {
     208          10 :       final childVRoute = vRouteElement.buildRoute(
     209             :         vPathRequestData,
     210          10 :         parentRemainingPath: getPathMatchResult.remainingPath,
     211          10 :         parentPathParameters: getPathMatchResult.pathParameters,
     212             :       );
     213             :       if (childVRoute != null) return childVRoute;
     214             :     }
     215             :   }
     216             : 
     217             :   /// Try to form a [VRoute] where this [VRouteElement] is the last [VRouteElement]
     218             :   /// This is possible is:
     219             :   ///   - [mustMatchSubRoute] is false
     220             :   ///   - Their is a match of the path and it is exact
     221          10 :   VRoute? getVRouteFromSelf(
     222             :     VPathRequestData vPathRequestData, {
     223             :     required Map<String, String> parentPathParameters,
     224             :     required GetPathMatchResult getPathMatchResult,
     225             :   }) {
     226          30 :     if (!mustMatchSubRoute && (getPathMatchResult.remainingPath?.isEmpty ?? false)) {
     227          10 :       return VRoute(
     228          10 :         vRouteElementNode: VRouteElementNode(this),
     229          10 :         pages: [],
     230          10 :         pathParameters: getPathMatchResult.pathParameters,
     231          10 :         vRouteElements: <VRouteElement>[this],
     232             :       );
     233             :     }
     234             :   }
     235             : 
     236             :   /// Returns path information given a local path.
     237             :   ///
     238             :   /// [entirePath] is the whole path, useful when [selfPathRegExp] is absolute
     239             :   /// [remainingPathFromParent] is the path that remain after removing the parent paths, useful when [selfPathRegExp] relative
     240             :   /// [selfPathRegExp] the RegExp corresponding to the path that should be tested
     241             :   ///
     242             :   /// Returns a [GetPathMatchResult] which holds two information:
     243             :   ///   - The remaining path, after having removed the [selfPathRegExp] (null if there is no match)
     244             :   ///   - The path parameters gotten from [selfPathRegExp] and the path, added to the parentPathParameters if local path
     245          10 :   GetPathMatchResult getPathMatch({
     246             :     required String entirePath,
     247             :     required String? remainingPathFromParent,
     248             :     required String? selfPath,
     249             :     required RegExp? selfPathRegExp,
     250             :     required List<String> selfPathParametersKeys,
     251             :     required Map<String, String> parentPathParameters,
     252             :   }) {
     253             :     late final Match? match;
     254             : 
     255             :     // remainingPath is null if there is no match
     256             :     late String? remainingPath;
     257             :     late final Map<String, String> newPathParameters;
     258             :     if (selfPath == null) {
     259             :       // This is ugly but the only way to return a non-null empty match...
     260           4 :       match = RegExp('').matchAsPrefix('');
     261             :       remainingPath = remainingPathFromParent;
     262           0 :       newPathParameters = parentPathParameters;
     263          10 :     } else if ((selfPath.startsWith('/'))) {
     264             :       // If our path starts with '/', this is an absolute path
     265          10 :       match = selfPathRegExp!.matchAsPrefix(entirePath);
     266          20 :       remainingPath = (match != null) ? entirePath.substring(match.end) : null;
     267          20 :       newPathParameters = (match != null) ? extract(selfPathParametersKeys, match) : {};
     268             :     } else if ((remainingPathFromParent != null)) {
     269             :       // If it does not start with '/', the path is relative
     270             :       // We try to remove this part of the path from the remainingPathFromParent
     271           2 :       match = selfPathRegExp!.matchAsPrefix(remainingPathFromParent);
     272           4 :       remainingPath = (match != null) ? remainingPathFromParent.substring(match.end) : null;
     273           0 :       newPathParameters = (match != null)
     274           2 :           ? {
     275           2 :               ...parentPathParameters,
     276           2 :               ...extract(selfPathParametersKeys, match),
     277             :             }
     278           2 :           : {};
     279             :     } else {
     280             :       // If remainingPathFromParent is null and the path is relative
     281             :       // the parent did not match, so there is no match
     282           0 :       match = null;
     283             :       remainingPath = null;
     284           1 :       newPathParameters = {};
     285             :     }
     286             : 
     287             :     // Remove the trailing '/' in remainingPath if needed
     288          10 :     if (remainingPath != null && remainingPath.startsWith('/'))
     289           2 :       remainingPath = remainingPath.replaceFirst('/', '');
     290             : 
     291          10 :     return GetPathMatchResult(remainingPath: remainingPath, pathParameters: newPathParameters);
     292             :   }
     293             : 
     294             :   /// Tries to a path from a name
     295             :   ///
     296             :   /// This first asks its stackedRoutes if they have a match
     297             :   /// Else is tries to see if this [VRouteElement] matches the name
     298             :   /// Else return null
     299             :   ///
     300             :   /// Note that not only the name must match but the path parameters must be able to form a
     301             :   /// valid path afterward too
     302           5 :   String? getPathFromName(
     303             :     String nameToMatch, {
     304             :     required Map<String, String> pathParameters,
     305             :     required String? parentPath,
     306             :     required Map<String, String> remainingPathParameters,
     307             :   }) {
     308             :     // A variable to store the new parentPath from the path
     309             :     late final String? newParentPathFromPath;
     310             :     late final Map<String, String> newRemainingPathParametersFromPath;
     311             : 
     312             :     // A variable to store the new parent path from the aliases
     313           5 :     final List<String?> newParentPathFromAliases = [];
     314           5 :     final List<Map<String, String>> newRemainingPathParametersFromAliases = [];
     315             : 
     316             :     // Check if any subroute matches the name using path
     317             : 
     318             :     // Get the new parent path by taking this path into account
     319           5 :     newParentPathFromPath = getNewParentPath(parentPath,
     320          10 :         path: path, pathParametersKeys: pathParametersKeys, pathParameters: pathParameters);
     321             : 
     322          15 :     newRemainingPathParametersFromPath = (path != null && path!.startsWith('/'))
     323           5 :         ? Map<String, String>.from(pathParameters)
     324           2 :         : Map<String, String>.from(remainingPathParameters)
     325          14 :       ..removeWhere((key, value) => pathParametersKeys.contains(key));
     326             : 
     327          10 :     for (var vRouteElement in stackedRoutes) {
     328           5 :       String? childPathFromName = vRouteElement.getPathFromName(
     329             :         nameToMatch,
     330             :         pathParameters: pathParameters,
     331           0 :         parentPath: newParentPathFromPath,
     332           0 :         remainingPathParameters: newRemainingPathParametersFromPath,
     333             :       );
     334             :       if (childPathFromName != null) {
     335             :         return childPathFromName;
     336             :       }
     337             :     }
     338             : 
     339             :     // Check if any subroute matches the name using aliases
     340             : 
     341          16 :     for (var i = 0; i < aliases.length; i++) {
     342             :       // Get the new parent path by taking this alias into account
     343           2 :       newParentPathFromAliases.add(getNewParentPath(
     344             :         parentPath,
     345           2 :         path: aliases[i],
     346           2 :         pathParametersKeys: aliasesPathParametersKeys[i],
     347             :         pathParameters: pathParameters,
     348             :       ));
     349           1 :       newRemainingPathParametersFromAliases.add(
     350           3 :         (aliases[i].startsWith('/'))
     351           1 :             ? Map<String, String>.from(pathParameters)
     352           0 :             : Map<String, String>.from(remainingPathParameters)
     353           5 :           ..removeWhere((key, value) => aliasesPathParametersKeys[i].contains(key)),
     354             :       );
     355           1 :       for (var vRouteElement in stackedRoutes) {
     356           0 :         String? childPathFromName = vRouteElement.getPathFromName(
     357             :           nameToMatch,
     358             :           pathParameters: pathParameters,
     359           0 :           parentPath: newParentPathFromAliases[i],
     360           0 :           remainingPathParameters: newRemainingPathParametersFromAliases[i],
     361             :         );
     362             :         if (childPathFromName != null) {
     363             :           return childPathFromName;
     364             :         }
     365             :       }
     366             :     }
     367             : 
     368             :     // If no subroute matches the name, try to match this name
     369          10 :     if (name == nameToMatch) {
     370             :       // Note that newParentPath will be null if this path can't be included so the return value
     371             :       // is the right one
     372           4 :       if (newParentPathFromPath != null && newRemainingPathParametersFromPath.isEmpty) {
     373           0 :         return newParentPathFromPath;
     374             :       }
     375           4 :       for (var i = 0; i < aliases.length; i++) {
     376           1 :         if (newParentPathFromAliases[i] != null &&
     377           2 :             newRemainingPathParametersFromAliases[i].isEmpty) {
     378           1 :           return newParentPathFromAliases[i];
     379             :         }
     380             :       }
     381             :     }
     382             : 
     383             :     // Else we return null
     384             :     return null;
     385             :   }
     386             : 
     387             :   /// The goal is that, considering [path] and [parentPath] we can form a new parentPath
     388             :   ///
     389             :   /// If this path is null, then the new parentPath is the same as the old one
     390             :   /// If this path starts with '/':
     391             :   ///     - Either the path parameters from [pathParameters] include those of this path and
     392             :   ///       we return the corresponding path
     393             :   ///     - Or we return null
     394             :   /// If this path does not start with '/':
     395             :   ///     - If the parent path is null we return null
     396             :   ///     - If the parent path is not null:
     397             :   ///         * Either the path parameters from [pathParameters] include those of this path and
     398             :   ///             we return the parent path + this path
     399             :   ///         * Or we return null
     400             :   ///
     401             :   /// This method is used in [getPathFromPop]
     402           9 :   String? getNewParentPath(
     403             :     String? parentPath, {
     404             :     required String? path,
     405             :     required List<String> pathParametersKeys,
     406             :     required Map<String, String> pathParameters,
     407             :   }) {
     408             :     // First check that we have the path parameters needed to have this path
     409             :     final indexNoMatch =
     410          19 :         pathParametersKeys.indexWhere((key) => !pathParameters.containsKey(key));
     411             : 
     412             :     // If we have all the path parameters needed, get the local path
     413             :     final localPath =
     414          36 :         (indexNoMatch == -1 && path != null) ? pathToFunction(path)(pathParameters) : null;
     415             : 
     416             :     late final String? newParentPath;
     417             :     if (path == null) {
     418             :       // If the path is null, the new parent path is the same as the previous one
     419           0 :       newParentPath = parentPath;
     420           9 :     } else if (path.startsWith('/')) {
     421           0 :       newParentPath = localPath;
     422             :     } else if (parentPath == null) {
     423             :       // if we don't start with '/' and parent path is null, then newParentPath is also null
     424           0 :       newParentPath = null;
     425             :     } else {
     426             :       // If localPath is null, the pathParameters did not match so newParentPath is null
     427           6 :       newParentPath = (localPath != null) ? parentPath + '/' + localPath : null;
     428             :     }
     429             : 
     430           0 :     return newParentPath;
     431             :   }
     432             : 
     433           8 :   GetPathFromPopResult? getPathFromPop(
     434             :     VRouteElement elementToPop, {
     435             :     required Map<String, String> pathParameters,
     436             :     required String? parentPath,
     437             :   }) {
     438             :     // If vRouteElement is this, then this is the element to pop so we return null
     439           8 :     if (elementToPop == this) {
     440           8 :       return GetPathFromPopResult(path: parentPath, didPop: true);
     441             :     }
     442             : 
     443             :     // Try to match the path given the path parameters
     444           8 :     final newParentPathFromPath = getNewParentPath(
     445             :       parentPath,
     446           8 :       path: path,
     447           8 :       pathParametersKeys: pathParametersKeys,
     448             :       pathParameters: pathParameters,
     449             :     );
     450             : 
     451          16 :     print('newParentPathFromPath: $newParentPathFromPath');
     452             : 
     453             :     // If the path matched and produced a non null newParentPath, try to pop from the stackedRoutes
     454             :     if (newParentPathFromPath != null) {
     455          16 :       for (var vRouteElement in stackedRoutes) {
     456           8 :         final childPopResult = vRouteElement.getPathFromPop(
     457             :           elementToPop,
     458             :           pathParameters: pathParameters,
     459             :           parentPath: newParentPathFromPath,
     460             :         );
     461             :         if (childPopResult != null) {
     462          24 :           print('childPopResult.path: ${childPopResult.path}');
     463          16 :           return GetPathFromPopResult(path: childPopResult.path, didPop: false);
     464             :         }
     465             :       }
     466             :     }
     467             : 
     468             :     // Try to match the aliases given the path parameters
     469           6 :     for (var i = 0; i < aliases.length; i++) {
     470           1 :       final newParentPathFromAlias = getNewParentPath(
     471             :         parentPath,
     472           2 :         path: aliases[i],
     473           2 :         pathParametersKeys: aliasesPathParametersKeys[i],
     474             :         pathParameters: pathParameters,
     475             :       );
     476             : 
     477             :       // If an alias matched and produced a non null newParentPath, try to pop from the stackedRoutes
     478             :       if (newParentPathFromAlias != null) {
     479             :         // Try to pop from the stackedRoutes
     480           2 :         for (var vRouteElement in stackedRoutes) {
     481           1 :           final childPopResult = vRouteElement.getPathFromPop(
     482             :             elementToPop,
     483             :             pathParameters: pathParameters,
     484             :             parentPath: newParentPathFromAlias,
     485             :           );
     486             :           if (childPopResult != null) {
     487           2 :             return GetPathFromPopResult(path: childPopResult.path, didPop: false);
     488             :           }
     489             :         }
     490             :       }
     491             :     }
     492             : 
     493             :     // If none of the stackedRoutes popped and this did not pop, return a null result
     494             :     return null;
     495             :   }
     496             : }
     497             : 
     498             : /// Return type for [VRouteElementWithPath.getPathMatch]
     499             : class GetPathMatchResult {
     500             :   final String? remainingPath;
     501             :   final Map<String, String> pathParameters;
     502             : 
     503          10 :   GetPathMatchResult({
     504             :     required this.remainingPath,
     505             :     required this.pathParameters,
     506             :   });
     507             : }

Generated by: LCOV version 1.14