Source/TNLParameterCollection.m (493 lines of code) (raw):

// // TNLParameterCollection.m // TwitterNetworkLayer // // Created on 10/24/14. // Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" #import "TNLParameterCollection.h" #import "TNLURLCoding.h" NS_ASSUME_NONNULL_BEGIN NSString * const kParametersCodingKey = @"parameters"; typedef NSString *(^TNLParameterCollectionUpdateKeysAndValuesIterativeKeyBlock)(NSString *key, id obj); TNL_OBJC_DIRECT_MEMBERS @interface TNLParameterCollection () @property (nonatomic, readonly) NSDictionary *parameters; - (instancetype)initWithDirectlyAssignedDictionary:(nullable NSDictionary<NSString *, id> *)dict; @end TNL_OBJC_DIRECT_MEMBERS @implementation TNLParameterCollection { @protected NSDictionary<NSString *, id> *_parameters; } @synthesize parameters = _parameters; - (instancetype)init { return [super init]; } - (instancetype)initWithDirectlyAssignedDictionary:(nullable NSDictionary<NSString *, id> *)dict { if (self = [self init]) { _parameters = dict; // don't copy! } return self; } - (instancetype)initWithURLEncodedString:(nullable NSString *)params { return [self initWithURLEncodedString:params options:TNLURLDecodingOptionsNone]; } - (instancetype)initWithURLEncodedString:(nullable NSString *)params options:(TNLURLDecodingOptions)options { TNLMutableParameterCollection *mCollection = [[TNLMutableParameterCollection alloc] initWithURLEncodedString:params options:options]; return [self initWithParameterCollection:mCollection]; } - (instancetype)initWithDictionary:(nullable NSDictionary<NSString *, id> *)dictionary { TNLMutableParameterCollection *mCollection = [[TNLMutableParameterCollection alloc] initWithDictionary:dictionary]; return [self initWithParameterCollection:mCollection]; } - (instancetype)initWithURL:(nullable NSURL *)URL parsingParameterTypes:(TNLParameterTypes)types { return [self initWithURL:URL parsingParameterTypes:types options:TNLURLDecodingOptionsNone]; } - (instancetype)initWithURL:(nullable NSURL *)URL parsingParameterTypes:(TNLParameterTypes)types options:(TNLURLDecodingOptions)options { TNLMutableParameterCollection *mCollection = [[TNLMutableParameterCollection alloc] initWithURL:URL parsingParameterTypes:types options:options]; return [self initWithParameterCollection:mCollection]; } - (instancetype)initWithParameterCollection:(nullable TNLParameterCollection *)otherCollection { if (self = [super init]) { _parameters = [otherCollection.parameters copy]; } return self; } #pragma mark NSCopying - (id)copyWithZone:(nullable NSZone *)zone { return self; } #pragma mark NSMutableCopying - (id)mutableCopyWithZone:(nullable NSZone *)zone { return [[TNLMutableParameterCollection alloc] initWithParameterCollection:self]; } #pragma mark Count - (NSUInteger)count { return _parameters.count; } #pragma mark NSSecureCoding + (BOOL)supportsSecureCoding { return YES; } - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { NSDictionary *d = [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:kParametersCodingKey]; return [self initWithDirectlyAssignedDictionary:d]; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_parameters forKey:kParametersCodingKey]; } #pragma mark NSFastEnumeration - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained __nullable [__nonnull])buffer count:(NSUInteger)len { return [_parameters countByEnumeratingWithState:state objects:buffer count:len]; } #pragma mark Keyed Subscripting - (nullable id)objectForKeyedSubscript:(NSString *)key { return [self parameterValueForKey:key]; } #pragma mark Access - (nullable id)parameterValueForKey:(NSString *)key { return _parameters[key]; } #pragma mark Helpers - (NSArray *)allKeys { return _parameters.allKeys; } - (void)enumerateParameterKeysAndValuesUsingBlock:(void (^)(NSString *, id, BOOL *))block { [_parameters enumerateKeysAndObjectsUsingBlock:block]; } - (void)enumerateParameterKeysAndValuesWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(NSString *, id, BOOL *))block { [_parameters enumerateKeysAndObjectsWithOptions:opts usingBlock:block]; } #pragma mark URL Params - (NSString *)URLEncodedStringValue { return [self URLEncodedStringValueWithOptions:TNLURLEncodingOptionsNone]; } - (NSString *)stableURLEncodedStringValue { return [self URLEncodedStringValueWithOptions:TNLURLEncodingOptionStableOrder]; } - (NSDictionary<NSString *, id> *)underlyingDictionaryValue { return [_parameters copy]; } - (NSDictionary<NSString *, id> *)encodableDictionaryValue { const TNLURLEncodableDictionaryOptions options = TNLURLEncodableDictionaryOptionReplaceArraysWithArraysOfEncodableStrings | TNLURLEncodableDictionaryOptionReplaceDictionariesWithDictionariesOfEncodableStrings; return [self encodableDictionaryValueWithOptions:options]; } - (NSDictionary<NSString *, id> *)encodableDictionaryValueWithOptions:(TNLURLEncodableDictionaryOptions)options { return TNLURLEncodableDictionary(_parameters, options); } + (NSString *)stringByCombiningParameterString:(nullable TNLParameterCollection *)parameterStringCollection query:(nullable TNLParameterCollection *)queryCollection fragment:(nullable TNLParameterCollection *)fragmentCollection options:(TNLURLEncodingOptions)options { NSString *parameterString; NSString *query; NSString *fragment; if (parameterStringCollection) { parameterString = [parameterStringCollection URLEncodedStringValueWithOptions:options]; TNLAssert(parameterString); } if (queryCollection) { query = [queryCollection URLEncodedStringValueWithOptions:options]; TNLAssert(query); } if (fragmentCollection) { fragment = [fragmentCollection URLEncodedStringValueWithOptions:options]; TNLAssert(fragment); } NSMutableString *string = [NSMutableString string]; if (parameterString.length > 0) { [string appendString:@";"]; [string appendString:parameterString]; } if (query.length > 0) { [string appendString:@"?"]; [string appendString:query]; } if (fragment.length > 0) { [string appendString:@"#"]; [string appendString:fragment]; } return string; } - (NSString *)URLEncodedStringValueWithOptions:(TNLURLEncodingOptions)options { return TNLURLEncodeDictionary(_parameters, options); } #pragma mark Description - (NSString *)description { return [NSString stringWithFormat:@"<%@ %p: %@='%@'>", NSStringFromClass([self class]), self, NSStringFromSelector(@selector(URLEncodedStringValue)), [self URLEncodedStringValueWithOptions:TNLURLEncodingOptionsNone | TNLURLEncodingOptionTreatUnsupportedValuesAsEmpty]]; } #pragma mark Equivalence - (NSUInteger)hash { return _parameters.hash; } - (BOOL)isEqual:(id)object { if ([super isEqual:object]) { return YES; } if ([object isKindOfClass:[TNLParameterCollection class]]) { return [_parameters isEqualToDictionary:((TNLParameterCollection *)object)->_parameters]; } return NO; } @end TNL_OBJC_DIRECT_MEMBERS @implementation TNLMutableParameterCollection - (instancetype)init { return [self initWithCapacity:0]; } - (instancetype)initWithCapacity:(NSUInteger)capacity { if (self = [super init]) { _parameters = capacity ? [[NSMutableDictionary alloc] initWithCapacity:capacity] : [[NSMutableDictionary alloc] init]; } return self; } - (instancetype)initWithURLEncodedString:(nullable NSString *)params options:(TNLURLDecodingOptions)options { if (self = [self init]) { [self addParametersWithURLEncodedString:params options:options]; } return self; } - (instancetype)initWithDictionary:(nullable NSDictionary<NSString *, id> *)dictionary { if (self = [self initWithCapacity:dictionary.count]) { [self _addParametersDirectly:dictionary combineRepeatingKeys:NO]; } return self; } - (instancetype)initWithURL:(nullable NSURL *)URL parsingParameterTypes:(TNLParameterTypes)types options:(TNLURLDecodingOptions)options { if (self = [self init]) { [self addParametersFromURL:URL parsingParameterTypes:types options:options]; } return self; } - (instancetype)initWithParameterCollection:(nullable TNLParameterCollection *)otherCollection { if (self = [super init]) { _parameters = [[NSMutableDictionary alloc] initWithDictionary:otherCollection.parameters]; } return self; } - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { NSDictionary *d = [aDecoder decodeObjectOfClass:[NSMutableDictionary class] forKey:kParametersCodingKey]; if (!d) { d = [[NSMutableDictionary alloc] init]; } return [self initWithDirectlyAssignedDictionary:d]; } - (id)copyWithZone:(nullable NSZone *)zone { return [[TNLParameterCollection alloc] initWithParameterCollection:self]; } - (void)addParametersWithURLEncodedString:(nullable NSString *)params { [self addParametersWithURLEncodedString:params options:TNLURLDecodingOptionsNone]; } - (void)addParametersWithURLEncodedString:(nullable NSString *)params options:(TNLURLDecodingOptions)options { const BOOL combine = TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLDecodingOptionCombineRepeatingKeysIntoArray); [self _addParametersDirectly:TNLURLDecodeDictionary(params, options) combineRepeatingKeys:combine]; } - (void)addParametersFromURL:(nullable NSURL *)URL parsingParameterTypes:(TNLParameterTypes)types { [self addParametersFromURL:URL parsingParameterTypes:types options:TNLURLDecodingOptionsNone]; } - (void)addParametersFromURL:(nullable NSURL *)URL parsingParameterTypes:(TNLParameterTypes)types options:(TNLURLDecodingOptions)options { if (TNL_BITMASK_HAS_SUBSET_FLAGS(types, TNLParameterTypeURLParameterString)) { NSString *parameterString; if (tnl_available_ios_13) { // parameter string is no longer considered valid according to Apple, which is wise // ... but we'll still support parsing it NSString *path = URL.path; if (path) { NSRange range = [path rangeOfString:@";"]; if (range.location != NSNotFound) { parameterString = [path substringFromIndex:range.location + 1]; } } #if !TARGET_OS_MACCATALYST } else { parameterString = URL.parameterString; #endif } [self addParametersWithURLEncodedString:parameterString options:options]; } if (TNL_BITMASK_HAS_SUBSET_FLAGS(types, TNLParameterTypeURLQuery)) { [self addParametersWithURLEncodedString:URL.query options:options]; } if (TNL_BITMASK_HAS_SUBSET_FLAGS(types, TNLParameterTypeURLFragment)) { [self addParametersWithURLEncodedString:URL.fragment options:options]; } } - (void)addParametersDirectlyFromDictionary:(nullable NSDictionary<NSString *,id> *)dictionary combineRepeatingKeys:(BOOL)combine { [self _addParametersDirectly:dictionary combineRepeatingKeys:combine]; } - (void)addParametersFromDictionary:(nullable NSDictionary<NSString *, id> *)dictionary withFormattingMode:(TNLParameterCollectionAddParametersFromDictionaryMode)mode combineRepeatingKeys:(BOOL)combine forKey:(NSString *)key { if (!dictionary) { return; } switch (mode) { case TNLParameterCollectionAddParametersFromDictionaryModeUseKeysDirectly: [self _addParametersDirectly:dictionary combineRepeatingKeys:combine]; return; case TNLParameterCollectionAddParametersFromDictionaryModeURLEncoded: [self _addParametersUsingURLEncoding:dictionary combineRepeatingKeys:combine dictionaryKey:key]; return; case TNLParameterCollectionAddParametersFromDictionaryModeJSONEncoded: [self _addParametersUsingJSONEncoding:dictionary combineRepeatingKeys:combine dictionaryKey:key]; return; case TNLParameterCollectionAddParametersFromDictionaryModeDotSyntaxOnProvidedKey: [self _addParametersUsingDotSyntax:dictionary combineRepeatingKeys:combine dictionaryKey:key]; return; } TNLAssertNever(); } - (void)_setObject:(id)obj forKey:(NSString *)key combineRepeatingKeys:(BOOL)combine { if (combine) { id oldValue = _parameters[key]; if ([oldValue isKindOfClass:[NSString class]]) { obj = @[ oldValue, obj ]; } else if ([oldValue isKindOfClass:[NSArray class]]) { oldValue = [oldValue mutableCopy]; [(NSMutableArray *)oldValue addObject:obj]; obj = oldValue; } } self[key] = obj; } - (void)_updateWithDictionary:(nullable NSDictionary<NSString *, id> *)dictionary combineRepeatingKeys:(BOOL)combine iterativeKeyBlock:(TNLParameterCollectionUpdateKeysAndValuesIterativeKeyBlock)iterativeKeyBlock { [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { if (![key isKindOfClass:[NSString class]]) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"keys must be NSStrings for TNLParameterCollection" userInfo:@{ @"key" : key, @"value" : obj }]; } if ([obj respondsToSelector:@selector(copyWithZone:)]) { obj = [obj copy]; TNLAssert(obj != nil); } NSString *newKey = iterativeKeyBlock(key, obj); TNLAssert(newKey != nil); [self _setObject:obj forKey:newKey combineRepeatingKeys:combine]; }]; } - (void)_addParametersDirectly:(nullable NSDictionary<NSString *, id> *)dictionary combineRepeatingKeys:(BOOL)combine { [self _updateWithDictionary:dictionary combineRepeatingKeys:combine iterativeKeyBlock:^NSString *(NSString *key, id obj) { return key; }]; } - (void)_addParametersUsingURLEncoding:(nullable NSDictionary<NSString *, id> *)dictionary combineRepeatingKeys:(BOOL)combine dictionaryKey:(NSString *)key { dictionary = TNLURLEncodableDictionary(dictionary, TNLURLEncodableDictionaryOptionReplaceArraysWithArraysOfEncodableStrings | TNLURLEncodableDictionaryOptionReplaceDictionariesWithDictionariesOfEncodableStrings); NSString *obj = TNLURLEncodeDictionary(dictionary, TNLURLEncodingOptionStableOrder); [self _setObject:obj forKey:key combineRepeatingKeys:combine]; } - (void)_addParametersUsingJSONEncoding:(nullable NSDictionary<NSString *, id> *)dictionary combineRepeatingKeys:(BOOL)combine dictionaryKey:(NSString *)key { dictionary = TNLURLEncodableDictionary(dictionary, TNLURLEncodableDictionaryOptionReplaceArraysWithArraysOfEncodableStrings | TNLURLEncodableDictionaryOptionReplaceDictionariesWithDictionariesOfEncodableStrings); NSJSONWritingOptions options = 0; if (tnl_available_ios_11) { options = NSJSONWritingSortedKeys; } NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:options error:NULL]; NSString *json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; [self _setObject:json forKey:key combineRepeatingKeys:combine]; } - (void)_addParametersUsingDotSyntax:(nullable NSDictionary<NSString *, id> *)dictionary combineRepeatingKeys:(BOOL)combine dictionaryKey:(NSString *)key { [self _updateWithDictionary:dictionary combineRepeatingKeys:combine iterativeKeyBlock:^NSString *(NSString * _Nonnull iterKey, id _Nonnull obj) { return [NSString stringWithFormat:@"%@.%@", key, iterKey]; }]; } - (void)addParametersFromParameterCollection:(nullable TNLParameterCollection *)params combineRepeatingKeys:(BOOL)combine { [self _addParametersDirectly:params.underlyingDictionaryValue combineRepeatingKeys:combine]; } - (void)addParametersFromParameterCollection:(nullable TNLParameterCollection *)params { [self addParametersFromParameterCollection:params combineRepeatingKeys:NO]; } - (void)setObject:(nullable id)obj forKeyedSubscript:(NSString *)key { [self setParameterValue:obj forKey:key]; } - (void)setParameterValue:(nullable id)obj forKey:(NSString *)key { if (!obj) { [((NSMutableDictionary *)_parameters) removeObjectForKey:key]; return; } if (![key isKindOfClass:[NSString class]]) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"keys must be NSStrings for TNLParameterCollection" userInfo:@{ @"key" : key ?: [NSNull null], @"value" : obj }]; } else if (key.length == 0) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"keys cannot be empty strings for TNLParameterCollection" userInfo:@{ @"key" : key, @"value" : obj }]; } if ([obj respondsToSelector:@selector(copyWithZone:)]) { obj = [obj copy]; TNLAssert(obj != nil); } ((NSMutableDictionary *)_parameters)[key] = obj; } - (void)removeAllParameters { [(NSMutableDictionary *)_parameters removeAllObjects]; } @end @implementation NSURL (Parameters) - (TNLParameterCollection *)tnl_parameterStringCollection { return [self tnl_parameterStringCollectionWithOptions:TNLURLDecodingOptionsNone]; } - (TNLParameterCollection *)tnl_parameterStringCollectionWithOptions:(TNLURLDecodingOptions)options { return [[TNLParameterCollection alloc] initWithURL:self parsingParameterTypes:TNLParameterTypeURLParameterString options:options]; } - (TNLParameterCollection *)tnl_queryCollection { return [self tnl_queryCollectionWithOptions:TNLURLDecodingOptionsNone]; } - (TNLParameterCollection *)tnl_queryCollectionWithOptions:(TNLURLDecodingOptions)options { return [[TNLParameterCollection alloc] initWithURL:self parsingParameterTypes:TNLParameterTypeURLQuery options:options]; } - (TNLParameterCollection *)tnl_fragmentCollection { return [self tnl_fragmentCollectionWithOptions:TNLURLDecodingOptionsNone]; } - (TNLParameterCollection *)tnl_fragmentCollectionWithOptions:(TNLURLDecodingOptions)options { return [[TNLParameterCollection alloc] initWithURL:self parsingParameterTypes:TNLParameterTypeURLFragment options:options]; } @end NS_ASSUME_NONNULL_END