From c2d4d3a9cf7be4d86efe6549bbdecad3d72b7088 Mon Sep 17 00:00:00 2001 From: Michael Lehenbauer Date: Thu, 12 Apr 2018 13:50:02 -0700 Subject: Small cleanup to UserDataConverter / FieldValue sentinel code to make adding more FieldValue sentinels easier. (#1077) Port of https://github.com/firebase/firebase-js-sdk/commit/b14678002bf6e8cc9ebd7561efbafe29315ceb8e and https://github.com/firebase/firebase-js-sdk/commit/b14678002bf6e8cc9ebd7561efbafe29315ceb8e. --- Firestore/Source/API/FIRFieldValue+Internal.h | 8 ++ Firestore/Source/API/FIRFieldValue.mm | 8 ++ Firestore/Source/API/FSTUserDataConverter.mm | 125 +++++++++++++++++--------- 3 files changed, 101 insertions(+), 40 deletions(-) (limited to 'Firestore/Source') diff --git a/Firestore/Source/API/FIRFieldValue+Internal.h b/Firestore/Source/API/FIRFieldValue+Internal.h index 1b4a99c..2eb927e 100644 --- a/Firestore/Source/API/FIRFieldValue+Internal.h +++ b/Firestore/Source/API/FIRFieldValue+Internal.h @@ -18,6 +18,14 @@ NS_ASSUME_NONNULL_BEGIN +@interface FIRFieldValue (Internal) +/** + * The method name (e.g. "FieldValue.delete()") that was used to create this FIRFieldValue + * instance, for use in error messages, etc. + */ +@property(nonatomic, strong, readonly) NSString *methodName; +@end + /** * FIRFieldValue class for field deletes. Exposed internally so code can do isKindOfClass checks on * it. diff --git a/Firestore/Source/API/FIRFieldValue.mm b/Firestore/Source/API/FIRFieldValue.mm index 7ae4fb0..0d0a9c4 100644 --- a/Firestore/Source/API/FIRFieldValue.mm +++ b/Firestore/Source/API/FIRFieldValue.mm @@ -46,6 +46,10 @@ NS_ASSUME_NONNULL_BEGIN return sharedInstance; } +- (NSString *)methodName { + return @"FieldValue.delete()"; +} + @end #pragma mark - FSTServerTimestampFieldValue @@ -72,6 +76,10 @@ NS_ASSUME_NONNULL_BEGIN return sharedInstance; } +- (NSString *)methodName { + return @"FieldValue.serverTimestamp()"; +} + @end #pragma mark - FIRFieldValue diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm index 2eee878..6320e83 100644 --- a/Firestore/Source/API/FSTUserDataConverter.mm +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -507,64 +507,110 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { */ - (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context { input = self.preConverter(input); - if ([input isKindOfClass:[NSArray class]]) { - // TODO(b/34871131): Include the path containing the array in the error message. - if (context.isArrayElement) { - FSTThrowInvalidArgument(@"Nested arrays are not supported"); - } - NSArray *array = input; - NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; - [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { - FSTFieldValue *_Nullable parsedEntry = - [self parseData:entry context:[context contextForArrayIndex:idx]]; - if (!parsedEntry) { - // Just include nulls in the array for fields being replaced with a sentinel. - parsedEntry = [FSTNullValue nullValue]; - } - [result addObject:parsedEntry]; - }]; + if ([input isKindOfClass:[NSDictionary class]]) { + return [self parseDictionary:(NSDictionary *)input context:context]; + } else { // If context.path is nil we are already inside an array and we don't support field mask paths // more granular than the top-level array. if (context.path) { [context appendToFieldMaskWithFieldPath:*context.path]; } - return [[FSTArrayValue alloc] initWithValueNoCopy:result]; - } else if ([input isKindOfClass:[NSDictionary class]]) { - NSDictionary *dict = input; - NSMutableDictionary *result = - [NSMutableDictionary dictionaryWithCapacity:dict.count]; - [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - FSTFieldValue *_Nullable parsedValue = - [self parseData:value context:[context contextForField:key]]; - if (parsedValue) { - result[key] = parsedValue; + if ([input isKindOfClass:[NSArray class]]) { + // TODO(b/34871131): Include the path containing the array in the error message. + if (context.isArrayElement) { + FSTThrowInvalidArgument(@"Nested arrays are not supported"); } - }]; - return [[FSTObjectValue alloc] initWithDictionary:result]; + return [self parseArray:(NSArray *)input context:context]; + } else if ([input isKindOfClass:[FIRFieldValue class]]) { + // parseSentinelFieldValue may add an FSTFieldTransform, but we return nil since nothing + // should be included in the actual parsed data. + [self parseSentinelFieldValue:(FIRFieldValue *)input context:context]; + return nil; + } else { + return [self parseScalarValue:input context:context]; + } + } +} - } else { - // If context.path is null, we are inside an array and we should have already added the root of - // the array to the field mask. - if (context.path) { - [context appendToFieldMaskWithFieldPath:*context.path]; +- (FSTFieldValue *)parseDictionary:(NSDictionary *)dict context:(FSTParseContext *)context { + NSMutableDictionary *result = + [NSMutableDictionary dictionaryWithCapacity:dict.count]; + [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForField:key]]; + if (parsedValue) { + result[key] = parsedValue; + } + }]; + return [[FSTObjectValue alloc] initWithDictionary:result]; +} + +- (FSTFieldValue *)parseArray:(NSArray *)array context:(FSTParseContext *)context { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; + [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { + FSTFieldValue *_Nullable parsedEntry = + [self parseData:entry context:[context contextForArrayIndex:idx]]; + if (!parsedEntry) { + // Just include nulls in the array for fields being replaced with a sentinel. + parsedEntry = [FSTNullValue nullValue]; } - return [self parseScalarValue:input context:context]; + [result addObject:parsedEntry]; + }]; + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; +} + +/** + * "Parses" the provided FIRFieldValue, adding any necessary transforms to + * context.fieldTransforms. + */ +- (void)parseSentinelFieldValue:(FIRFieldValue *)fieldValue context:(FSTParseContext *)context { + // Sentinels are only supported with writes, and not within arrays. + if (![context isWrite]) { + FSTThrowInvalidArgument(@"%@ can only be used with updateData() and setData()%@", + fieldValue.methodName, [context fieldDescription]); + } + if (!context.path) { + FSTThrowInvalidArgument(@"%@ is not currently supported inside arrays", fieldValue.methodName); + } + + if ([fieldValue isKindOfClass:[FSTDeleteFieldValue class]]) { + if (context.dataSource == FSTUserDataSourceMergeSet) { + // No transform to add for a delete, so we do nothing. + } else if (context.dataSource == FSTUserDataSourceUpdate) { + FSTAssert(context.path->size() > 0, + @"FieldValue.delete() at the top level should have already been handled."); + FSTThrowInvalidArgument( + @"FieldValue.delete() can only appear at the top level of your " + "update data%@", + [context fieldDescription]); + } else { + // We shouldn't encounter delete sentinels for queries or non-merge setData calls. + FSTThrowInvalidArgument( + @"FieldValue.delete() can only be used with updateData() and setData() with " + @"SetOptions.merge()%@", + [context fieldDescription]); + } + } else if ([fieldValue isKindOfClass:[FSTServerTimestampFieldValue class]]) { + [context appendToFieldTransformsWithFieldPath:*context.path + transformOperation:absl::make_unique( + ServerTimestampTransform::Get())]; + } else { + FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([fieldValue class])); } } /** - * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray). + * Helper to parse a scalar value (i.e. not an NSDictionary, NSArray, or FIRFieldValue). * * Note that it handles all NSNumber values that are encodable as int64_t or doubles * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though * any value outside what is representable by int64_t (a signed 64-bit value) will throw an * exception. * - * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be - * included in the resulting parsed data. + * @return The parsed value. */ -- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { +- (FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { if (!input || [input isMemberOfClass:[NSNull class]]) { return [FSTNullValue nullValue]; @@ -671,8 +717,7 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { FSTAssert(context.path->size() > 0, @"FieldValue.delete() at the top level should have already been handled."); FSTThrowInvalidArgument( - @"FieldValue.delete() can only appear at the top level of your " - "update data%@", + @"FieldValue.delete() can only appear at the top level of your update data%@", [context fieldDescription]); } else { // We shouldn't encounter delete sentinels for queries or non-merge setData calls. -- cgit v1.2.3