// // MTLModel.m // Mantle // // Created by Justin Spahr-Summers on 2012-09-11. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSError+AWSMTLModelException.h" #import "AWSMTLModel.h" #import "AWSEXTRuntimeExtensions.h" #import "AWSEXTScope.h" #import "AWSMTLReflection.h" #import // This coupling is needed for backwards compatibility in MTLModel's deprecated // methods. #import "AWSMTLJSONAdapter.h" #import "AWSMTLModel+NSCoding.h" // Used to cache the reflection performed in +propertyKeys. static void *MTLModelCachedPropertyKeysKey = &MTLModelCachedPropertyKeysKey; // Validates a value for an object and sets it if necessary. // // obj - The object for which the value is being validated. This value // must not be nil. // key - The name of one of `obj`s properties. This value must not be // nil. // value - The new value for the property identified by `key`. // forceUpdate - If set to `YES`, the value is being updated even if validating // it did not change it. // error - If not NULL, this may be set to any error that occurs during // validation // // Returns YES if `value` could be validated and set, or NO if an error // occurred. static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUpdate, NSError **error) { // Mark this as being autoreleased, because validateValue may return // a new object to be stored in this variable (and we don't want ARC to // double-free or leak the old or new values). __autoreleasing id validatedValue = value; @try { if (![obj validateValue:&validatedValue forKey:key error:error]) return NO; if (forceUpdate || value != validatedValue) { [obj setValue:validatedValue forKey:key]; } return YES; } @catch (NSException *ex) { NSLog(@"*** Caught exception setting key \"%@\" : %@", key, ex); // Fail fast in Debug builds. #if DEBUG @throw ex; #else if (error != NULL) { *error = [NSError awsmtl_modelErrorWithException:ex]; } return NO; #endif } } @interface AWSMTLModel () // Enumerates all properties of the receiver's class hierarchy, starting at the // receiver, and continuing up until (but not including) MTLModel. // // The given block will be invoked multiple times for any properties declared on // multiple classes in the hierarchy. + (void)enumeratePropertiesUsingBlock:(void (^)(objc_property_t property, BOOL *stop))block; @end @implementation AWSMTLModel #pragma mark Lifecycle + (instancetype)modelWithDictionary:(NSDictionary *)dictionary error:(NSError **)error { return [[self alloc] initWithDictionary:dictionary error:error]; } - (instancetype)init { // Nothing special by default, but we have a declaration in the header. return [super init]; } - (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error { self = [self init]; if (self == nil) return nil; for (NSString *key in dictionary) { // Mark this as being autoreleased, because validateValue may return // a new object to be stored in this variable (and we don't want ARC to // double-free or leak the old or new values). __autoreleasing id value = [dictionary objectForKey:key]; if ([value isEqual:NSNull.null]) value = nil; BOOL success = MTLValidateAndSetValue(self, key, value, YES, error); if (!success) return nil; } return self; } #pragma mark Reflection + (void)enumeratePropertiesUsingBlock:(void (^)(objc_property_t property, BOOL *stop))block { Class cls = self; BOOL stop = NO; while (!stop && ![cls isEqual:AWSMTLModel.class]) { unsigned count = 0; objc_property_t *properties = class_copyPropertyList(cls, &count); cls = cls.superclass; if (properties == NULL) continue; @onExit { free(properties); }; for (unsigned i = 0; i < count; i++) { block(properties[i], &stop); if (stop) break; } } } + (NSSet *)propertyKeys { NSSet *cachedKeys = objc_getAssociatedObject(self, MTLModelCachedPropertyKeysKey); if (cachedKeys != nil) return cachedKeys; NSMutableSet *keys = [NSMutableSet set]; [self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) { awsmtl_propertyAttributes *attributes = awsmtl_copyPropertyAttributes(property); @onExit { free(attributes); }; if (attributes->readonly && attributes->ivar == NULL) return; NSString *key = @(property_getName(property)); [keys addObject:key]; }]; // It doesn't really matter if we replace another thread's work, since we do // it atomically and the result should be the same. objc_setAssociatedObject(self, MTLModelCachedPropertyKeysKey, keys, OBJC_ASSOCIATION_COPY); return keys; } - (NSDictionary *)dictionaryValue { return [self dictionaryWithValuesForKeys:self.class.propertyKeys.allObjects]; } #pragma mark Merging - (void)mergeValueForKey:(NSString *)key fromModel:(AWSMTLModel *)model { NSParameterAssert(key != nil); SEL selector = AWSMTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:"); if (![self respondsToSelector:selector]) { if (model != nil) { [self setValue:[model valueForKey:key] forKey:key]; } return; } NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; invocation.target = self; invocation.selector = selector; [invocation setArgument:&model atIndex:2]; [invocation invoke]; } - (void)mergeValuesForKeysFromModel:(AWSMTLModel *)model { NSSet *propertyKeys = model.class.propertyKeys; for (NSString *key in self.class.propertyKeys) { if (![propertyKeys containsObject:key]) continue; [self mergeValueForKey:key fromModel:model]; } } #pragma mark Validation - (BOOL)validate:(NSError **)error { for (NSString *key in self.class.propertyKeys) { id value = [self valueForKey:key]; BOOL success = MTLValidateAndSetValue(self, key, value, NO, error); if (!success) return NO; } return YES; } #pragma mark NSCopying - (instancetype)copyWithZone:(NSZone *)zone { return [[self.class allocWithZone:zone] initWithDictionary:self.dictionaryValue error:NULL]; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, self.dictionaryValue]; } - (NSUInteger)hash { NSUInteger value = 0; for (NSString *key in self.class.propertyKeys) { value ^= [[self valueForKey:key] hash]; } return value; } - (BOOL)isEqual:(AWSMTLModel *)model { if (self == model) return YES; if (![model isMemberOfClass:self.class]) return NO; for (NSString *key in self.class.propertyKeys) { id selfValue = [self valueForKey:key]; id modelValue = [model valueForKey:key]; BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]); if (!valuesEqual) return NO; } return YES; } @end