Skip to content

Small cleanup to UserDataConverter / FieldValue sentinel code to make adding more FieldValue sentinels easier. #1077

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

Merged
merged 1 commit into from
Apr 12, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ - (void)testSetsWithFieldValueDeleteFail {
[self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]}
toFailWithReason:
@"FieldValue.delete() can only be used with updateData() and setData() with "
@"SetOptions.merge()."];
@"SetOptions.merge() (found in field foo)"];
}

- (void)testUpdatesWithNestedFieldValueDeleteFail {
Expand Down
8 changes: 8 additions & 0 deletions Firestore/Source/API/FIRFieldValue+Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions Firestore/Source/API/FIRFieldValue.mm
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ + (instancetype)deleteFieldValue {
return sharedInstance;
}

- (NSString *)methodName {
return @"FieldValue.delete()";
}

@end

#pragma mark - FSTServerTimestampFieldValue
Expand All @@ -72,6 +76,10 @@ + (instancetype)serverTimestampFieldValue {
return sharedInstance;
}

- (NSString *)methodName {
return @"FieldValue.serverTimestamp()";
}

@end

#pragma mark - FIRFieldValue
Expand Down
125 changes: 85 additions & 40 deletions Firestore/Source/API/FSTUserDataConverter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -507,64 +507,110 @@ - (FSTFieldValue *)parsedQueryValue:(id)input {
*/
- (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<FSTFieldValue *> *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<NSString *, FSTFieldValue *> *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<NSString *, FSTFieldValue *> *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<FSTFieldValue *> *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>(
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];

Expand Down Expand Up @@ -671,8 +717,7 @@ - (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTPars
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.
Expand Down