Skip to content

Commit d3e7dfe

Browse files
committed
Merge pull request #47 from usebutton/chris_wes/regex_refactor
Add Regex-filters to named components. Refactor named groups support into RegularExpression subclass
2 parents cfb8577 + dc588ea commit d3e7dfe

File tree

11 files changed

+309
-59
lines changed

11 files changed

+309
-59
lines changed

DeepLinkSDK.xcodeproj/project.pbxproj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
1252340A196B4771BE27D5FD /* libPods-ReceiverDemo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CFE63F7FEA51182807D98A78 /* libPods-ReceiverDemo.a */; };
1111
2F4988DE1AE71ABC0069EF2B /* DPLRouteHandlerIntegrationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2F4988DD1AE71ABC0069EF2B /* DPLRouteHandlerIntegrationTest.m */; };
12+
4D4F412B1B02A96400B710DB /* DPLRegularExpressionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D4F412A1B02A96400B710DB /* DPLRegularExpressionSpec.m */; };
1213
6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
1314
6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; };
1415
6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
@@ -65,6 +66,11 @@
6566
19367BAF0FEDE5B798128F3D /* libPods-Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6667
2AE3E05821FBC0C05F248E61 /* Pods-Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.debug.xcconfig"; sourceTree = "<group>"; };
6768
2F4988DD1AE71ABC0069EF2B /* DPLRouteHandlerIntegrationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DPLRouteHandlerIntegrationTest.m; path = IntegrationTests/DPLRouteHandlerIntegrationTest.m; sourceTree = "<group>"; };
69+
4D4F41231B0298FF00B710DB /* DPLRegularExpression.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPLRegularExpression.h; sourceTree = "<group>"; };
70+
4D4F41241B0298FF00B710DB /* DPLRegularExpression.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPLRegularExpression.m; sourceTree = "<group>"; };
71+
4D4F41251B029D9E00B710DB /* DPLMatchResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DPLMatchResult.h; sourceTree = "<group>"; };
72+
4D4F41261B029D9E00B710DB /* DPLMatchResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DPLMatchResult.m; sourceTree = "<group>"; };
73+
4D4F412A1B02A96400B710DB /* DPLRegularExpressionSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DPLRegularExpressionSpec.m; sourceTree = "<group>"; };
6874
57D5F02E049D7887B4F4ACDF /* Pods-ReceiverDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReceiverDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReceiverDemo/Pods-ReceiverDemo.debug.xcconfig"; sourceTree = "<group>"; };
6975
6003F58A195388D20070C39A /* ReceiverDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReceiverDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
7076
6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
@@ -220,6 +226,25 @@
220226
name = Pods;
221227
sourceTree = "<group>";
222228
};
229+
4D4F41221B0298AA00B710DB /* Regex */ = {
230+
isa = PBXGroup;
231+
children = (
232+
4D4F41231B0298FF00B710DB /* DPLRegularExpression.h */,
233+
4D4F41241B0298FF00B710DB /* DPLRegularExpression.m */,
234+
4D4F41251B029D9E00B710DB /* DPLMatchResult.h */,
235+
4D4F41261B029D9E00B710DB /* DPLMatchResult.m */,
236+
);
237+
path = Regex;
238+
sourceTree = "<group>";
239+
};
240+
4D4F41291B02A95800B710DB /* Regex */ = {
241+
isa = PBXGroup;
242+
children = (
243+
4D4F412A1B02A96400B710DB /* DPLRegularExpressionSpec.m */,
244+
);
245+
path = Regex;
246+
sourceTree = "<group>";
247+
};
223248
6003F581195388D10070C39A = {
224249
isa = PBXGroup;
225250
children = (
@@ -405,6 +430,7 @@
405430
DE99EF6A1A3B6CDD00CE3449 /* Protocols */,
406431
DE3E61071A3B4485008D6DFC /* Categories */,
407432
DEB4EDBB1A49CEA400F31D14 /* Errors */,
433+
4D4F41221B0298AA00B710DB /* Regex */,
408434
DE16E91F1A42882F00077E18 /* Router */,
409435
DEAC406F1A5DA7B8004A9095 /* RouteHandler */,
410436
DE16E9331A4289D500077E18 /* RouteMatcher */,
@@ -481,6 +507,7 @@
481507
children = (
482508
2F4988DC1AE71A930069EF2B /* IntegrationTests */,
483509
DEEBD4A61AAB7928000BCA84 /* Fixtures */,
510+
4D4F41291B02A95800B710DB /* Regex */,
484511
DE16E9221A42883B00077E18 /* Router */,
485512
DE16E9361A4289DF00077E18 /* RouteMatcher */,
486513
DE058E0E1A3B485500147C04 /* DeepLink */,
@@ -806,6 +833,7 @@
806833
files = (
807834
DE16E9281A42883B00077E18 /* DPLDeepLinkRouterSpec.m in Sources */,
808835
DE16E9381A4289DF00077E18 /* DPLRouteMatcherSpec.m in Sources */,
836+
4D4F412B1B02A96400B710DB /* DPLRegularExpressionSpec.m in Sources */,
809837
DECB32531A881CA10071C76E /* NSObject_DPLJSONObjectSpec.m in Sources */,
810838
DEEBD4A91AAB7946000BCA84 /* DPLSerializableObject.m in Sources */,
811839
DE058E101A3B485500147C04 /* DPLDeepLinkSpec.m in Sources */,

DeepLinkSDK/Regex/DPLMatchResult.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#import <Foundation/Foundation.h>
2+
3+
@interface DPLMatchResult : NSObject
4+
5+
@property (nonatomic, assign, getter=isMatch) BOOL match;
6+
@property (nonatomic, strong) NSDictionary *namedProperties;
7+
8+
@end

DeepLinkSDK/Regex/DPLMatchResult.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#import "DPLMatchResult.h"
2+
3+
@implementation DPLMatchResult
4+
5+
@end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#import <Foundation/Foundation.h>
2+
#import "DPLMatchResult.h"
3+
4+
@interface DPLRegularExpression : NSRegularExpression
5+
6+
@property (nonatomic, strong) NSArray *groupNames;
7+
8+
+ (instancetype)regularExpressionWithPattern:(NSString *)pattern;
9+
10+
- (DPLMatchResult *)matchResultForString:(NSString *)str;
11+
12+
@end
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#import "DPLRegularExpression.h"
2+
3+
static NSString * const DPLNamedGroupComponentPattern = @":[a-zA-Z0-9-_][^/]+";
4+
static NSString * const DPLRouteParameterPattern = @":[a-zA-Z0-9-_]+";
5+
static NSString * const DPLURLParameterPattern = @"([^/]+)";
6+
7+
@implementation DPLRegularExpression
8+
9+
+ (instancetype)regularExpressionWithPattern:(NSString *)pattern {
10+
return [[self alloc] initWithPattern:pattern options:0 error:nil];
11+
}
12+
13+
14+
- (instancetype)initWithPattern:(NSString *)pattern
15+
options:(NSRegularExpressionOptions)options
16+
error:(NSError *__autoreleasing *)error {
17+
18+
NSString *cleanedPattern = [[self class] stringByRemovingNamedGroupsFromString:pattern];
19+
20+
self = [super initWithPattern:cleanedPattern options:options error:error];
21+
if (self) {
22+
self.groupNames = [[self class] namedGroupsForString:pattern];
23+
}
24+
return self;
25+
}
26+
27+
28+
- (DPLMatchResult *)matchResultForString:(NSString *)str {
29+
NSArray *matches = [self matchesInString:str options:0 range:NSMakeRange(0, str.length)];
30+
DPLMatchResult *matchResult = [[DPLMatchResult alloc] init];
31+
32+
if (!matches.count) {
33+
return matchResult;
34+
}
35+
36+
matchResult.match = YES;
37+
38+
// Set route parameters in the routeParameters dictionary
39+
NSMutableDictionary *routeParameters = [NSMutableDictionary dictionary];
40+
for (NSTextCheckingResult *result in matches) {
41+
// Begin at 1 as first range is the whole match
42+
for (NSInteger i = 1; i < result.numberOfRanges && i <= self.groupNames.count; i++) {
43+
NSString *parameterName = self.groupNames[i - 1];
44+
NSString *parameterValue = [str substringWithRange:[result rangeAtIndex:i]];
45+
routeParameters[parameterName] = parameterValue;
46+
}
47+
}
48+
49+
matchResult.namedProperties = routeParameters;
50+
return matchResult;
51+
}
52+
53+
54+
#pragma mark - Named Group Helpers
55+
56+
+ (NSArray *)namedGroupTokensForString:(NSString *)str {
57+
NSRegularExpression *componentRegex = [NSRegularExpression regularExpressionWithPattern:DPLNamedGroupComponentPattern
58+
options:0
59+
error:nil];
60+
NSArray *matches = [componentRegex matchesInString:str
61+
options:0
62+
range:NSMakeRange(0, str.length)];
63+
64+
NSMutableArray *namedGroupTokens = [NSMutableArray array];
65+
for (NSTextCheckingResult *result in matches) {
66+
NSString *namedGroupToken = [str substringWithRange:result.range];
67+
[namedGroupTokens addObject:namedGroupToken];
68+
}
69+
return [NSArray arrayWithArray:namedGroupTokens];
70+
}
71+
72+
73+
+ (NSString *)stringByRemovingNamedGroupsFromString:(NSString *)str {
74+
NSString *modifiedStr = [str copy];
75+
76+
NSArray *namedGroupExpressions = [self namedGroupTokensForString:str];
77+
NSRegularExpression *parameterRegex = [NSRegularExpression regularExpressionWithPattern:DPLRouteParameterPattern
78+
options:0
79+
error:nil];
80+
// For each of the named group expressions (including name & regex)
81+
for (NSString *namedExpression in namedGroupExpressions) {
82+
NSString *replacementExpression = [namedExpression copy];
83+
NSTextCheckingResult *foundGroupName = [[parameterRegex matchesInString:namedExpression
84+
options:0
85+
range:NSMakeRange(0, namedExpression.length)] firstObject];
86+
// If it's a named group, remove the name
87+
if (foundGroupName) {
88+
NSString *stringToReplace = [namedExpression substringWithRange:foundGroupName.range];
89+
replacementExpression = [replacementExpression stringByReplacingOccurrencesOfString:stringToReplace
90+
withString:@""];
91+
}
92+
93+
// If it was a named group, without regex constraining it, put in default regex
94+
if (replacementExpression.length == 0) {
95+
replacementExpression = DPLURLParameterPattern;
96+
}
97+
98+
modifiedStr = [modifiedStr stringByReplacingOccurrencesOfString:namedExpression
99+
withString:replacementExpression];
100+
}
101+
102+
if (modifiedStr.length && !([modifiedStr characterAtIndex:0] == '/')) {
103+
modifiedStr = [@"^" stringByAppendingString:modifiedStr];
104+
}
105+
modifiedStr = [modifiedStr stringByAppendingString:@"$"];
106+
107+
return modifiedStr;
108+
}
109+
110+
111+
+ (NSArray *)namedGroupsForString:(NSString *)str {
112+
NSMutableArray *groupNames = [NSMutableArray array];
113+
114+
NSArray *namedGroupExpressions = [self namedGroupTokensForString:str];
115+
NSRegularExpression *parameterRegex = [NSRegularExpression regularExpressionWithPattern:DPLRouteParameterPattern
116+
options:0
117+
error:nil];
118+
119+
for (NSString *namedExpression in namedGroupExpressions) {
120+
NSArray *componentMatches = [parameterRegex matchesInString:namedExpression
121+
options:0
122+
range:NSMakeRange(0, namedExpression.length)];
123+
NSTextCheckingResult *foundGroupName = [componentMatches firstObject];
124+
if (foundGroupName) {
125+
NSString *stringToReplace = [namedExpression substringWithRange:foundGroupName.range];
126+
NSString *variableName = [stringToReplace stringByReplacingOccurrencesOfString:@":"
127+
withString:@""];
128+
129+
[groupNames addObject:variableName];
130+
}
131+
}
132+
return [NSArray arrayWithArray:groupNames];
133+
}
134+
135+
@end

DeepLinkSDK/RouteMatcher/DPLRouteMatcher.m

Lines changed: 7 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
#import "DPLRouteMatcher.h"
22
#import "DPLDeepLink_Private.h"
33
#import "NSString+DPLTrim.h"
4-
5-
static NSString * const DPLRouteParameterPattern = @":[a-zA-Z0-9-_]+";
6-
static NSString * const DPLURLParameterPattern = @"([^/]+)";
4+
#import "DPLRegularExpression.h"
75

86
@interface DPLRouteMatcher ()
97

10-
@property (nonatomic, copy) NSString *route;
11-
@property (nonatomic, strong) NSRegularExpression *regex;
12-
@property (nonatomic, strong) NSMutableArray *routeParamaterNames;
8+
@property (nonatomic, strong) DPLRegularExpression *regexMatcher;
139

1410
@end
1511

@@ -27,70 +23,26 @@ - (instancetype)initWithRoute:(NSString *)route {
2723

2824
self = [super init];
2925
if (self) {
30-
_route = route;
26+
_regexMatcher = [DPLRegularExpression regularExpressionWithPattern:route];
3127
}
3228

3329
return self;
3430
}
3531

3632

37-
- (NSRegularExpression *)regex {
38-
if (!_regex) {
39-
_routeParamaterNames = [NSMutableArray array];
40-
NSRegularExpression *parameterRegex = [NSRegularExpression regularExpressionWithPattern:DPLRouteParameterPattern
41-
options:0
42-
error:nil];
43-
44-
__block NSString *modifiedRoute = [self.route copy];
45-
NSArray *matches = [parameterRegex matchesInString:self.route
46-
options:0
47-
range:NSMakeRange(0, self.route.length)];
48-
49-
for (NSTextCheckingResult *result in matches) {
50-
51-
NSString *stringToReplace = [self.route substringWithRange:result.range];
52-
NSString *variableName = [stringToReplace stringByReplacingOccurrencesOfString:@":"
53-
withString:@""];
54-
[self.routeParamaterNames addObject:variableName];
55-
56-
modifiedRoute = [modifiedRoute stringByReplacingOccurrencesOfString:stringToReplace
57-
withString:DPLURLParameterPattern];
58-
}
59-
60-
modifiedRoute = [modifiedRoute stringByAppendingString:@"$"];
61-
_regex = [NSRegularExpression regularExpressionWithPattern:modifiedRoute
62-
options:0
63-
error:nil];
64-
}
65-
66-
return _regex;
67-
}
68-
69-
7033
- (DPLDeepLink *)deepLinkWithURL:(NSURL *)url {
7134

7235
DPLDeepLink *deepLink = [[DPLDeepLink alloc] initWithURL:url];
7336
NSString *deepLinkString = [NSString stringWithFormat:@"%@%@",
7437
deepLink.URL.host, deepLink.URL.path];
75-
NSArray *matches = [self.regex matchesInString:deepLinkString
76-
options:0
77-
range:NSMakeRange(0, deepLinkString.length)];
7838

79-
if (!matches.count) {
39+
40+
DPLMatchResult *matchResult = [self.regexMatcher matchResultForString:deepLinkString];
41+
if (!matchResult.isMatch) {
8042
return nil;
8143
}
8244

83-
// Set route parameters in the routeParameters dictionary
84-
NSMutableDictionary *routeParameters = [NSMutableDictionary dictionary];
85-
for (NSTextCheckingResult *result in matches) {
86-
// Begin at 1 as first range is the whole match
87-
for (int i = 1; i < result.numberOfRanges && i <= self.routeParamaterNames.count; i++) {
88-
NSString *parameterName = self.routeParamaterNames[i - 1];
89-
NSString *parameterValue = [deepLinkString substringWithRange:[result rangeAtIndex:i]];
90-
routeParameters[parameterName] = parameterValue;
91-
}
92-
}
93-
deepLink.routeParameters = routeParameters;
45+
deepLink.routeParameters = matchResult.namedProperties;
9446

9547
return deepLink;
9648
}

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
desc "Runs the specs"
22
task :spec do
3-
sh "xcodebuild test -workspace 'DeepLinkSDK.xcworkspace' -scheme 'DeepLinkSDK' -destination 'platform=iOS Simulator,name=iPhone 6,OS=8.1' | xcpretty -c"
3+
sh "xcodebuild test -workspace 'DeepLinkSDK.xcworkspace' -scheme 'ReceiverDemo' -destination 'platform=iOS Simulator,name=iPhone 6,OS=8.1' | xcpretty -c"
44
end
55

66
desc "Synchronizes Xcode project folder with Xcode groups"

Tests/IntegrationTests/DPLRouteHandlerIntegrationTest.m

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ @interface DPLRouteHandlerIntegrationTest : XCTestCase
77

88
@implementation DPLRouteHandlerIntegrationTest
99

10-
- (void)testClassBasedRouteHandlerFlow
11-
{
10+
- (void)testClassBasedRouteHandlerFlow {
1211
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"dpl://dpl.com/product/93598?"]];
1312
[tester waitForViewWithAccessibilityLabel:@"Shiner Oktoberfest"];
1413
[tester waitForViewWithAccessibilityLabel:@"8.99"];
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#import "Specta.h"
2+
#import "DPLRegularExpression.h"
3+
4+
SpecBegin(DPLRegularExpression)
5+
6+
describe(@"Matching named components", ^{
7+
8+
it(@"should match named components without regex", ^{
9+
DPLRegularExpression *expression = [DPLRegularExpression regularExpressionWithPattern:@"/hello/:do/:this/and/:that"];
10+
11+
DPLMatchResult *matchResult = [expression matchResultForString:@"/hello/dovalue/thisvalue/and/thatvalue"];
12+
expect(matchResult.match).to.beTruthy();
13+
expect(matchResult.namedProperties).to.equal(@{ @"do": @"dovalue",
14+
@"this": @"thisvalue",
15+
@"that": @"thatvalue" });
16+
});
17+
18+
it(@"should match named components with regex", ^{
19+
DPLRegularExpression *expression = [DPLRegularExpression regularExpressionWithPattern:@"/hello/:do([a-zA-Z]+)/:this([a-zA-Z]+)/and/:that([a-zA-Z]+)"];
20+
21+
DPLMatchResult *matchResult = [expression matchResultForString:@"/hello/dovalue/thisvalue/and/thatvalue"];
22+
expect(matchResult.match).to.beTruthy();
23+
expect(matchResult.namedProperties).to.equal(@{ @"do": @"dovalue",
24+
@"this": @"thisvalue",
25+
@"that": @"thatvalue" });
26+
});
27+
28+
it(@"should match a mixture of with and without regex", ^{
29+
DPLRegularExpression *expression = [DPLRegularExpression regularExpressionWithPattern:@"/hello/:do/:this([a-zA-Z]+)/and/:that([a-zA-Z]+)"];
30+
31+
DPLMatchResult *matchResult = [expression matchResultForString:@"/hello/dovalue/thisvalue/and/thatvalue"];
32+
expect(matchResult.match).to.beTruthy();
33+
expect(matchResult.namedProperties).to.equal(@{ @"do": @"dovalue",
34+
@"this": @"thisvalue",
35+
@"that": @"thatvalue" });
36+
});
37+
38+
it(@"should NOT match named components with regex that does not match", ^{
39+
DPLRegularExpression *expression = [DPLRegularExpression regularExpressionWithPattern:@"/hello/:do([a-zA-Z]+)/:this([0-9]+)/and/:that([a-zA-Z]+)"];
40+
41+
DPLMatchResult *matchResult = [expression matchResultForString:@"/hello/dovalue/thisvalue/and/thatvalue"];
42+
expect(matchResult.match).to.beFalsy();
43+
});
44+
45+
});
46+
47+
SpecEnd

0 commit comments

Comments
 (0)