Skip to content

Commit

Permalink
add support for nested values in STPFormEncoder
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-stripe committed Feb 5, 2016
1 parent c7b4b56 commit 6ee981e
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 35 deletions.
186 changes: 154 additions & 32 deletions Stripe/STPFormEncoder.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,176 @@
#import "STPCardParams.h"
#import "STPFormEncodable.h"

FOUNDATION_EXPORT NSString * STPPercentEscapedStringFromString(NSString *string);
FOUNDATION_EXPORT NSString * STPQueryStringFromParameters(NSDictionary *parameters);

@implementation STPFormEncoder

+ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input {
NSArray *parts = [input componentsSeparatedByString:@"_"];
NSMutableString *camelCaseParam = [NSMutableString string];
[parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) {
[camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])];
}];

return [camelCaseParam copy];
}

+ (nonnull NSData *)formEncodedDataForObject:(nonnull NSObject<STPFormEncodable> *)object {
NSMutableArray *parts = [NSMutableArray array];
NSDictionary *dict = @{
[object.class rootObjectName]: [self keyPairDictionaryForObject:object]
};
return [STPQueryStringFromParameters(dict) dataUsingEncoding:NSUTF8StringEncoding];
}

+ (NSDictionary *)keyPairDictionaryForObject:(nonnull NSObject<STPFormEncodable> *)object {
NSMutableDictionary *keyPairs = [NSMutableDictionary dictionary];
[[object.class propertyNamesToFormFieldNamesMapping] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull propertyName, NSString * _Nonnull formFieldName, __unused BOOL * _Nonnull stop) {
NSString *formFieldValue = [[object valueForKey:propertyName] description];
if (formFieldValue) {
[parts addObject:[NSString stringWithFormat:@"%@[%@]=%@", [object.class rootObjectName], formFieldName, [self.class stringByURLEncoding:formFieldValue]]];
id value = [self formEncodableValueForObject:[object valueForKey:propertyName]];
if (value) {
keyPairs[formFieldName] = value;
}
}];
[object.additionalAPIParameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull additionalFieldName, id _Nonnull additionalFieldValue, __unused BOOL * _Nonnull stop) {
if (additionalFieldValue) {
[parts addObject:[NSString stringWithFormat:@"%@[%@]=%@", [object.class rootObjectName], additionalFieldName, [self.class stringByURLEncoding:additionalFieldValue]]];
id value = [self formEncodableValueForObject:additionalFieldValue];
if (value) {
keyPairs[additionalFieldName] = value;
}
}];
return [[parts componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding];
return [keyPairs copy];
}

+ (id)formEncodableValueForObject:(NSObject *)object {
if ([object conformsToProtocol:@protocol(STPFormEncodable)]) {
return [self keyPairDictionaryForObject:(NSObject<STPFormEncodable>*)object];
} else {
return object;
}
}

/* This code is adapted from the code by David DeLong in this StackOverflow post:
http://stackoverflow.com/questions/3423545/objective-c-iphone-percent-encode-a-string . It is protected under the terms of a Creative Commons
license: http://creativecommons.org/licenses/by-sa/3.0/
*/
+ (NSString *)stringByURLEncoding:(NSString *)string {
NSMutableString *output = [NSMutableString string];
const unsigned char *source = (const unsigned char *)[string UTF8String];
NSInteger sourceLen = strlen((const char *)source);
for (int i = 0; i < sourceLen; ++i) {
const unsigned char thisChar = source[i];
if (thisChar == ' ') {
[output appendString:@"+"];
} else if (thisChar == '.' || thisChar == '-' || thisChar == '_' || thisChar == '~' || (thisChar >= 'a' && thisChar <= 'z') ||
(thisChar >= 'A' && thisChar <= 'Z') || (thisChar >= '0' && thisChar <= '9')) {
[output appendFormat:@"%c", thisChar];
} else {
[output appendFormat:@"%%%02X", thisChar];
}
return STPPercentEscapedStringFromString(string);
}

@end


// This code is adapted from https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFURLRequestSerialization.m . The only modifications are to replace the AF namespace with the STP namespace to avoid collisions with apps that are using both Stripe and AFNetworking.
NSString * STPPercentEscapedStringFromString(NSString *string) {
static NSString * const kSTPCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
static NSString * const kSTPCharactersSubDelimitersToEncode = @"!$&'()*+,;=";

NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
[allowedCharacterSet removeCharactersInString:[kSTPCharactersGeneralDelimitersToEncode stringByAppendingString:kSTPCharactersSubDelimitersToEncode]];

// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
// return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];

static NSUInteger const batchSize = 50;

NSUInteger index = 0;
NSMutableString *escaped = @"".mutableCopy;

while (index < string.length) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wgnu"
NSUInteger length = MIN(string.length - index, batchSize);
#pragma GCC diagnostic pop
NSRange range = NSMakeRange(index, length);

// To avoid breaking up character sequences such as 👴🏻👮🏽
range = [string rangeOfComposedCharacterSequencesForRange:range];

NSString *substring = [string substringWithRange:range];
NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
[escaped appendString:encoded];

index += range.length;
}
return output;

return escaped;
}

+ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input {
NSArray *parts = [input componentsSeparatedByString:@"_"];
NSMutableString *camelCaseParam = [NSMutableString string];
[parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) {
[camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])];
}];
#pragma mark -

@interface STPQueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;

- (instancetype)initWithField:(id)field value:(id)value;

- (NSString *)URLEncodedStringValue;
@end

@implementation STPQueryStringPair

- (instancetype)initWithField:(id)field value:(id)value {
self = [super init];
if (!self) {
return nil;
}

return [camelCaseParam copy];
self.field = field;
self.value = value;

return self;
}

- (NSString *)URLEncodedStringValue {
if (!self.value || [self.value isEqual:[NSNull null]]) {
return STPPercentEscapedStringFromString([self.field description]);
} else {
return [NSString stringWithFormat:@"%@=%@", STPPercentEscapedStringFromString([self.field description]), STPPercentEscapedStringFromString([self.value description])];
}
}

@end

#pragma mark -

FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary);
FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value);

NSString * STPQueryStringFromParameters(NSDictionary *parameters) {
NSMutableArray *mutablePairs = [NSMutableArray array];
for (STPQueryStringPair *pair in STPQueryStringPairsFromDictionary(parameters)) {
[mutablePairs addObject:[pair URLEncodedStringValue]];
}

return [mutablePairs componentsJoinedByString:@"&"];
}

NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary) {
return STPQueryStringPairsFromKeyAndValue(nil, dictionary);
}

NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value) {
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];

NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

if ([value isKindOfClass:[NSDictionary class]]) {
NSDictionary *dictionary = value;
// Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries
for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
id nestedValue = dictionary[nestedKey];
if (nestedValue) {
[mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
}
}
} else if ([value isKindOfClass:[NSArray class]]) {
NSArray *array = value;
for (id nestedValue in array) {
[mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
}
} else if ([value isKindOfClass:[NSSet class]]) {
NSSet *set = value;
for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
[mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue(key, obj)];
}
} else {
[mutableQueryStringComponents addObject:[[STPQueryStringPair alloc] initWithField:key value:value]];
}

return mutableQueryStringComponents;
}
2 changes: 1 addition & 1 deletion Tests/Tests/STPBankAccountTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ - (void)testFormEncode {

for (NSString *part in parts) {
NSArray *subparts = [part componentsSeparatedByString:@"="];
NSString *key = subparts[0];
NSString *key = [subparts[0] stringByRemovingPercentEncoding];
NSString *value = subparts[1];

XCTAssertTrue([expectedKeys containsObject:key], @"unexpected key %@", key);
Expand Down
6 changes: 6 additions & 0 deletions Tests/Tests/STPCardFunctionalTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ - (void)testCreateCardToken {
card.expMonth = 6;
card.expYear = 2018;
card.currency = @"usd";
card.addressLine1 = @"123 Fake Street";
card.addressLine2 = @"Apartment 4";
card.addressCity = @"New York";
card.addressState = @"NY";
card.addressCountry = @"USA";
card.addressZip = @"10002";

STPAPIClient *client = [[STPAPIClient alloc] initWithPublishableKey:@"pk_test_5fhKkYDKKNr4Fp6q7Mq9CwJd"];

Expand Down
7 changes: 5 additions & 2 deletions Tests/Tests/STPCardTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ - (void)testInitializingCardWithAttributeDictionary {
- (void)testFormEncode {
NSDictionary *attributes = [self completeAttributeDictionary];
STPCard *cardWithAttributes = [STPCard decodedObjectFromAPIResponse:attributes];
cardWithAttributes.additionalAPIParameters = @{@"foo": @"bar"};
cardWithAttributes.additionalAPIParameters = @{@"foo": @"bar", @"nested": @{@"nested_key": @"nested_value"}};

NSData *encoded = [STPFormEncoder formEncodedDataForObject:cardWithAttributes];
NSString *formData = [[NSString alloc] initWithData:encoded encoding:NSUTF8StringEncoding];
Expand All @@ -103,10 +103,12 @@ - (void)testFormEncode {
@"card[address_country]",
@"card[currency]",
@"card[foo]",
@"card[nested][nested_key]",
nil];

NSMutableArray *values = [[attributes allValues] mutableCopy];
[values addObject:@"bar"];
[values addObject:@"nested_value"];
NSMutableArray *encodedValues = [NSMutableArray array];
for (NSString *value in values) {
[encodedValues addObject:[STPFormEncoder stringByURLEncoding:value]];
Expand All @@ -115,8 +117,9 @@ - (void)testFormEncode {
NSSet *expectedValues = [NSSet setWithArray:encodedValues];
for (NSString *part in parts) {
NSArray *subparts = [part componentsSeparatedByString:@"="];
NSString *key = subparts[0];
NSString *key = [subparts[0] stringByRemovingPercentEncoding];
NSString *value = subparts[1];


XCTAssertTrue([expectedKeys containsObject:key], @"unexpected key %@", key);
XCTAssertTrue([expectedValues containsObject:value], @"unexpected value %@", value);
Expand Down

0 comments on commit 6ee981e

Please sign in to comment.