@@ -67,6 +67,12 @@ - (NSString *)JLRoutes_fragmentQuery;
6767
6868@end
6969
70+ @interface NSArray (Combinations)
71+
72+ - (NSArray <NSArray *> *)JLRoutes_allOrderedCombinations ;
73+
74+ @end
75+
7076
7177#pragma mark -
7278
@@ -152,33 +158,18 @@ - (void)addRoutes:(NSArray<NSString *> *)routePatterns handler:(BOOL (^)(NSDicti
152158
153159- (void )addRoute : (NSString *)routePattern priority : (NSUInteger )priority handler : (BOOL (^)(NSDictionary <NSString *, id> *parameters))handlerBlock
154160{
155- // if there's a pair of parenthesis, process optionals, trim the parenthesis, put it on trimmedRoute
156- NSString *trimmedRoute = routePattern;
161+ NSArray <NSString *> *optionalRoutePatterns = [self _optionalRoutesForPattern: routePattern];
157162
158- // repeat until no parenthesis pair is found
159- while ([trimmedRoute rangeOfString: @" )" options: NSBackwardsSearch].location > [trimmedRoute rangeOfString: @" (" options: NSBackwardsSearch].location ) {
160- // build route with the optionals
161- NSString *patternWithOptionals = [trimmedRoute stringByReplacingOccurrencesOfString: @" (" withString: @" " ];
162- patternWithOptionals = [patternWithOptionals stringByReplacingOccurrencesOfString: @" )" withString: @" " ];
163- [self _registerRoute: patternWithOptionals priority: priority handler: handlerBlock];
164-
165- // build route without optionals
166- NSRange rangeOfLastParentheses = [trimmedRoute rangeOfString: @" (" options: NSBackwardsSearch];
167- NSRange rangeToRemove = NSMakeRange (rangeOfLastParentheses.location , trimmedRoute.length - rangeOfLastParentheses.location );
168- NSString *patternWithLastOptionalRemoved = [trimmedRoute stringByReplacingCharactersInRange: rangeToRemove withString: @" " ];
169-
170- // remove any parenthesis for other optionals that might still be in the route
171- NSString *patternWithoutOptionals = [patternWithLastOptionalRemoved stringByReplacingOccurrencesOfString: @" (" withString: @" " ];
172- patternWithoutOptionals = [patternWithoutOptionals stringByReplacingOccurrencesOfString: @" )" withString: @" " ];
173- [self _registerRoute: patternWithoutOptionals priority: priority handler: handlerBlock];
174-
175- trimmedRoute = patternWithLastOptionalRemoved;
163+ if (optionalRoutePatterns.count > 0 ) {
164+ // there are optional params, parse and add them
165+ for (NSString *route in optionalRoutePatterns) {
166+ [self _verboseLog: @" Automatically created optional route: %@ " , route];
167+ [self _registerRoute: route priority: priority handler: handlerBlock];
168+ }
169+ return ;
176170 }
177171
178- // Only register original route if trimmedRoute haven't been modified.
179- if (trimmedRoute == routePattern) {
180- [self _registerRoute: routePattern priority: priority handler: handlerBlock];
181- }
172+ [self _registerRoute: routePattern priority: priority handler: handlerBlock];
182173}
183174
184175- (void )removeRoute : (NSString *)routePattern
@@ -249,6 +240,15 @@ - (BOOL)routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters
249240
250241#pragma mark - Private
251242
243+ + (instancetype )_routesControllerForURL : (NSURL *)URL
244+ {
245+ if (URL == nil ) {
246+ return nil ;
247+ }
248+
249+ return routeControllersMap[URL.scheme] ?: [JLRoutes globalRoutes ];
250+ }
251+
252252- (void )_registerRoute : (NSString *)routePattern priority : (NSUInteger )priority handler : (BOOL (^)(NSDictionary *parameters))handlerBlock
253253{
254254 _JLRoute *route = [[_JLRoute alloc ] init ];
@@ -369,13 +369,102 @@ - (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters execute
369369 return didRoute;
370370}
371371
372- + (instancetype )_routesControllerForURL : (NSURL *)URL
372+ - (NSArray <NSString *> *)_optionalRoutesForPattern : (NSString *)routePattern
373+ {
374+ /* this method exists to take a route pattern that is known to contain optional params, such as:
375+
376+ /path/:thing/(/a)(/b)(/c)
377+
378+ and create the following paths:
379+
380+ /path/:thing/a/b/c
381+ /path/:thing/a/b
382+ /path/:thing/a/c
383+ /path/:thing/b/a
384+ /path/:thing/a
385+ /path/:thing/b
386+ /path/:thing/c
387+ */
388+
389+ if ([routePattern rangeOfString: @" (" ].location == NSNotFound ) {
390+ return nil ;
391+ }
392+
393+ NSString *baseRoute = nil ;
394+ NSArray *components = [self _optionalComponentsForPattern: routePattern baseRoute: &baseRoute];
395+ NSArray *routes = [self _routesForOptionalComponents: components baseRoute: baseRoute];
396+
397+ return routes;
398+ }
399+
400+ - (NSArray <NSString *> *)_optionalComponentsForPattern : (NSString *)routePattern baseRoute : (NSString **)outBaseRoute ;
373401{
374- if (URL == nil ) {
402+ if (routePattern. length == 0 ) {
375403 return nil ;
376404 }
377405
378- return routeControllersMap[URL.scheme] ?: [JLRoutes globalRoutes ];
406+ NSMutableArray *optionalComponents = [NSMutableArray array ];
407+
408+ NSScanner *scanner = [NSScanner scannerWithString: routePattern];
409+ NSString *nonOptionalRouteSubpath = nil ;
410+
411+ BOOL parsedBaseRoute = NO ;
412+ BOOL parseError = NO ;
413+
414+ // first, we need to parse the string and find the array of optional params.
415+ // aka, take (/a)(/b)(/c) and turn it into ["/a", "/b", "/c"]
416+ while ([scanner scanUpToString: @" (" intoString: &nonOptionalRouteSubpath]) {
417+ if ([scanner isAtEnd ]) {
418+ break ;
419+ }
420+
421+ if (nonOptionalRouteSubpath.length > 0 && outBaseRoute != NULL && !parsedBaseRoute) {
422+ // the first 'non optional subpath' is always the base route
423+ *outBaseRoute = nonOptionalRouteSubpath;
424+ parsedBaseRoute = YES ;
425+ }
426+
427+ scanner.scanLocation = scanner.scanLocation + 1 ;
428+
429+ NSString *component = nil ;
430+ if (![scanner scanUpToString: @" )" intoString: &component]) {
431+ parseError = YES ;
432+ break ;
433+ }
434+
435+ [optionalComponents addObject: component];
436+ }
437+
438+ if (parseError) {
439+ NSLog (@" [JLRoutes]: Parse error, unsupported route: %@ " , routePattern);
440+ return nil ;
441+ }
442+
443+ return [optionalComponents copy ];
444+ }
445+
446+ - (NSArray <NSString *> *)_routesForOptionalComponents : (NSArray <NSString *> *)optionalComponents baseRoute : (NSString *)baseRoute
447+ {
448+ if (optionalComponents.count == 0 || baseRoute.length == 0 ) {
449+ return nil ;
450+ }
451+
452+ NSMutableArray *routes = [NSMutableArray array ];
453+
454+ // generate all possible combinations of the components that could exist (taking order into account)
455+ // aka, "/path/:thing/(/a)(/b)(/c)" should never generate a route for "/path/:thing/(/b)(/a)"
456+ NSArray *combinations = [optionalComponents JLRoutes_allOrderedCombinations ];
457+
458+ // generate the actual final route path strings
459+ for (NSArray *components in combinations) {
460+ NSString *path = [components componentsJoinedByString: @" " ];
461+ [routes addObject: [baseRoute stringByAppendingString: path]];
462+ }
463+
464+ // sort them so that the longest routes are first (since they are the most selective)
465+ [routes sortUsingSelector: @selector (length )];
466+
467+ return [routes copy ];
379468}
380469
381470- (BOOL )_isGlobalRoutesController
@@ -480,8 +569,7 @@ - (NSDictionary *)_parametersForURL:(NSURL *)URL patternComponents:(NSArray *)pa
480569 NSString *urlDecodedVariableValue = [variableValue JLRoutes_URLDecodedString ];
481570 if ([variableName length ] > 0 && [urlDecodedVariableValue length ] > 0 ) {
482571 variables[variableName] = urlDecodedVariableValue;
483- }
484- else {
572+ } else {
485573 NSMutableArray * newComponents = [NSMutableArray arrayWithArray: components];
486574 [newComponents addObject: @" " ];
487575 components = newComponents;
@@ -571,6 +659,29 @@ - (NSString *)JLRoutes_fragmentQuery
571659
572660@end
573661
662+ @implementation NSArray (JLRoutes)
663+
664+ - (NSArray <NSArray *> *)JLRoutes_allOrderedCombinations
665+ {
666+ NSInteger length = self.count ;
667+ if (length == 0 ) {
668+ return [NSArray arrayWithObject: [NSArray array ]];
669+ }
670+
671+ id lastObject = [self lastObject ];
672+ NSArray *subarray = [self subarrayWithRange: NSMakeRange (0 , length - 1 )];
673+ NSArray *subarrayCombinations = [subarray JLRoutes_allOrderedCombinations ];
674+ NSMutableArray *combinations = [NSMutableArray arrayWithArray: subarrayCombinations];
675+
676+ for (NSArray *subarrayCombos in subarrayCombinations) {
677+ [combinations addObject: [subarrayCombos arrayByAddingObject: lastObject]];
678+ }
679+
680+ return [NSArray arrayWithArray: combinations];
681+ }
682+
683+ @end
684+
574685
575686#pragma mark - Global Options
576687
0 commit comments