From 4e3df7a9a269c729cd575ad3b3376b4242504f57 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 25 Jun 2020 14:12:18 -0700 Subject: [PATCH] Separate public utils from private utils appropriately (#913) * Separated public utils from private utils * Made SEGAnalyticsUtils public. * Fixed header includes. Co-authored-by: Brandon Sneed --- Analytics.xcodeproj/project.pbxproj | 4 +- Analytics/Classes/SEGAnalyticsUtils.h | 10 + Analytics/Classes/SEGAnalyticsUtils.m | 23 ++ Analytics/Classes/SEGSegmentIntegration.m | 2 +- Analytics/Internal/SEGAnalyticsUtils.h | 50 ---- Analytics/Internal/SEGAnalyticsUtils.m | 319 ---------------------- Analytics/Internal/SEGUtils.h | 40 +++ Analytics/Internal/SEGUtils.m | 297 ++++++++++++++++++++ 8 files changed, 373 insertions(+), 372 deletions(-) create mode 100644 Analytics/Classes/SEGAnalyticsUtils.h create mode 100644 Analytics/Classes/SEGAnalyticsUtils.m delete mode 100644 Analytics/Internal/SEGAnalyticsUtils.h delete mode 100644 Analytics/Internal/SEGAnalyticsUtils.m diff --git a/Analytics.xcodeproj/project.pbxproj b/Analytics.xcodeproj/project.pbxproj index 9f61cce48..8bf89ef46 100644 --- a/Analytics.xcodeproj/project.pbxproj +++ b/Analytics.xcodeproj/project.pbxproj @@ -358,6 +358,8 @@ EADEB89B1DECD12B005322DA /* SEGStorage.h */, EADEB88A1DECD12B005322DA /* SEGTrackPayload.h */, EADEB88B1DECD12B005322DA /* SEGTrackPayload.m */, + EADEB88F1DECD12B005322DA /* SEGAnalyticsUtils.h */, + EADEB8901DECD12B005322DA /* SEGAnalyticsUtils.m */, ); path = Classes; sourceTree = ""; @@ -371,8 +373,6 @@ EADEB88E1DECD12B005322DA /* NSData+SEGGZIP.m */, EADEB8781DECD12B005322DA /* SEGAES256Crypto.h */, EADEB8791DECD12B005322DA /* SEGAES256Crypto.m */, - EADEB88F1DECD12B005322DA /* SEGAnalyticsUtils.h */, - EADEB8901DECD12B005322DA /* SEGAnalyticsUtils.m */, EADEB8911DECD12B005322DA /* SEGFileStorage.h */, EADEB8921DECD12B005322DA /* SEGFileStorage.m */, EADEB8841DECD12B005322DA /* SEGIntegrationsManager.h */, diff --git a/Analytics/Classes/SEGAnalyticsUtils.h b/Analytics/Classes/SEGAnalyticsUtils.h new file mode 100644 index 000000000..a2b8b0675 --- /dev/null +++ b/Analytics/Classes/SEGAnalyticsUtils.h @@ -0,0 +1,10 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +// Logging + +void SEGSetShowDebugLogs(BOOL showDebugLogs); +void SEGLog(NSString *format, ...); + +NS_ASSUME_NONNULL_END diff --git a/Analytics/Classes/SEGAnalyticsUtils.m b/Analytics/Classes/SEGAnalyticsUtils.m new file mode 100644 index 000000000..5cd36ef92 --- /dev/null +++ b/Analytics/Classes/SEGAnalyticsUtils.m @@ -0,0 +1,23 @@ +#import "SEGAnalyticsUtils.h" +#import "SEGAnalytics.h" + +static BOOL kAnalyticsLoggerShowLogs = NO; + +// Logging + +void SEGSetShowDebugLogs(BOOL showDebugLogs) +{ + kAnalyticsLoggerShowLogs = showDebugLogs; +} + +void SEGLog(NSString *format, ...) +{ + if (!kAnalyticsLoggerShowLogs) + return; + + va_list args; + va_start(args, format); + NSLogv(format, args); + va_end(args); +} + diff --git a/Analytics/Classes/SEGSegmentIntegration.m b/Analytics/Classes/SEGSegmentIntegration.m index 272caa4a5..9fa19faee 100644 --- a/Analytics/Classes/SEGSegmentIntegration.m +++ b/Analytics/Classes/SEGSegmentIntegration.m @@ -2,7 +2,7 @@ #import #import "SEGAnalytics.h" -#import "SEGAnalyticsUtils.h" +#import "SEGUtils.h" #import "SEGSegmentIntegration.h" #import "SEGReachability.h" #import "SEGHTTPClient.h" diff --git a/Analytics/Internal/SEGAnalyticsUtils.h b/Analytics/Internal/SEGAnalyticsUtils.h deleted file mode 100644 index ec7610211..000000000 --- a/Analytics/Internal/SEGAnalyticsUtils.h +++ /dev/null @@ -1,50 +0,0 @@ -#import -#import "SEGSerializableValue.h" - -NS_ASSUME_NONNULL_BEGIN - -NSString *GenerateUUIDString(void); - -// Date Utils -NSString *iso8601FormattedString(NSDate *date); -NSString *iso8601NanoFormattedString(NSDate *date); - -void trimQueue(NSMutableArray *array, NSUInteger size); - -// Async Utils -dispatch_queue_t seg_dispatch_queue_create_specific(const char *label, - dispatch_queue_attr_t _Nullable attr); -BOOL seg_dispatch_is_on_specific_queue(dispatch_queue_t queue); -void seg_dispatch_specific(dispatch_queue_t queue, dispatch_block_t block, - BOOL waitForCompletion); -void seg_dispatch_specific_async(dispatch_queue_t queue, - dispatch_block_t block); -void seg_dispatch_specific_sync(dispatch_queue_t queue, dispatch_block_t block); - -// Logging - -void SEGSetShowDebugLogs(BOOL showDebugLogs); -void SEGLog(NSString *format, ...); - -// JSON Utils - -JSON_DICT SEGCoerceDictionary(NSDictionary *_Nullable dict); - -NSString *_Nullable SEGIDFA(void); - -NSString *SEGEventNameForScreenTitle(NSString *title); - -// Deep copy and check NSCoding conformance -@protocol SEGSerializableDeepCopy --(id _Nullable) serializableMutableDeepCopy; --(id _Nullable) serializableDeepCopy; -@end - -@interface NSDictionary(SerializableDeepCopy) -@end - -@interface NSArray(SerializableDeepCopy) -@end - - -NS_ASSUME_NONNULL_END diff --git a/Analytics/Internal/SEGAnalyticsUtils.m b/Analytics/Internal/SEGAnalyticsUtils.m deleted file mode 100644 index c25e2662e..000000000 --- a/Analytics/Internal/SEGAnalyticsUtils.m +++ /dev/null @@ -1,319 +0,0 @@ -#import "SEGAnalyticsUtils.h" -#import "SEGAnalytics.h" - -static BOOL kAnalyticsLoggerShowLogs = NO; - - -@interface SEGISO8601NanosecondDateFormatter: NSDateFormatter -@end - -@implementation SEGISO8601NanosecondDateFormatter - -- (id)init -{ - self = [super init]; - self.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS:'Z'"; - self.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; - self.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; - return self; -} - -const NSInteger __SEG_NANO_MAX_LENGTH = 9; -- (NSString * _Nonnull)stringFromDate:(NSDate *)date -{ - NSCalendar *calendar = [NSCalendar currentCalendar]; - NSDateComponents *dateComponents = [calendar components:NSCalendarUnitSecond | NSCalendarUnitNanosecond fromDate:date]; - NSString *genericDateString = [super stringFromDate:date]; - - NSMutableArray *stringComponents = [[genericDateString componentsSeparatedByString:@"."] mutableCopy]; - NSString *nanoSeconds = [NSString stringWithFormat:@"%li", (long)dateComponents.nanosecond]; - - if (nanoSeconds.length > __SEG_NANO_MAX_LENGTH) { - nanoSeconds = [nanoSeconds substringToIndex:__SEG_NANO_MAX_LENGTH]; - } else { - nanoSeconds = [nanoSeconds stringByPaddingToLength:__SEG_NANO_MAX_LENGTH withString:@"0" startingAtIndex:0]; - } - - NSString *result = [NSString stringWithFormat:@"%@.%@Z", stringComponents[0], nanoSeconds]; - - return result; -} - -@end - - -NSString *GenerateUUIDString() -{ - CFUUIDRef theUUID = CFUUIDCreate(NULL); - NSString *UUIDString = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, theUUID); - CFRelease(theUUID); - return UUIDString; -} - - -// Date Utils -NSString *iso8601NanoFormattedString(NSDate *date) -{ - static NSDateFormatter *dateFormatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - dateFormatter = [[SEGISO8601NanosecondDateFormatter alloc] init]; - }); - return [dateFormatter stringFromDate:date]; -} - -NSString *iso8601FormattedString(NSDate *date) -{ - static NSDateFormatter *dateFormatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - dateFormatter = [[NSDateFormatter alloc] init]; - dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; - dateFormatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS'Z'"; - dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; - }); - return [dateFormatter stringFromDate:date]; -} - - -/** trim the queue so that it contains only upto `max` number of elements. */ -void trimQueue(NSMutableArray *queue, NSUInteger max) -{ - if (queue.count < max) { - return; - } - - // Previously we didn't cap the queue. Hence there are cases where - // the queue may already be larger than 1000 events. Delete as many - // events as required to trim the queue size. - NSRange range = NSMakeRange(0, queue.count - max); - [queue removeObjectsInRange:range]; -} - -// Async Utils -dispatch_queue_t -seg_dispatch_queue_create_specific(const char *label, - dispatch_queue_attr_t attr) -{ - dispatch_queue_t queue = dispatch_queue_create(label, attr); - dispatch_queue_set_specific(queue, (__bridge const void *)queue, - (__bridge void *)queue, NULL); - return queue; -} - -BOOL seg_dispatch_is_on_specific_queue(dispatch_queue_t queue) -{ - return dispatch_get_specific((__bridge const void *)queue) != NULL; -} - -void seg_dispatch_specific(dispatch_queue_t queue, dispatch_block_t block, - BOOL waitForCompletion) -{ - dispatch_block_t autoreleasing_block = ^{ - @autoreleasepool - { - block(); - } - }; - if (dispatch_get_specific((__bridge const void *)queue)) { - autoreleasing_block(); - } else if (waitForCompletion) { - dispatch_sync(queue, autoreleasing_block); - } else { - dispatch_async(queue, autoreleasing_block); - } -} - -void seg_dispatch_specific_async(dispatch_queue_t queue, - dispatch_block_t block) -{ - seg_dispatch_specific(queue, block, NO); -} - -void seg_dispatch_specific_sync(dispatch_queue_t queue, - dispatch_block_t block) -{ - seg_dispatch_specific(queue, block, YES); -} - -// Logging - -void SEGSetShowDebugLogs(BOOL showDebugLogs) -{ - kAnalyticsLoggerShowLogs = showDebugLogs; -} - -void SEGLog(NSString *format, ...) -{ - if (!kAnalyticsLoggerShowLogs) - return; - - va_list args; - va_start(args, format); - NSLogv(format, args); - va_end(args); -} - -// JSON Utils - -static id SEGCoerceJSONObject(id obj) -{ - // if the object is a NSString, NSNumber - // then we're good - if ([obj isKindOfClass:[NSString class]] || - [obj isKindOfClass:[NSNumber class]] || - [obj isKindOfClass:[NSNull class]]) { - return obj; - } - - if ([obj isKindOfClass:[NSArray class]]) { - NSMutableArray *array = [NSMutableArray array]; - for (id i in obj) { - NSObject *value = i; - // Hotfix: Storage format should support NSNull instead - if ([value isKindOfClass:[NSNull class]]) { - value = [NSData data]; - } - [array addObject:SEGCoerceJSONObject(value)]; - } - return array; - } - - if ([obj isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *dict = [NSMutableDictionary dictionary]; - for (NSString *key in obj) { - NSObject *value = obj[key]; - if (![key isKindOfClass:[NSString class]]) - SEGLog(@"warning: dictionary keys should be strings. got: %@. coercing " - @"to: %@", - [key class], [key description]); - dict[key.description] = SEGCoerceJSONObject(value); - } - return dict; - } - - if ([obj isKindOfClass:[NSDate class]]) - return iso8601FormattedString(obj); - - if ([obj isKindOfClass:[NSURL class]]) - return [obj absoluteString]; - - // default to sending the object's description - SEGLog(@"warning: dictionary values should be valid json types. got: %@. " - @"coercing to: %@", - [obj class], [obj description]); - return [obj description]; -} - -NSDictionary *SEGCoerceDictionary(NSDictionary *dict) -{ - // make sure that a new dictionary exists even if the input is null - dict = dict ?: @{}; - // assert that the proper types are in the dictionary - dict = [dict serializableDeepCopy]; - // coerce urls, and dates to the proper format - return SEGCoerceJSONObject(dict); -} - -NSString *SEGEventNameForScreenTitle(NSString *title) -{ - return [[NSString alloc] initWithFormat:@"Viewed %@ Screen", title]; -} - - -@implementation NSDictionary(SerializableDeepCopy) - -- (id)serializableDeepCopy:(BOOL)mutable -{ - NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:self.count]; - NSArray *keys = [self allKeys]; - for (id key in keys) { - id aValue = [self objectForKey:key]; - id theCopy = nil; - - if (![aValue conformsToProtocol:@protocol(NSCoding)]) { -#ifdef DEBUG - NSAssert(FALSE, @"key `%@` doesn't conform to NSCoding and can't be serialized for delivery.", key); -#else - SEGLog(@"key `%@` doesn't conform to NSCoding and can't be serialized for delivery.", key); - // simply leave it out since we can't encode it anyway. - continue; -#endif - } - - if ([aValue conformsToProtocol:@protocol(SEGSerializableDeepCopy)]) { - theCopy = [aValue serializableDeepCopy:mutable]; - } else if ([aValue conformsToProtocol:@protocol(NSCopying)]) { - theCopy = [aValue copy]; - } else { - theCopy = aValue; - } - - [result setValue:theCopy forKey:key]; - } - - if (mutable) { - return result; - } else { - return [result copy]; - } -} - -- (NSDictionary *)serializableDeepCopy { - return [self serializableDeepCopy:NO]; -} - -- (NSMutableDictionary *)serializableMutableDeepCopy { - return [self serializableDeepCopy:YES]; -} - -@end - - -@implementation NSArray(SerializableDeepCopy) - --(id)serializableDeepCopy:(BOOL)mutable -{ - NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:self.count]; - - for (id aValue in self) { - id theCopy = nil; - - if (![aValue conformsToProtocol:@protocol(NSCoding)]) { -#ifdef DEBUG - NSAssert(FALSE, @"type `%@` doesn't conform to NSCoding and can't be serialized for delivery.", NSStringFromClass([aValue class])); -#else - SEGLog(@"type `%@` doesn't conform to NSCoding and can't be serialized for delivery.", NSStringFromClass([aValue class])); - // simply leave it out since we can't encode it anyway. - continue; -#endif - } - - if ([aValue conformsToProtocol:@protocol(SEGSerializableDeepCopy)]) { - theCopy = [aValue serializableDeepCopy:mutable]; - } else if ([aValue conformsToProtocol:@protocol(NSCopying)]) { - theCopy = [aValue copy]; - } else { - theCopy = aValue; - } - [result addObject:theCopy]; - } - - if (mutable) { - return result; - } else { - return [result copy]; - } -} - - -- (NSArray *)serializableDeepCopy { - return [self serializableDeepCopy:NO]; -} - -- (NSMutableArray *)serializableMutableDeepCopy { - return [self serializableDeepCopy:YES]; -} - - -@end diff --git a/Analytics/Internal/SEGUtils.h b/Analytics/Internal/SEGUtils.h index 68c96bc25..be350685a 100644 --- a/Analytics/Internal/SEGUtils.h +++ b/Analytics/Internal/SEGUtils.h @@ -5,6 +5,7 @@ #import #import "SEGAnalyticsUtils.h" +#import "SEGSerializableValue.h" NS_ASSUME_NONNULL_BEGIN @@ -29,4 +30,43 @@ BOOL getAdTrackingEnabled(SEGAnalyticsConfiguration *configuration); NSDictionary *getStaticContext(SEGAnalyticsConfiguration *configuration, NSString * _Nullable deviceToken); NSDictionary *getLiveContext(SEGReachability *reachability, NSDictionary * _Nullable referrer, NSDictionary * _Nullable traits); +NSString *GenerateUUIDString(void); + +// Date Utils +NSString *iso8601FormattedString(NSDate *date); +NSString *iso8601NanoFormattedString(NSDate *date); + +void trimQueue(NSMutableArray *array, NSUInteger size); + +// Async Utils +dispatch_queue_t seg_dispatch_queue_create_specific(const char *label, + dispatch_queue_attr_t _Nullable attr); +BOOL seg_dispatch_is_on_specific_queue(dispatch_queue_t queue); +void seg_dispatch_specific(dispatch_queue_t queue, dispatch_block_t block, + BOOL waitForCompletion); +void seg_dispatch_specific_async(dispatch_queue_t queue, + dispatch_block_t block); +void seg_dispatch_specific_sync(dispatch_queue_t queue, dispatch_block_t block); + +// JSON Utils + +JSON_DICT SEGCoerceDictionary(NSDictionary *_Nullable dict); + +NSString *_Nullable SEGIDFA(void); + +NSString *SEGEventNameForScreenTitle(NSString *title); + +// Deep copy and check NSCoding conformance +@protocol SEGSerializableDeepCopy +-(id _Nullable) serializableMutableDeepCopy; +-(id _Nullable) serializableDeepCopy; +@end + +@interface NSDictionary(SerializableDeepCopy) +@end + +@interface NSArray(SerializableDeepCopy) +@end + + NS_ASSUME_NONNULL_END diff --git a/Analytics/Internal/SEGUtils.m b/Analytics/Internal/SEGUtils.m index ee1d1ae53..5a4ce926c 100644 --- a/Analytics/Internal/SEGUtils.m +++ b/Analytics/Internal/SEGUtils.m @@ -272,3 +272,300 @@ BOOL getAdTrackingEnabled(SEGAnalyticsConfiguration *configuration) return [context copy]; } + + +@interface SEGISO8601NanosecondDateFormatter: NSDateFormatter +@end + +@implementation SEGISO8601NanosecondDateFormatter + +- (id)init +{ + self = [super init]; + self.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS:'Z'"; + self.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + self.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + return self; +} + +const NSInteger __SEG_NANO_MAX_LENGTH = 9; +- (NSString * _Nonnull)stringFromDate:(NSDate *)date +{ + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSDateComponents *dateComponents = [calendar components:NSCalendarUnitSecond | NSCalendarUnitNanosecond fromDate:date]; + NSString *genericDateString = [super stringFromDate:date]; + + NSMutableArray *stringComponents = [[genericDateString componentsSeparatedByString:@"."] mutableCopy]; + NSString *nanoSeconds = [NSString stringWithFormat:@"%li", (long)dateComponents.nanosecond]; + + if (nanoSeconds.length > __SEG_NANO_MAX_LENGTH) { + nanoSeconds = [nanoSeconds substringToIndex:__SEG_NANO_MAX_LENGTH]; + } else { + nanoSeconds = [nanoSeconds stringByPaddingToLength:__SEG_NANO_MAX_LENGTH withString:@"0" startingAtIndex:0]; + } + + NSString *result = [NSString stringWithFormat:@"%@.%@Z", stringComponents[0], nanoSeconds]; + + return result; +} + +@end + + +NSString *GenerateUUIDString() +{ + CFUUIDRef theUUID = CFUUIDCreate(NULL); + NSString *UUIDString = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, theUUID); + CFRelease(theUUID); + return UUIDString; +} + + +// Date Utils +NSString *iso8601NanoFormattedString(NSDate *date) +{ + static NSDateFormatter *dateFormatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [[SEGISO8601NanosecondDateFormatter alloc] init]; + }); + return [dateFormatter stringFromDate:date]; +} + +NSString *iso8601FormattedString(NSDate *date) +{ + static NSDateFormatter *dateFormatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + dateFormatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS'Z'"; + dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + }); + return [dateFormatter stringFromDate:date]; +} + + +/** trim the queue so that it contains only upto `max` number of elements. */ +void trimQueue(NSMutableArray *queue, NSUInteger max) +{ + if (queue.count < max) { + return; + } + + // Previously we didn't cap the queue. Hence there are cases where + // the queue may already be larger than 1000 events. Delete as many + // events as required to trim the queue size. + NSRange range = NSMakeRange(0, queue.count - max); + [queue removeObjectsInRange:range]; +} + +// Async Utils +dispatch_queue_t +seg_dispatch_queue_create_specific(const char *label, + dispatch_queue_attr_t attr) +{ + dispatch_queue_t queue = dispatch_queue_create(label, attr); + dispatch_queue_set_specific(queue, (__bridge const void *)queue, + (__bridge void *)queue, NULL); + return queue; +} + +BOOL seg_dispatch_is_on_specific_queue(dispatch_queue_t queue) +{ + return dispatch_get_specific((__bridge const void *)queue) != NULL; +} + +void seg_dispatch_specific(dispatch_queue_t queue, dispatch_block_t block, + BOOL waitForCompletion) +{ + dispatch_block_t autoreleasing_block = ^{ + @autoreleasepool + { + block(); + } + }; + if (dispatch_get_specific((__bridge const void *)queue)) { + autoreleasing_block(); + } else if (waitForCompletion) { + dispatch_sync(queue, autoreleasing_block); + } else { + dispatch_async(queue, autoreleasing_block); + } +} + +void seg_dispatch_specific_async(dispatch_queue_t queue, + dispatch_block_t block) +{ + seg_dispatch_specific(queue, block, NO); +} + +void seg_dispatch_specific_sync(dispatch_queue_t queue, + dispatch_block_t block) +{ + seg_dispatch_specific(queue, block, YES); +} + +// JSON Utils + +static id SEGCoerceJSONObject(id obj) +{ + // if the object is a NSString, NSNumber + // then we're good + if ([obj isKindOfClass:[NSString class]] || + [obj isKindOfClass:[NSNumber class]] || + [obj isKindOfClass:[NSNull class]]) { + return obj; + } + + if ([obj isKindOfClass:[NSArray class]]) { + NSMutableArray *array = [NSMutableArray array]; + for (id i in obj) { + NSObject *value = i; + // Hotfix: Storage format should support NSNull instead + if ([value isKindOfClass:[NSNull class]]) { + value = [NSData data]; + } + [array addObject:SEGCoerceJSONObject(value)]; + } + return array; + } + + if ([obj isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + for (NSString *key in obj) { + NSObject *value = obj[key]; + if (![key isKindOfClass:[NSString class]]) + SEGLog(@"warning: dictionary keys should be strings. got: %@. coercing " + @"to: %@", + [key class], [key description]); + dict[key.description] = SEGCoerceJSONObject(value); + } + return dict; + } + + if ([obj isKindOfClass:[NSDate class]]) + return iso8601FormattedString(obj); + + if ([obj isKindOfClass:[NSURL class]]) + return [obj absoluteString]; + + // default to sending the object's description + SEGLog(@"warning: dictionary values should be valid json types. got: %@. " + @"coercing to: %@", + [obj class], [obj description]); + return [obj description]; +} + +NSDictionary *SEGCoerceDictionary(NSDictionary *dict) +{ + // make sure that a new dictionary exists even if the input is null + dict = dict ?: @{}; + // assert that the proper types are in the dictionary + dict = [dict serializableDeepCopy]; + // coerce urls, and dates to the proper format + return SEGCoerceJSONObject(dict); +} + +NSString *SEGEventNameForScreenTitle(NSString *title) +{ + return [[NSString alloc] initWithFormat:@"Viewed %@ Screen", title]; +} + + +@implementation NSDictionary(SerializableDeepCopy) + +- (id)serializableDeepCopy:(BOOL)mutable +{ + NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:self.count]; + NSArray *keys = [self allKeys]; + for (id key in keys) { + id aValue = [self objectForKey:key]; + id theCopy = nil; + + if (![aValue conformsToProtocol:@protocol(NSCoding)]) { +#ifdef DEBUG + NSAssert(FALSE, @"key `%@` doesn't conform to NSCoding and can't be serialized for delivery.", key); +#else + SEGLog(@"key `%@` doesn't conform to NSCoding and can't be serialized for delivery.", key); + // simply leave it out since we can't encode it anyway. + continue; +#endif + } + + if ([aValue conformsToProtocol:@protocol(SEGSerializableDeepCopy)]) { + theCopy = [aValue serializableDeepCopy:mutable]; + } else if ([aValue conformsToProtocol:@protocol(NSCopying)]) { + theCopy = [aValue copy]; + } else { + theCopy = aValue; + } + + [result setValue:theCopy forKey:key]; + } + + if (mutable) { + return result; + } else { + return [result copy]; + } +} + +- (NSDictionary *)serializableDeepCopy { + return [self serializableDeepCopy:NO]; +} + +- (NSMutableDictionary *)serializableMutableDeepCopy { + return [self serializableDeepCopy:YES]; +} + +@end + + +@implementation NSArray(SerializableDeepCopy) + +-(id)serializableDeepCopy:(BOOL)mutable +{ + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:self.count]; + + for (id aValue in self) { + id theCopy = nil; + + if (![aValue conformsToProtocol:@protocol(NSCoding)]) { +#ifdef DEBUG + NSAssert(FALSE, @"type `%@` doesn't conform to NSCoding and can't be serialized for delivery.", NSStringFromClass([aValue class])); +#else + SEGLog(@"type `%@` doesn't conform to NSCoding and can't be serialized for delivery.", NSStringFromClass([aValue class])); + // simply leave it out since we can't encode it anyway. + continue; +#endif + } + + if ([aValue conformsToProtocol:@protocol(SEGSerializableDeepCopy)]) { + theCopy = [aValue serializableDeepCopy:mutable]; + } else if ([aValue conformsToProtocol:@protocol(NSCopying)]) { + theCopy = [aValue copy]; + } else { + theCopy = aValue; + } + [result addObject:theCopy]; + } + + if (mutable) { + return result; + } else { + return [result copy]; + } +} + + +- (NSArray *)serializableDeepCopy { + return [self serializableDeepCopy:NO]; +} + +- (NSMutableArray *)serializableMutableDeepCopy { + return [self serializableDeepCopy:YES]; +} + + +@end