GBA003/Pods/GoogleAPIClientForREST/Source/Objects/GTLRObject.m
2024-05-30 10:22:15 +08:00

769 lines
26 KiB
Objective-C

/* Copyright (c) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if !__has_feature(objc_arc)
#error "This file needs to be compiled with ARC enabled."
#endif
#include <objc/runtime.h>
#import "GTLRObject.h"
#import "GTLRRuntimeCommon.h"
#import "GTLRUtilities.h"
static NSString *const kUserDataPropertyKey = @"_userData";
static NSString *const kGTLRObjectJSONCoderKey = @"json";
static NSMutableDictionary *DeepMutableCopyOfJSONDictionary(NSDictionary *initialJSON);
@interface GTLRObject () <GTLRRuntimeCommon>
@property(nonatomic, strong) id<GTLRObjectClassResolver>objectClassResolver;
@end
@implementation GTLRObject {
// Any complex object hung off this object goes into the cache so the
// next fetch will get the same object back instead of having to recreate
// it.
NSMutableDictionary *_childCache;
}
@synthesize JSON = _json,
objectClassResolver = _objectClassResolver,
userProperties = _userProperties;
+ (instancetype)object {
return [[self alloc] init];
}
+ (instancetype)objectWithJSON:(NSDictionary *)dict {
GTLRObject *obj = [self object];
obj->_json = DeepMutableCopyOfJSONDictionary(dict);
return obj;
}
+ (instancetype)objectWithJSON:(nullable NSDictionary *)dict
objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver {
GTLRObject *obj = [self objectWithJSON:dict];
obj->_objectClassResolver = objectClassResolver;
return obj;
}
+ (NSDictionary<NSString *, NSString *> *)propertyToJSONKeyMap {
return nil;
}
+ (NSDictionary<NSString *, Class> *)arrayPropertyToClassMap {
return nil;
}
+ (Class)classForAdditionalProperties {
return Nil;
}
+ (BOOL)isKindValidForClassRegistry {
return YES;
}
- (BOOL)isEqual:(GTLRObject *)other {
if (self == other) return YES;
if (other == nil) return NO;
// The objects should be the same class, or one should be a subclass of the
// other's class
if (![other isKindOfClass:[self class]]
&& ![self isKindOfClass:[other class]]) return NO;
// What we're not comparing here:
// properties
return GTLR_AreEqualOrBothNil(_json, [other JSON]);
}
// By definition, for two objects to potentially be considered equal,
// they must have the same hash value. The hash is mostly ignored,
// but removeObjectsInArray: in Leopard does seem to check the hash,
// and NSObject's default hash method just returns the instance pointer.
// We'll define hash here for all of our GTLRObjects.
- (NSUInteger)hash {
return (NSUInteger) (__bridge void *) [GTLRObject class];
}
- (id)copyWithZone:(NSZone *)zone {
GTLRObject *newObject = [[[self class] allocWithZone:zone] init];
newObject.JSON = DeepMutableCopyOfJSONDictionary(self.JSON);
newObject.objectClassResolver = self.objectClassResolver;
// What we're not copying:
// userProperties
return newObject;
}
- (NSString *)descriptionWithLocale:(id)locale {
return self.description;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (self) {
// NSDictionary/NSArray seem to allow strings and numbers with secure coding
// just fine, but to allow sub arrays or dictionaries (or an null) the
// classes have to be explicitly listed to decode correctly.
NSSet *expectedClasses =
[NSSet setWithObjects:
[NSMutableDictionary class], [NSMutableArray class], [NSNull class], nil];
_json = [decoder decodeObjectOfClasses:expectedClasses
forKey:kGTLRObjectJSONCoderKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:_json forKey:kGTLRObjectJSONCoderKey];
}
#pragma mark JSON values
- (void)setJSONValue:(id)obj forKey:(NSString *)key {
NSMutableDictionary *dict = self.JSON;
if (dict == nil && obj != nil) {
dict = [NSMutableDictionary dictionaryWithCapacity:1];
self.JSON = dict;
}
[dict setValue:obj forKey:key];
}
- (id)JSONValueForKey:(NSString *)key {
id obj = [self.JSON objectForKey:key];
return obj;
}
- (NSString *)JSONString {
NSError *error;
NSDictionary *json = self.JSON;
if (json) {
NSData *data = [NSJSONSerialization dataWithJSONObject:json
options:NSJSONWritingPrettyPrinted
error:&error];
GTLR_DEBUG_ASSERT(data != nil, @"JSONString generate failed: %@\n JSON: %@", error, json);
if (data) {
NSString *jsonStr = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
if (jsonStr) return jsonStr;
}
}
return @"";
}
- (NSArray<NSString *> *)additionalJSONKeys {
NSArray *knownKeys = [[self class] allKnownKeys];
NSMutableArray *result;
NSArray *allKeys = _json.allKeys;
if (allKeys) {
result = [NSMutableArray arrayWithArray:allKeys];
[result removeObjectsInArray:knownKeys];
// Return nil instead of an empty array.
if (result.count == 0) {
result = nil;
}
}
return result;
}
#pragma mark Partial - Fields
- (NSString *)fieldsDescription {
NSString *str = [GTLRObject fieldsDescriptionForJSON:self.JSON];
return str;
}
+ (NSString *)fieldsDescriptionForJSON:(NSDictionary *)targetJSON {
// Internal routine: recursively generate a string field description
// by joining elements
NSArray *array = [self fieldsElementsForJSON:targetJSON];
NSString *str = [array componentsJoinedByString:@","];
return str;
}
+ (NSArray *)fieldsElementsForJSON:(NSDictionary *)targetJSON {
// Internal routine: recursively generate an array of field description
// element strings
NSMutableArray *resultFields = [NSMutableArray array];
// Sorting the dictionary keys gives us deterministic results when iterating
NSArray *sortedKeys = [targetJSON.allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
for (NSString *key in sortedKeys) {
// We'll build a comma-separated list of fields
id value = [targetJSON objectForKey:key];
if ([value isKindOfClass:[NSString class]]
|| [value isKindOfClass:[NSNumber class]]) {
// Basic type (string, number), so the key is what we want
[resultFields addObject:key];
} else if ([value isKindOfClass:[NSDictionary class]]) {
// Object (dictionary): "parent/child1,parent/child2,parent/child3"
NSArray *subElements = [self fieldsElementsForJSON:value];
for (NSString *subElem in subElements) {
NSString *prepended = [NSString stringWithFormat:@"%@/%@",
key, subElem];
[resultFields addObject:prepended];
}
} else if ([value isKindOfClass:[NSArray class]]) {
// Array; we'll generate from the first array entry:
// "parent(child1,child2,child3)"
//
// Open question: should this instead create the union of elements for
// all items in the array, rather than just get fields from the first
// array object?
if (((NSArray *)value).count > 0) {
id firstObj = [value objectAtIndex:0];
if ([firstObj isKindOfClass:[NSDictionary class]]) {
// An array of objects
NSString *contentsStr = [self fieldsDescriptionForJSON:firstObj];
NSString *encapsulated = [NSString stringWithFormat:@"%@(%@)",
key, contentsStr];
[resultFields addObject:encapsulated];
} else {
// An array of some basic type, or of arrays
[resultFields addObject:key];
}
}
} else {
GTLR_ASSERT(0, @"GTLRObject unknown field element for %@ (%@)",
key, NSStringFromClass([value class]));
}
}
return resultFields;
}
#pragma mark Partial - Patch
- (id)patchObjectFromOriginal:(GTLRObject *)original {
GTLRObject *resultObj;
NSMutableDictionary *resultJSON = [GTLRObject patchDictionaryForJSON:self.JSON
fromOriginalJSON:original.JSON];
if (resultJSON.count > 0) {
// Avoid an extra copy by assigning the JSON directly rather than using +objectWithJSON:
resultObj = [[self class] object];
resultObj.JSON = resultJSON;
} else {
// Client apps should not attempt to patch with an object containing
// empty JSON
resultObj = nil;
}
return resultObj;
}
+ (NSMutableDictionary *)patchDictionaryForJSON:(NSDictionary *)newJSON
fromOriginalJSON:(NSDictionary *)originalJSON {
// Internal recursive routine to create an object suitable for
// our patch semantics
NSMutableDictionary *resultJSON = [NSMutableDictionary dictionary];
// Iterate through keys present in the old object
NSArray *originalKeys = originalJSON.allKeys;
for (NSString *key in originalKeys) {
id originalValue = [originalJSON objectForKey:key];
id newValue = [newJSON valueForKey:key];
if (newValue == nil) {
// There is no new value for this key, so set the value to NSNull
[resultJSON setValue:[NSNull null] forKey:key];
} else if (!GTLR_AreEqualOrBothNil(originalValue, newValue)) {
// The values for this key differ
if ([originalValue isKindOfClass:[NSDictionary class]]
&& [newValue isKindOfClass:[NSDictionary class]]) {
// Both are objects; recurse
NSMutableDictionary *subDict = [self patchDictionaryForJSON:newValue
fromOriginalJSON:originalValue];
[resultJSON setValue:subDict forKey:key];
} else {
// They are non-object values; the new replaces the old. Per the
// documentation for patch, this replaces entire arrays.
[resultJSON setValue:newValue forKey:key];
}
} else {
// The values are the same; omit this key-value pair
}
}
// Iterate through keys present only in the new object, and add them to the
// result
NSMutableArray *newKeys = [NSMutableArray arrayWithArray:newJSON.allKeys];
[newKeys removeObjectsInArray:originalKeys];
for (NSString *key in newKeys) {
id value = [newJSON objectForKey:key];
[resultJSON setValue:value forKey:key];
}
return resultJSON;
}
+ (id)nullValue {
return [NSNull null];
}
#pragma mark Additional Properties
- (id)additionalPropertyForName:(NSString *)name {
// Return the cached object, if any, before creating one.
id result = [self cacheChildForKey:name];
if (result != nil) {
return result;
}
Class defaultClass = [[self class] classForAdditionalProperties];
id jsonObj = [self JSONValueForKey:name];
BOOL shouldCache = NO;
if (jsonObj != nil) {
id<GTLRObjectClassResolver>objectClassResolver = self.objectClassResolver;
result = [GTLRRuntimeCommon objectFromJSON:jsonObj
defaultClass:defaultClass
objectClassResolver:objectClassResolver
isCacheable:&shouldCache];
}
[self setCacheChild:(shouldCache ? result : nil)
forKey:name];
return result;
}
- (void)setAdditionalProperty:(id)obj forName:(NSString *)name {
BOOL shouldCache = NO;
Class defaultClass = [[self class] classForAdditionalProperties];
id json = [GTLRRuntimeCommon jsonFromAPIObject:obj
expectedClass:defaultClass
isCacheable:&shouldCache];
[self setJSONValue:json forKey:name];
[self setCacheChild:(shouldCache ? obj : nil)
forKey:name];
}
- (NSDictionary<NSString *, id> *)additionalProperties {
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSArray *propertyNames = [self additionalJSONKeys];
for (NSString *name in propertyNames) {
id obj = [self additionalPropertyForName:name];
[result setObject:obj forKey:name];
}
return result;
}
#pragma mark Child Cache methods
// There is no property for _childCache as there shouldn't be KVC/KVO
// support for it, it's an implementation detail.
- (void)setCacheChild:(id)obj forKey:(NSString *)key {
if (_childCache == nil && obj != nil) {
_childCache = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
obj, key, nil];
} else {
[_childCache setValue:obj forKey:key];
}
}
- (id)cacheChildForKey:(NSString *)key {
id obj = [_childCache objectForKey:key];
return obj;
}
#pragma mark Support methods
+ (NSMutableArray *)allDeclaredProperties {
NSMutableArray *array = [NSMutableArray array];
// walk from this class up the hierarchy to GTLRObject
Class topClass = class_getSuperclass([GTLRObject class]);
for (Class currClass = self;
currClass != topClass;
currClass = class_getSuperclass(currClass)) {
// step through this class's properties, and add the property names to the
// array
objc_property_t *properties = class_copyPropertyList(currClass, NULL);
if (properties) {
for (objc_property_t *prop = properties;
*prop != NULL;
++prop) {
const char *propName = property_getName(*prop);
// We only want dynamic properties; their attributes contain ",D".
const char *attr = property_getAttributes(*prop);
const char *dynamicMarker = strstr(attr, ",D");
if (dynamicMarker &&
(dynamicMarker[2] == 0 || dynamicMarker[2] == ',' )) {
[array addObject:(id _Nonnull)@(propName)];
}
}
free(properties);
}
}
return array;
}
+ (NSArray *)allKnownKeys {
NSArray *allProps = [self allDeclaredProperties];
NSMutableArray *knownKeys = [NSMutableArray arrayWithArray:allProps];
NSDictionary *propMap = [GTLRObject propertyToJSONKeyMapForClass:[self class]];
NSUInteger idx = 0;
for (NSString *propName in allProps) {
NSString *jsonKey = [propMap objectForKey:propName];
if (jsonKey) {
[knownKeys replaceObjectAtIndex:idx
withObject:jsonKey];
}
++idx;
}
return knownKeys;
}
- (NSString *)description {
NSString *jsonDesc = [self JSONDescription];
NSString *str = [NSString stringWithFormat:@"%@ %p: %@",
[self class], self, jsonDesc];
return str;
}
// Internal utility for creating an appropriate description summary for the object's JSON.
- (NSString *)JSONDescription {
// Find the list of declared and otherwise known JSON keys for this class.
NSArray *knownKeys = [[self class] allKnownKeys];
NSMutableString *descStr = [NSMutableString stringWithString:@"{"];
NSString *spacer = @"";
for (NSString *key in [[_json allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]) {
NSString *value = nil;
// show question mark for JSON keys not supported by a declared property:
// foo?:"Hi mom."
NSString *qmark = [knownKeys containsObject:key] ? @"" : @"?";
// determine property value to dislay
id rawValue = [_json valueForKey:key];
if ([rawValue isKindOfClass:[NSDictionary class]]) {
// for dictionaries, show the list of keys:
// {key1,key2,key3}
NSArray *subKeys = [((NSDictionary *)rawValue).allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
NSString *subkeyList = [subKeys componentsJoinedByString:@","];
value = [NSString stringWithFormat:@"{%@}", subkeyList];
} else if ([rawValue isKindOfClass:[NSArray class]]) {
// for arrays, show the number of items in the array:
// [3]
value = [NSString stringWithFormat:@"[%lu]",
(unsigned long)((NSArray *)rawValue).count];
} else if ([rawValue isKindOfClass:[NSString class]]) {
// for strings, show the string in quotes:
// "Hi mom."
value = [NSString stringWithFormat:@"\"%@\"", rawValue];
} else {
// for numbers, show just the number
value = [rawValue description];
}
[descStr appendFormat:@"%@%@%@:%@", spacer, key, qmark, value];
spacer = @" ";
}
[descStr appendString:@"}"];
return descStr;
}
#pragma mark Object Instantiation
+ (GTLRObject *)objectForJSON:(NSMutableDictionary *)json
defaultClass:(Class)defaultClass
objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver {
if (((id)json == [NSNull null]) || json.count == 0) {
if (json != nil && defaultClass != Nil) {
// The JSON included an empty dictionary, just create the object.
Class classToCreate =
GTLRObjectResolveClass(objectClassResolver,
[NSDictionary dictionary],
defaultClass);
return [classToCreate object];
}
// No actual result, such as the response from a delete.
return nil;
}
if (defaultClass == Nil) {
defaultClass = self;
}
Class classToCreate =
GTLRObjectResolveClass(objectClassResolver, json, defaultClass);
// now instantiate the GTLRObject
GTLRObject *parsedObject = [classToCreate object];
parsedObject.objectClassResolver = objectClassResolver;
parsedObject.JSON = json;
return parsedObject;
}
#pragma mark Runtime Utilities
static NSMutableDictionary *gJSONKeyMapCache = nil;
static NSMutableDictionary *gArrayPropertyToClassMapCache = nil;
+ (void)initialize {
// Note that initialize is guaranteed by the runtime to be called in a
// thread-safe manner
if (gJSONKeyMapCache == nil) {
gJSONKeyMapCache = [[NSMutableDictionary alloc] init];
}
if (gArrayPropertyToClassMapCache == nil) {
gArrayPropertyToClassMapCache = [[NSMutableDictionary alloc] init];
}
}
+ (NSDictionary *)propertyToJSONKeyMapForClass:(Class<GTLRRuntimeCommon>)aClass {
NSDictionary *resultMap =
[GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(propertyToJSONKeyMap)
startClass:aClass
ancestorClass:[GTLRObject class]
cache:gJSONKeyMapCache];
return resultMap;
}
+ (NSDictionary *)arrayPropertyToClassMapForClass:(Class<GTLRRuntimeCommon>)aClass {
NSDictionary *resultMap =
[GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(arrayPropertyToClassMap)
startClass:aClass
ancestorClass:[GTLRObject class]
cache:gArrayPropertyToClassMapCache];
return resultMap;
}
#pragma mark Runtime Support
+ (Class<GTLRRuntimeCommon>)ancestorClass {
return [GTLRObject class];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
BOOL resolved = [GTLRRuntimeCommon resolveInstanceMethod:sel onClass:self];
if (resolved)
return YES;
return [super resolveInstanceMethod:sel];
}
@end
@implementation GTLRCollectionObject
+ (NSString *)collectionItemsKey {
// GTLRCollectionObject fast enumeration, indexed access, and automatic pagination
// (when shouldFetchNextPages is enabled) applies to the object array property "items".
// The array property's key may be different if subclasses override this method.
return @"items";
}
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
NSString *key = [[self class] collectionItemsKey];
NSArray *items = [self valueForKey:key];
if (items == nil) {
[NSException raise:NSRangeException
format:@"index %lu beyond bounds (%@ property \"%@\" is nil)",
(unsigned long)idx, [self class], key];
}
id result = [items objectAtIndexedSubscript:idx];
return result;
}
// NSFastEnumeration protocol
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nonnull *)stackbuf
count:(NSUInteger)len {
NSString *key = [[self class] collectionItemsKey];
NSArray *items = [self valueForKey:key];
NSUInteger result = [items countByEnumeratingWithState:state
objects:stackbuf
count:len];
return result;
}
@end
@implementation GTLRDataObject
@synthesize data = _data,
contentType = _contentType;
- (NSString *)description {
NSString *jsonDesc = @"";
if (self.JSON.count > 0) {
jsonDesc = [self JSONDescription];
}
return [NSString stringWithFormat:@"%@ %p: %lu bytes, contentType:%@ %@",
[self class], self, (unsigned long)self.data.length, self.contentType,
jsonDesc];
}
- (id)copyWithZone:(NSZone *)zone {
GTLRDataObject *newObj = [super copyWithZone:zone];
newObj.data = [self.data copy];
newObj.contentType = self.contentType;
return newObj;
}
@end
@implementation GTLRResultArray
- (NSArray *)itemsWithItemClass:(Class)itemClass {
// Return the cached array before creating on demand.
NSString *cacheKey = @"result_array_items";
NSMutableArray *cachedArray = [self cacheChildForKey:cacheKey];
if (cachedArray != nil) {
return cachedArray;
}
NSArray *result = nil;
NSArray *array = (NSArray *)self.JSON;
if (array != nil) {
if ([array isKindOfClass:[NSArray class]]) {
id<GTLRObjectClassResolver>objectClassResolver = self.objectClassResolver;
result = [GTLRRuntimeCommon objectFromJSON:array
defaultClass:itemClass
objectClassResolver:objectClassResolver
isCacheable:NULL];
} else {
#if DEBUG
if (![array isKindOfClass:[NSNull class]]) {
GTLR_DEBUG_LOG(@"GTLRObject: unexpected JSON: %@ should be an array, actually is a %@:\n%@",
NSStringFromClass([self class]),
NSStringFromClass([array class]),
array);
}
#endif
result = array;
}
}
[self setCacheChild:result forKey:cacheKey];
return result;
}
- (NSString *)JSONDescription {
// Just like GTLRObject's handing of arrays, just return the count.
return [NSString stringWithFormat:@"[%lu]", (unsigned long)self.JSON.count];
}
@end
Class GTLRObjectResolveClass(
id<GTLRObjectClassResolver>objectClassResolver,
NSDictionary *json,
Class defaultClass) {
Class result = [objectClassResolver classForJSON:json
defaultClass:defaultClass];
if (result == Nil) {
result = defaultClass;
}
return result;
}
@implementation GTLRObjectClassResolver {
NSDictionary<NSString *, Class> *_kindToClassMap;
NSDictionary<Class, Class> *_surrogates;
}
+ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap {
GTLRObjectClassResolver *result = [[self alloc] initWithKindMap:kindStringToClassMap
surrogates:nil];
return result;
}
+ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap
surrogates:(NSDictionary<Class, Class> *)surrogates {
GTLRObjectClassResolver *result = [[self alloc] initWithKindMap:kindStringToClassMap
surrogates:surrogates];
return result;
}
- (instancetype)initWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap
surrogates:(NSDictionary<Class, Class> *)surrogates {
self = [super init];
if (self) {
_kindToClassMap = [kindStringToClassMap copy];
_surrogates = [surrogates copy];
}
return self;
}
- (Class)classForJSON:(NSDictionary *)json
defaultClass:(Class)defaultClass {
Class result = defaultClass;
// Apply kind map.
BOOL shouldUseKind = (result == Nil) || [result isKindValidForClassRegistry];
if (shouldUseKind && [json isKindOfClass:[NSDictionary class]]) {
NSString *kind = [json valueForKey:@"kind"];
if ([kind isKindOfClass:[NSString class]] && kind.length > 0) {
Class dynamicClass = [_kindToClassMap objectForKey:kind];
if (dynamicClass) {
result = dynamicClass;
}
}
}
// Apply surrogate map.
Class surrogate = [_surrogates objectForKey:result];
if (surrogate) {
result = surrogate;
}
return result;
}
@end
static NSMutableDictionary *DeepMutableCopyOfJSONDictionary(NSDictionary *initialJSON) {
if (!initialJSON) return nil;
NSMutableDictionary *result;
CFPropertyListRef ref = CFPropertyListCreateDeepCopy(kCFAllocatorDefault,
(__bridge CFPropertyListRef)(initialJSON),
kCFPropertyListMutableContainers);
if (ref) {
result = CFBridgingRelease(ref);
} else {
// Failed to copy, probably due to a non-plist type such as NSNull.
//
// As a fallback, round-trip through NSJSONSerialization.
NSError *serializationError;
NSData *data = [NSJSONSerialization dataWithJSONObject:initialJSON
options:0
error:&serializationError];
if (!data) {
GTLR_DEBUG_ASSERT(0, @"Copy failed due to serialization: %@\nJSON: %@",
serializationError, initialJSON);
} else {
result = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&serializationError];
GTLR_DEBUG_ASSERT(result != nil, @"Copy failed due to deserialization: %@\nJSON: %@",
serializationError,
[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}
}
return result;
}