Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue-89 - Add support for arrays in query parameters #98

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions DeepLinkKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
DEE40E5F1A42B0530097EA33 /* DPLActionDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE40E5D1A42ABE80097EA33 /* DPLActionDataSource.m */; };
DEE40E601A42B0580097EA33 /* DPLDemoAction.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE40E581A42A3F50097EA33 /* DPLDemoAction.m */; };
DEEBD4A91AAB7946000BCA84 /* DPLSerializableObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DEEBD4A81AAB7946000BCA84 /* DPLSerializableObject.m */; };
E9A189201CB6E6600029E56B /* DPLArrayRouteHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A1891F1CB6E6600029E56B /* DPLArrayRouteHandler.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -190,6 +191,8 @@
DEEBD4A71AAB7946000BCA84 /* DPLSerializableObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DPLSerializableObject.h; path = Fixtures/DPLSerializableObject.h; sourceTree = "<group>"; };
DEEBD4A81AAB7946000BCA84 /* DPLSerializableObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DPLSerializableObject.m; path = Fixtures/DPLSerializableObject.m; sourceTree = "<group>"; };
DF9272621ECB6C2824AD5C94 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ./LICENSE; sourceTree = "<group>"; };
E989F5AC1CB709B8005D51D6 /* NSString+DPLQuery_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+DPLQuery_Private.h"; sourceTree = "<group>"; };
E9A1891F1CB6E6600029E56B /* DPLArrayRouteHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DPLArrayRouteHandler.swift; sourceTree = "<group>"; };
E9CA1DB95577CF3689F4B77F /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ./README.md; sourceTree = "<group>"; };
EC3E153594BC11ACCA16FACF /* Pods_ReceiverDemoSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReceiverDemoSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FB774EA6D84A50233F032ADC /* Pods-ReceiverDemoSwift.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReceiverDemoSwift.test.xcconfig"; path = "Pods/Target Support Files/Pods-ReceiverDemoSwift/Pods-ReceiverDemoSwift.test.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -343,6 +346,7 @@
children = (
62EE96F31B570891003D7564 /* DPLProductRouteHandler.swift */,
62891E8A1B57FE5A00C2AF4F /* DPLMessageRouteHandler.swift */,
E9A1891F1CB6E6600029E56B /* DPLArrayRouteHandler.swift */,
);
path = RouteHandlers;
sourceTree = "<group>";
Expand Down Expand Up @@ -470,6 +474,7 @@
DE16E91C1A42720D00077E18 /* NSString+DPLTrim.h */,
DE16E91B1A42720D00077E18 /* NSString+DPLTrim.m */,
DE058E071A3B46F500147C04 /* NSString+DPLQuery.h */,
E989F5AC1CB709B8005D51D6 /* NSString+DPLQuery_Private.h */,
DE058E081A3B46F500147C04 /* NSString+DPLQuery.m */,
DE3E61081A3B4485008D6DFC /* NSString+DPLJSON.h */,
DE3E61091A3B4485008D6DFC /* NSString+DPLJSON.m */,
Expand Down Expand Up @@ -1076,6 +1081,7 @@
buildActionMask = 2147483647;
files = (
62EE96F61B570B10003D7564 /* DPLProductDataSource.m in Sources */,
E9A189201CB6E6600029E56B /* DPLArrayRouteHandler.swift in Sources */,
62EE96F51B570AF4003D7564 /* DPLProductDetailViewController.m in Sources */,
62335E031B57007F00E3818C /* DPLReceiverSwiftAppDelegate.swift in Sources */,
62891E8B1B57FE5A00C2AF4F /* DPLMessageRouteHandler.swift in Sources */,
Expand Down
69 changes: 62 additions & 7 deletions DeepLinkKit/Categories/NSString+DPLQuery.m
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
#import "NSString+DPLQuery.h"

static NSString * const DPL_ArrayLiteral = @"[]";

@implementation NSString (UBRQuery)

+ (NSString *)DPL_queryStringWithParameters:(NSDictionary *)parameters {
NSMutableString *query = [NSMutableString string];
[[parameters allKeys] enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL *stop) {
NSString *value = [parameters[key] description];
key = [key DPL_stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
value = [value DPL_stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[query appendFormat:@"%@%@%@%@", (idx > 0) ? @"&" : @"", key, (value.length > 0) ? @"=" : @"", value];
NSString *value = @"";
if ([parameters[key] isKindOfClass:[NSArray class]]) {
NSString *keyValuePair = nil;
NSString *arrayValueString = nil;
for (NSString *arrayValue in parameters[key]) {
arrayValueString = [arrayValue.description DPL_stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
keyValuePair = [NSString stringWithFormat:@"%@[]=%@", key, arrayValueString];
keyValuePair = [NSString stringWithFormat:@"%@%@", (value.length > 0) ? @"&" : @"", keyValuePair];
value = [value stringByAppendingString:keyValuePair];
}
if (value.length == 0) {
value = [NSString stringWithFormat:@"%@[]", key];
}
key = @"";
}
else {
value = [parameters[key] description];
key = [key DPL_stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
value = [value DPL_stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}
[query appendFormat:@"%@%@%@%@", (idx > 0) ? @"&" : @"", key, (value.length > 0 && key.length > 0) ? @"=" : @"", value];
}];
return [query copy];
}
Expand All @@ -23,12 +42,33 @@ - (NSDictionary *)DPL_parametersFromQueryString {
// e.g. ?key=value
NSString *key = [pairs[0] DPL_stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *value = [pairs[1] DPL_stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
paramsDict[key] = value;
if ([key DPL_containsArrayLiteral]) {
// e.g. ?items[]=item1&items[]=item2
key = [key DPL_stringByRemovingArrayLiteral];
if (!paramsDict[key]) {
paramsDict[key] = @[value];
}
else {
paramsDict[key] = [paramsDict[key] arrayByAddingObject:value];
}
}
else {
paramsDict[key] = value;
}
}
else if (pairs.count == 1) {
// e.g. ?key
NSString *key = [[pairs firstObject] DPL_stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
paramsDict[key] = @"";
NSObject *value;
if ([key DPL_containsArrayLiteral]) {
// e.g. ?items[]
key = [key DPL_stringByRemovingArrayLiteral];
value = @[];
}
else {
// e.g. ?key
value = @"";
}
paramsDict[key] = value;
}
}
return [paramsDict copy];
Expand All @@ -47,4 +87,19 @@ - (NSString *)DPL_stringByReplacingPercentEscapesUsingEncoding:(NSStringEncoding
return [self stringByRemovingPercentEncoding];
}


#pragma mark - Array Literals

- (BOOL)DPL_containsArrayLiteral {
return [self hasSuffix:DPL_ArrayLiteral];
}


- (NSString *)DPL_stringByRemovingArrayLiteral {
if ([self DPL_containsArrayLiteral]) {
return [self substringWithRange:NSMakeRange(0, self.length - DPL_ArrayLiteral.length)];
}
return [self copy];
}

@end
25 changes: 25 additions & 0 deletions DeepLinkKit/Categories/NSString+DPLQuery_Private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#import "NSString+DPLQuery.h"

@interface NSString (DPLQuery_Private)



///---------------------
/// @name Array Literals
///---------------------


/**
Checks if string ends with array literal ('[]')
@return YES if string ends with '[]', NO otherwise
*/
- (BOOL)DPL_containsArrayLiteral;


/**
Removes array literal ('[]') from the string
@return String without array literal ('[]')
*/
- (NSString *)DPL_stringByRemovingArrayLiteral;

@end
1 change: 1 addition & 0 deletions DeepLinkKit/DeepLinkKit_Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
#import "NSString+DPLJSON.h"
#import "NSObject+DPLJSONObject.h"
#import "UINavigationController+DPLRouting.h"
#import "NSString+DPLQuery_Private.h"
15 changes: 15 additions & 0 deletions SampleApps/ReceiverDemo/DPLReceiverAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
[navController pushViewController:controller animated:NO];
};

self.router[@"shoppinglist"] = ^(DPLDeepLink *link) {
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Shopping List", nil)
message:NSLocalizedString(@"Please buy the following:", nil)
preferredStyle:UIAlertControllerStyleActionSheet];
for (NSString *title in link[@"list"]) {
[actionSheet addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:NULL]];
}
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:NULL]];
UIViewController *rootViewController = weakSelf.window.rootViewController;
if ([rootViewController.presentedViewController isKindOfClass:[UIAlertController class]]) {
[rootViewController dismissViewControllerAnimated:NO completion:NULL];
}
[rootViewController presentViewController:actionSheet animated:YES completion:NULL];
};

return YES;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class DPLReceiverSwiftAppDelegate: UIResponder, UIApplicationDelegate {

// Register a class to a route using object subscripting
self.router["/product/:sku"] = DPLProductRouteHandler.self

self.router["shoppinglist"] = DPLArrayRouteHandler.self

// Register a class to a route using the explicit registration call
self.router.registerHandlerClass(DPLMessageRouteHandler.self, forRoute: "/say/:title/:message")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public class DPLArrayRouteHandler: DPLRouteHandler {
public override func shouldHandleDeepLink(deepLink: DPLDeepLink!) -> Bool {
if let list = deepLink["list"] as? Array<String> {
let alertController = UIAlertController(title: NSLocalizedString("Shopping List", comment: ""),
message: NSLocalizedString("Please buy the following:", comment: ""),
preferredStyle: .ActionSheet)
for title in list {
alertController.addAction(UIAlertAction(title: title, style: .Default, handler: nil))
}
alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController, animated: true, completion: nil)
}
return false
}
}
13 changes: 12 additions & 1 deletion SampleApps/SenderDemo/ActionData/DPLActionDataSource.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ - (instancetype)init {
if (self) {
_actions = @[[self loadBeersAction],
[self loadOktoberfestAction],
[self logHelloWorldAction]];
[self logHelloWorldAction],
[self logShoppingList]];
}
return self;
}
Expand Down Expand Up @@ -74,4 +75,14 @@ - (DPLDemoAction *)logHelloWorldAction {
return action;
}


- (DPLDemoAction *)logShoppingList {
DPLMutableDeepLink *link = [[DPLMutableDeepLink alloc] initWithString:@"dpl://shoppinglist?list%5B%5D%3DBread&list%5B%5D%3DCandies&list%5B%5D%3DWine&list%5B%5D%3DBeer"];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if here should be easier way to initialize deep link with URLs of such kind?


DPLDemoAction *action = [[DPLDemoAction alloc] init];
action.actionURL = link.URL;
action.actionName = @"Print: Shopping List";
return action;
}

@end
110 changes: 110 additions & 0 deletions Tests/Categories/NSString_DPLQuerySpec.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "NSString+DPLQuery.h"
#import "NSString+DPLQuery_Private.h"

SpecBegin(NSString_DPLQuery)

Expand All @@ -18,6 +19,7 @@
NSString *encodedString = [plainTestString DPL_stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
expect(encodedString).to.equal(plainTestString);
});

});


Expand All @@ -32,6 +34,7 @@
NSString *decodedString = [plainTestString DPL_stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
expect(decodedString).to.equal(plainTestString);
});

});


Expand All @@ -54,6 +57,40 @@
NSString *query = [NSString DPL_queryStringWithParameters:params];
expect(query).to.equal(@"one=a%20one&two=http%3A%2F%2Fwww.example.com%3Ffoo%3Dbar");
});

it(@"should serialize array from dictionary into the query string", ^{
NSDictionary *params = @{ @"beers": @[ @"stout", @"ale" ] };
NSString *query = [NSString DPL_queryStringWithParameters:params];
expect(query).to.equal(@"beers[]=stout&beers[]=ale");
});

it(@"should serialize empty array from dictionary into the query string", ^{
NSDictionary *params = @{ @"beers": @[ ] };
NSString *query = [NSString DPL_queryStringWithParameters:params];
expect(query).to.equal(@"beers[]");
});

it(@"should serialize multiple arrays from dictionary into the query string", ^{
NSDictionary *params = @{ @"beers": @[ @"stout", @"ale" ], @"liquors": @[ @"vodka", @"whiskey" ] };
NSString *query = [NSString DPL_queryStringWithParameters:params];

NSString *queryStringToMatch;
NSString *queryStringPartBeers = @"beers[]=stout&beers[]=ale";
NSString *queryStringPartLiquors = @"liquors[]=vodka&liquors[]=whiskey";
if ([[params allKeys][0] isEqualToString:@"beers"]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably have a suitable default here such as ordered ascending that way we can expect a consistent result.

Similarly, when parsing a url, I would expect to have the parameter order preserved so re-serialization mirrors the original url though that can't really happen in the category (#15) without more information.

For example:

NSURL *url = [NSURL URLWithString:@"dpl://something/?liquors[]=vodka&liquors[]=whiskey&beers[]=stout&beers[]=ale"];
DPLDeepLink *link = [[DPLDeepLink alloc] initWithURL:url];
link.URL.query  // should serialize as => @"liquors[]=vodka&liquors[]=whiskey&beers[]=stout&beers[]=ale"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wessmith Just making sure I understand that correctly: you are talking here about DPLMutableDeepLink not DPLDeepLinkright? (DPLDeepLink returns previously set URL without any serialization work).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Yeah.

queryStringToMatch = [NSString stringWithFormat:@"%@&%@", queryStringPartBeers, queryStringPartLiquors];
} else {
queryStringToMatch = [NSString stringWithFormat:@"%@&%@", queryStringPartLiquors, queryStringPartBeers];
}
expect(query).to.equal(queryStringToMatch);
});

it(@"should percent encode parameters from dictionary into the query array", ^{
NSDictionary *params = @{ @"one": @"a one", @"two": @[ @"http://www.example.com?foo=bar", @"a two" ] };
NSString *query = [NSString DPL_queryStringWithParameters:params];
expect(query).to.equal(@"one=a%20one&two[]=http%3A%2F%2Fwww.example.com%3Ffoo%3Dbar&two[]=a%20two");
});

});


Expand All @@ -80,6 +117,79 @@
expect(params[@"one"]).to.equal(@"a one");
expect(params[@"two"]).to.equal(@"http://www.example.com?foo=bar");
});

it(@"should decode array query parameters into an array preserving order", ^{
NSString *query = @"beers[]=stout&beers[]=ale";
NSDictionary *params = [query DPL_parametersFromQueryString];
expect(params[@"beers"]).notTo.beNil;
expect([params[@"beers"] isKindOfClass:[NSArray class]]).to.beTruthy;
expect([params[@"beers"] count]).to.equal(2);
expect(params[@"beers"][0]).to.contain(@"stout");
expect(params[@"beers"][1]).to.contain(@"ale");
});

it(@"should decode mixed arrays query parameters into appropriate arrays preserving order", ^{
NSString *query = @"beers[]=stout&liquors[]=vodka&beers[]=ale&liquors[]=whiskey";
NSDictionary *params = [query DPL_parametersFromQueryString];
expect(params[@"beers"]).notTo.beNil;
expect(params[@"liquors"]).notTo.beNil;
expect([params[@"beers"] isKindOfClass:[NSArray class]]).to.beTruthy;
expect([params[@"liquors"] isKindOfClass:[NSArray class]]).to.beTruthy;
expect([params[@"beers"] count]).to.equal(2);
expect([params[@"liquors"] count]).to.equal(2);
expect(params[@"beers"][0]).to.contain(@"stout");
expect(params[@"beers"][1]).to.contain(@"ale");
expect(params[@"liquors"][0]).to.contain(@"vodka");
expect(params[@"liquors"][1]).to.contain(@"whiskey");
});

it (@"should decode empty array in case values were not provided", ^{
NSString *query = @"beers[]";
NSDictionary *params = [query DPL_parametersFromQueryString];
expect(params[@"beers"]).notTo.beNil;
expect([params[@"beers"] isKindOfClass:[NSArray class]]).to.beTruthy;
expect([params[@"beers"] count]).to.equal(0);
});

});

describe(@"Array literals", ^{

it (@"should return YES in case there's array literal in the key", ^{
NSString *key = @"beers[]";
expect([key DPL_containsArrayLiteral]).to.beTruthy;
});

it (@"should return NO in case there's no array literal in the key", ^{
NSString *key = @"beers";
expect([key DPL_containsArrayLiteral]).to.beFalsy;
});

it (@"should return NO in case there's array literal in the middle of the key", ^{
NSString *key = @"be[]ers";
expect([key DPL_containsArrayLiteral]).to.beFalsy;
});

it (@"should reply YES in case there's array literal in the key", ^{
NSString *key = @"beers[]";
expect([key DPL_containsArrayLiteral]).to.beTruthy;
});

it (@"should delete array literal at the end of a string", ^{
NSString *key = @"beers[]";
expect([key DPL_stringByRemovingArrayLiteral]).to.equal(@"beers");
});

it (@"should return the same string if there's no array literal", ^{
NSString *key = @"beers";
expect([key DPL_stringByRemovingArrayLiteral]).to.equal(@"beers");
});

it (@"should not delete array literal in the middle of a string", ^{
NSString *key = @"be[]ers";
expect([key DPL_stringByRemovingArrayLiteral]).to.equal(@"be[]ers");
});

});

SpecEnd
Loading