blob: 2ef60feda2aac4a6134cc41d3a9c5eb51dc3218a [file] [log] [blame]
//
// MTLModel.m
// Mantle
//
// Created by Justin Spahr-Summers on 2012-09-11.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import "NSError+MTLModelException.h"
#import "MTLModel.h"
#import "EXTRuntimeExtensions.h"
#import "EXTScope.h"
#import "MTLReflection.h"
#import <objc/runtime.h>
// This coupling is needed for backwards compatibility in MTLModel's deprecated
// methods.
#import "MTLJSONAdapter.h"
#import "MTLModel+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 mtl_modelErrorWithException:ex];
}
return NO;
#endif
}
}
@interface MTLModel ()
// 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 MTLModel
#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:MTLModel.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) {
mtl_propertyAttributes *attributes = mtl_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:(MTLModel *)model {
NSParameterAssert(key != nil);
SEL selector = MTLSelectorWithCapitalizedKeyPattern("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:(MTLModel *)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:(MTLModel *)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