1//
2//  KBFormatter.m
3//  Keybase
4//
5//  Created by Gabriel on 5/20/15.
6//  Copyright (c) 2015 Gabriel Handford. All rights reserved.
7//
8
9#import "KBFormatter.h"
10
11#import <ObjectiveSugar/ObjectiveSugar.h>
12#import <objc/objc-runtime.h>
13
14@interface KBFormatter (NSDictionaryCreation)
15- (NSDictionary *)toDictionary;
16@end
17
18
19@implementation KBFormatter
20
21- (NSString *)format:(id)obj {
22  return [self format:obj level:-1];
23}
24
25- (NSString *)format:(id)obj level:(NSInteger)level {
26  if (!obj) {
27    return @"null";
28  } else if (obj == (void*)kCFBooleanTrue) {
29    return @"true";
30  } else if (obj == (void*)kCFBooleanFalse) {
31    return @"false";
32  } else if ([obj isKindOfClass:NSNull.class]) {
33    return @"null";
34  } else if ([obj isKindOfClass:NSString.class]) {
35    return NSStringWithFormat(@"\"%@\"", obj);
36  } else if ([obj isKindOfClass:NSArray.class]) {
37    return [self formatArray:obj level:level];
38  } else if ([obj isKindOfClass:GHODictionary.class]) {
39    return [self formatDictionary:obj level:level+1];
40  } else if ([obj isKindOfClass:NSDictionary.class]) {
41    return [self formatDictionary:obj level:level+1];
42  } else if ([obj respondsToSelector:@selector(toDictionary)]) {
43    id dict = [obj toDictionary];
44    return [self formatDictionary:dict level:level+1];
45  //} else if ([obj isKindOfClass:NSData.class]) {
46  //  return KBHexString(obj, @"");
47  } else if ([obj conformsToProtocol:@protocol(NSCopying)]) { // Default for core types
48    return [obj description];
49  } else {
50    GHODictionary *dict = KBObjectToDictionary(obj, YES);
51    if ([dict count] == 0) return [obj description];
52    return [self formatDictionary:dict level:level+1];
53  }
54}
55
56- (NSString *)formatArray:(NSArray *)array level:(NSInteger)level {
57  if (!array) return @"null";
58  if ([array count] == 0) return @"[]";
59
60  NSString *astr = [[array map:^(id obj) { return [self format:obj level:level]; }] join:@", "];
61  return NSStringWithFormat(@"[%@]", astr);
62}
63
64- (NSString *)formatDictionary:(id)dict level:(NSInteger)level {
65  if (!dict) return @"";
66  if ([dict count] == 0) return @"{}";
67  NSString *prefix = [@"" stringByPaddingToLength:(level+1)*2 withString:@" " startingAtIndex:0];
68  NSString *endPrefix = [@"" stringByPaddingToLength:level*2 withString:@" " startingAtIndex:0];
69
70  NSArray *dictStr = [dict map:^id(id key, id value) {
71    return NSStringWithFormat(@"%@: %@", [self format:key level:level], [self format:value level:level]);
72  }];
73
74  NSString *str = NSStringWithFormat(@"{\n%@%@\n%@}", prefix, [dictStr join:NSStringWithFormat(@",\n%@", prefix)], endPrefix);
75  return str;
76}
77
78@end
79
80NSString *KBDescription(id obj) {
81  KBFormatter *formatter = [[KBFormatter alloc] init];
82  return [formatter format:obj];
83}
84
85GHODictionary *KBObjectToDictionary(id obj, BOOL includeNull) {
86  NSArray *propertyNames = KBPropertyNames([obj class]);
87  GHODictionary *odict = [[GHODictionary alloc] initWithCapacity:[propertyNames count]];
88  for (NSString *propertyName in propertyNames) {
89    id value = [obj valueForKey:propertyName];
90    if (!value && includeNull) odict[propertyName] = [NSNull null];
91    else if (value) odict[propertyName] = value;
92  }
93  return odict;
94}
95
96NSString *KBClassNameOfPropertyNamed(Class clazz, NSString *propertyName) {
97  objc_property_t property = class_getProperty(clazz, propertyName.UTF8String);
98  NSString *propertyAttributes = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
99  NSArray *splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@","];
100  if (splitPropertyAttributes.count > 0) {
101    // xcdoc://ios//library/prerelease/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html
102    NSString *encodeType = splitPropertyAttributes[0];
103    NSArray *splitEncodeType = [encodeType componentsSeparatedByString:@"\""];
104    if (splitEncodeType.count > 1) {
105      NSString *className = splitEncodeType[1];
106      return className;
107    }
108  }
109  return nil;
110}
111
112NSArray *KBPropertyNames(Class clazz) {
113  unsigned int count;
114  objc_property_t *properties = class_copyPropertyList(clazz, &count);
115  NSMutableArray *propertyNames = [NSMutableArray arrayWithCapacity:count];
116  for (NSUInteger i = 0; i < count; i++) {
117    objc_property_t property = properties[i];
118    const char *propName = property_getName(property);
119    NSString *propertyName = [NSString stringWithCString:propName encoding:NSUTF8StringEncoding];
120    [propertyNames addObject:propertyName];
121  }
122  free(properties);
123  return propertyNames;
124}
125
126NSString *KBHexString(NSData *data, NSString *defaultValue) {
127  if (!data) return defaultValue;
128  if ([data length] == 0) return defaultValue;
129  NSMutableString *hexString = [NSMutableString stringWithCapacity:[data length] * 2];
130  for (NSUInteger i = 0; i < [data length]; ++i) {
131    [hexString appendFormat:@"%02X", *((uint8_t *)[data bytes] + i)];
132  }
133  return [hexString lowercaseString];
134}
135
136NSData *KBHexData(NSString *s) {
137  if ((s.length % 2) != 0) {
138    return nil;
139  }
140
141  const char *chars = [s UTF8String];
142  NSMutableData *data = [NSMutableData dataWithCapacity:s.length / 2];
143  char byteChars[3] = {0, 0, 0};
144  unsigned long wholeByte;
145
146  for (int i = 0; i < s.length; i += 2) {
147    byteChars[0] = chars[i];
148    byteChars[1] = chars[i + 1];
149    wholeByte = strtoul(byteChars, NULL, 16);
150    [data appendBytes:&wholeByte length:1];
151  }
152
153  return data;
154}
155