Last active
February 26, 2018 00:56
-
-
Save avaidyam/f4aca393fa192d1d7bac1a98c853bfc9 to your computer and use it in GitHub Desktop.
Automatically map between an NSObject and a dictionary representation, including custom properties and excluding cyclic ones.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@implementation ExampleClass | |
+ (void)load { | |
NSArray *ignoreList = [NSArray arrayWithObjects: | |
[[NSIgnoredClassKey alloc] initWithClassName:@"GPBOneofDescriptor" key:@"containingOneof"], | |
nil]; | |
NSArray *includeList = [NSArray arrayWithObjects: | |
[[NSIncludedClassKey alloc] initWithClassName:@"GPBEnumDescriptor" key:@"namedValues" block:^(id obj) | |
{ | |
#pragma clang diagnostic push | |
#pragma clang diagnostic ignored "-Warc-performSelector-leaks" | |
[obj performSelector:NSSelectorFromString(@"calcValueNameOffsets")]; | |
#pragma clang diagnostic pop | |
uint32_t valueCount = (uint32_t)[obj instanceVariableForKey:@"valueCount_"]; | |
const int32_t *values = (const int32_t *)[obj instanceVariableForKey:@"values_"]; | |
int32_t *offsets = (int32_t *)[obj instanceVariableForKey:@"nameOffsets_"]; | |
const char *names = (const char *)[obj instanceVariableForKey:@"valueNames_"]; | |
// Wrap into [value: name] dictionary. | |
NSMutableDictionary *wrapper = [NSMutableDictionary dictionary]; | |
for (uint32_t i = 0; i < valueCount; i++) { | |
int32_t value = values[i]; | |
int32_t offset = offsets[i]; | |
const char *name = names + offset; | |
if (name == nil) continue; | |
[wrapper setObject:[NSString stringWithFormat:@"%s", name] | |
forKey:[NSString stringWithFormat:@"%d", value]]; | |
} | |
return wrapper; | |
}], [[NSIncludedClassKey alloc] initWithClassName:@"GPBDescriptor" key:@"extensionRanges" block:^(id obj) | |
{ | |
uint32_t valueCount = (uint32_t)[obj instanceVariableForKey:@"extensionRangesCount_"]; | |
const GPBExtensionRange *values = (const GPBExtensionRange *)[obj instanceVariableForKey:@"extensionRanges_"]; | |
NSMutableArray *wrapper = [NSMutableArray array]; | |
// Wrap into [value: name] dictionary. | |
for (uint32_t i = 0; i < valueCount; i++) { | |
GPBExtensionRange v = values[i]; | |
[wrapper addObject:[NSString stringWithFormat:@"%u...%u", v.start, v.end]]; | |
} | |
return wrapper; | |
}], nil]; | |
// Traverse the entire message hierarchy, touching all file and enum descriptors referenced. | |
NSMutableArray *descriptors = [NSMutableArray array]; | |
[[NSObject allSubclasses:@"GPBMessage"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { | |
id descriptor = [obj safeValueForKey:@"descriptor"]; | |
id mapped = [descriptor propertyDictionaryIgnoringKeys: ignoreList including: includeList]; | |
[descriptors addObject:mapped]; | |
}]; | |
// The extention registry & root hierarchy is a bit trickier; many nested dictionaries. | |
NSMutableDictionary *extensions = [NSMutableDictionary dictionary]; | |
[[NSObject allSubclasses:@"GPBRootObject"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { | |
NSMutableDictionary *registry = [[obj safeValueForKey:@"extensionRegistry"] instanceVariableForKey:@"mutableClassMap_"]; | |
NSMutableDictionary *output = [NSMutableDictionary dictionaryWithCapacity:registry.count]; | |
[registry enumerateKeysAndObjectsUsingBlock:^(id key /* Class */, id obj /* CFDictionaryRef */, BOOL *stop) { | |
NSMutableDictionary *inner = [NSMutableDictionary dictionary]; | |
[obj enumerateKeysAndObjectsUsingBlock:^(id key2 /* long */, id obj2 /* ExtensionDescriptor */, BOOL *stop2) { | |
id mapped = [obj2 propertyDictionaryIgnoringKeys: ignoreList including: includeList]; | |
[inner setObject:mapped forKey:[NSString stringWithFormat:@"%@", key2]]; | |
}]; | |
[output setObject:inner forKey:NSStringFromClass(key)]; | |
}]; | |
[extensions setObject:output forKey:NSStringFromClass([obj class])]; | |
}]; | |
[self writeObject:descriptors toFile:@"classeslist-details.txt"]; | |
[self writeObject:extensions toFile:@"classeslist-ext.txt"]; | |
} | |
+ (void)writeObject:(id)object toFile:(NSString *)filename { | |
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); | |
NSString *documentsDirectory = [paths objectAtIndex:0]; | |
NSURL *fileURL = [NSURL fileURLWithPath:[documentsDirectory stringByAppendingPathComponent:filename]]; | |
// Write JSON file and show output. | |
NSError *error; | |
NSData *dat = [NSJSONSerialization dataWithJSONObject:object options:NSJSONWritingPrettyPrinted error:&error]; | |
if (error == nil) { | |
NSString *str = [[NSString alloc] initWithData:dat encoding:NSUTF8StringEncoding]; | |
[str writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:&error]; | |
if (error == nil) { | |
[self showText:@"OK!"]; | |
} else { | |
[self showText:error.debugDescription]; | |
} | |
} else { | |
[self showText:error.debugDescription]; | |
} | |
} | |
+ (void)showText:(NSString *)text { | |
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ | |
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: @"Message" | |
message: text | |
preferredStyle: UIAlertControllerStyleAlert]; | |
[alertController addAction: [UIAlertAction actionWithTitle: @"OK" | |
style: UIAlertActionStyleCancel | |
handler: nil]]; | |
UIViewController *controller = [UIApplication sharedApplication].keyWindow.rootViewController; | |
while (controller.presentedViewController) { | |
controller = controller.presentedViewController; | |
} | |
[controller presentViewController:alertController animated:YES completion:NULL]; | |
}); | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import Foundation; | |
NS_ASSUME_NONNULL_BEGIN | |
@interface NSIgnoredClassKey: NSObject | |
@property (retain) NSString *key; | |
@property (retain) NSString *className; | |
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key; | |
@end | |
typedef id _Nullable (^NSIncludedClassKeyValueTransformer)(id _Nonnull); | |
@interface NSIncludedClassKey: NSObject | |
@property (retain) NSString *key; | |
@property (retain) NSString *className; | |
@property (copy) NSIncludedClassKeyValueTransformer block; | |
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key | |
block:(nonnull NSIncludedClassKeyValueTransformer)block; | |
@end | |
@interface NSObject (ObjectMap) | |
+ (NSArray *)allSubclasses:(NSString *)className; | |
- (void *)instanceVariableForKey:(NSString *)key; | |
- (nullable id)safeValueForKey:(NSString *)key; | |
- (NSDictionary *)propertyDictionaryIgnoringKeys:(NSArray *)ignore including:(NSArray *)include; | |
@end | |
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@implementation NSIgnoredClassKey | |
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key { | |
if ((self = [super init])) { | |
self.className = className; | |
self.key = key; | |
} | |
return self; | |
} | |
- (BOOL)isEqual:(id)object { | |
if (object == nil || ![object isKindOfClass:self.class]) | |
return NO; | |
return [self.className isEqual:((NSIgnoredClassKey *)object).className] && | |
[self.key isEqual:((NSIgnoredClassKey *)object).key]; | |
} | |
@end | |
@implementation NSIncludedClassKey | |
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key block:(nonnull NSIncludedClassKeyValueTransformer)block { | |
if ((self = [super init])) { | |
self.className = className; | |
self.key = key; | |
self.block = block; | |
} | |
return self; | |
} | |
- (BOOL)isEqual:(id)object { | |
if (object == nil || ![object isKindOfClass:self.class]) | |
return NO; | |
return [self.className isEqual:((NSIgnoredClassKey *)object).className] && | |
[self.key isEqual:((NSIgnoredClassKey *)object).key]; | |
} | |
@end | |
@implementation NSObject (ObjectMap) | |
+ (NSArray *)allSubclasses:(NSString *)className { | |
Class parentClass = NSClassFromString(className); | |
int numClasses = objc_getClassList(NULL, 0); | |
Class *classes = NULL; | |
classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses); | |
numClasses = objc_getClassList(classes, numClasses); | |
NSMutableArray *result = [NSMutableArray array]; | |
for (NSInteger i = 0; i < numClasses; i++) { | |
Class superClass = classes[i]; | |
do { | |
superClass = class_getSuperclass(superClass); | |
} while(superClass && superClass != parentClass); | |
if (superClass == nil) continue; | |
[result addObject:classes[i]]; | |
} | |
free(classes); | |
return result; | |
} | |
- (void *)instanceVariableForKey:(NSString *)key { | |
if (key == nil) return NULL; | |
Ivar ivar = class_getInstanceVariable([self class], [key UTF8String]); | |
return (__bridge void *)(object_getIvar(self, ivar)); | |
} | |
// Return a value for the key given, or nil if undefined. | |
- (nullable id)safeValueForKey:(NSString *)key { | |
id value = nil; | |
@try { value = [self valueForKey:key]; | |
} @catch(NSException *e) {} // ignore | |
return value; | |
} | |
// Return a dictionary formed from the object; careful of cyclic references! | |
- (NSDictionary *)propertyDictionaryIgnoringKeys:(NSArray *)ignore including:(NSArray *)include { | |
NSMutableDictionary *dict = [NSMutableDictionary dictionary]; | |
unsigned count; | |
objc_property_t *properties = class_copyPropertyList([self class], &count); | |
for (int i = 0; i < count; i++) { | |
// Get the key and the value for the key if any. | |
NSString *key = [NSString stringWithUTF8String: property_getName(properties[i])]; | |
id value = [self safeValueForKey:key]; | |
if (value == nil) continue; | |
// Skip any values in the ignore list (if not a class). | |
NSString *clz = NSStringFromClass([(NSObject *)value class]); | |
if (!object_isClass(value) && (ignore != nil) && | |
[ignore containsObject:[[NSIgnoredClassKey alloc] initWithClassName:clz key:key]]) { | |
continue; | |
} | |
// If a JSON dictionary, encode keys and objects recursively. | |
if ([value isKindOfClass:[NSDictionary class]]) { | |
NSMutableDictionary *inner = [NSMutableDictionary dictionary]; | |
[(NSDictionary *)value enumerateKeysAndObjectsUsingBlock:^(id key2, id value2, BOOL* stop) { | |
[inner setObject:[value2 propertyDictionaryIgnoringKeys:ignore including:include] forKey: key2]; | |
}]; | |
[dict setObject:[NSDictionary dictionaryWithDictionary:inner] forKey:key]; | |
// If a JSON array, encode values recursively. | |
} else if ([value isKindOfClass:[NSArray class]]) { | |
NSMutableArray *inner = [NSMutableArray array]; | |
[(NSArray *)value enumerateObjectsUsingBlock:^(id value2, NSUInteger idx2, BOOL* stop) { | |
[inner addObject:[value2 propertyDictionaryIgnoringKeys:ignore including:include]]; | |
}]; | |
[dict setObject:[NSArray arrayWithArray:inner] forKey:key]; | |
// If Class type, use its string value. | |
} else if (object_isClass(value)) { | |
[dict setObject:NSStringFromClass((Class)value) forKey:key]; | |
// If not a JSON primitive, continue encoding recursively. | |
} else if (!([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])) { | |
[dict setObject:[value propertyDictionaryIgnoringKeys:ignore including:include] forKey:key]; | |
// JSON primitive: stop here. | |
} else { | |
[dict setObject:value forKey:key]; | |
} | |
} | |
// Generate synthetic properties. | |
if (include != nil) { | |
[include enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * _Nonnull stop) { | |
if (![obj isKindOfClass:[NSIncludedClassKey class]]) return; | |
if (![NSStringFromClass([self class]) isEqualToString:[(NSIncludedClassKey *)obj className]]) return; | |
id output = [(NSIncludedClassKey *)obj block](self); | |
if (output != nil) | |
[dict setObject: output forKey: [(NSIncludedClassKey *)obj key]]; | |
}]; | |
} | |
free(properties); | |
return [NSDictionary dictionaryWithDictionary:dict]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment