MTLModel.m 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. //
  2. // MTLModel.m
  3. // Mantle
  4. //
  5. // Created by Justin Spahr-Summers on 2012-09-11.
  6. // Copyright (c) 2012 GitHub. All rights reserved.
  7. //
  8. #import "NSError+MTLModelException.h"
  9. #import "MTLModel.h"
  10. #import "EXTRuntimeExtensions.h"
  11. #import "EXTScope.h"
  12. #import "MTLReflection.h"
  13. #import <objc/runtime.h>
  14. // Used to cache the reflection performed in +propertyKeys.
  15. static void *MTLModelCachedPropertyKeysKey = &MTLModelCachedPropertyKeysKey;
  16. // Associated in +generateAndCachePropertyKeys with a set of all transitory
  17. // property keys.
  18. static void *MTLModelCachedTransitoryPropertyKeysKey = &MTLModelCachedTransitoryPropertyKeysKey;
  19. // Associated in +generateAndCachePropertyKeys with a set of all permanent
  20. // property keys.
  21. static void *MTLModelCachedPermanentPropertyKeysKey = &MTLModelCachedPermanentPropertyKeysKey;
  22. // Validates a value for an object and sets it if necessary.
  23. //
  24. // obj - The object for which the value is being validated. This value
  25. // must not be nil.
  26. // key - The name of one of `obj`s properties. This value must not be
  27. // nil.
  28. // value - The new value for the property identified by `key`.
  29. // forceUpdate - If set to `YES`, the value is being updated even if validating
  30. // it did not change it.
  31. // error - If not NULL, this may be set to any error that occurs during
  32. // validation
  33. //
  34. // Returns YES if `value` could be validated and set, or NO if an error
  35. // occurred.
  36. static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUpdate, NSError **error) {
  37. // Mark this as being autoreleased, because validateValue may return
  38. // a new object to be stored in this variable (and we don't want ARC to
  39. // double-free or leak the old or new values).
  40. __autoreleasing id validatedValue = value;
  41. @try {
  42. if (![obj validateValue:&validatedValue forKey:key error:error]) return NO;
  43. if (forceUpdate || value != validatedValue) {
  44. [obj setValue:validatedValue forKey:key];
  45. }
  46. return YES;
  47. } @catch (NSException *ex) {
  48. NSLog(@"*** Caught exception setting key \"%@\" : %@", key, ex);
  49. // Fail fast in Debug builds.
  50. #if DEBUG
  51. @throw ex;
  52. #else
  53. if (error != NULL) {
  54. *error = [NSError mtl_modelErrorWithException:ex];
  55. }
  56. return NO;
  57. #endif
  58. }
  59. }
  60. @interface MTLModel ()
  61. // Inspects all properties of returned by +propertyKeys using
  62. // +storageBehaviorForPropertyWithKey and caches the results.
  63. + (void)generateAndCacheStorageBehaviors;
  64. // Returns a set of all property keys for which
  65. // +storageBehaviorForPropertyWithKey returned MTLPropertyStorageTransitory.
  66. + (NSSet *)transitoryPropertyKeys;
  67. // Returns a set of all property keys for which
  68. // +storageBehaviorForPropertyWithKey returned MTLPropertyStoragePermanent.
  69. + (NSSet *)permanentPropertyKeys;
  70. // Enumerates all properties of the receiver's class hierarchy, starting at the
  71. // receiver, and continuing up until (but not including) MTLModel.
  72. //
  73. // The given block will be invoked multiple times for any properties declared on
  74. // multiple classes in the hierarchy.
  75. + (void)enumeratePropertiesUsingBlock:(void (^)(objc_property_t property, BOOL *stop))block;
  76. @end
  77. @implementation MTLModel
  78. #pragma mark Lifecycle
  79. + (void)generateAndCacheStorageBehaviors {
  80. NSMutableSet *transitoryKeys = [NSMutableSet set];
  81. NSMutableSet *permanentKeys = [NSMutableSet set];
  82. for (NSString *propertyKey in self.propertyKeys) {
  83. switch ([self storageBehaviorForPropertyWithKey:propertyKey]) {
  84. case MTLPropertyStorageNone:
  85. break;
  86. case MTLPropertyStorageTransitory:
  87. [transitoryKeys addObject:propertyKey];
  88. break;
  89. case MTLPropertyStoragePermanent:
  90. [permanentKeys addObject:propertyKey];
  91. break;
  92. }
  93. }
  94. // It doesn't really matter if we replace another thread's work, since we do
  95. // it atomically and the result should be the same.
  96. objc_setAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey, transitoryKeys, OBJC_ASSOCIATION_COPY);
  97. objc_setAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey, permanentKeys, OBJC_ASSOCIATION_COPY);
  98. }
  99. + (instancetype)modelWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
  100. return [[self alloc] initWithDictionary:dictionary error:error];
  101. }
  102. - (instancetype)init {
  103. // Nothing special by default, but we have a declaration in the header.
  104. return [super init];
  105. }
  106. - (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
  107. self = [self init];
  108. if (self == nil) return nil;
  109. for (NSString *key in dictionary) {
  110. // Mark this as being autoreleased, because validateValue may return
  111. // a new object to be stored in this variable (and we don't want ARC to
  112. // double-free or leak the old or new values).
  113. __autoreleasing id value = [dictionary objectForKey:key];
  114. if ([value isEqual:NSNull.null]) value = nil;
  115. BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
  116. if (!success) return nil;
  117. }
  118. return self;
  119. }
  120. #pragma mark Reflection
  121. + (void)enumeratePropertiesUsingBlock:(void (^)(objc_property_t property, BOOL *stop))block {
  122. Class cls = self;
  123. BOOL stop = NO;
  124. while (!stop && ![cls isEqual:MTLModel.class]) {
  125. unsigned count = 0;
  126. objc_property_t *properties = class_copyPropertyList(cls, &count);
  127. cls = cls.superclass;
  128. if (properties == NULL) continue;
  129. @onExit {
  130. free(properties);
  131. };
  132. for (unsigned i = 0; i < count; i++) {
  133. block(properties[i], &stop);
  134. if (stop) break;
  135. }
  136. }
  137. }
  138. + (NSSet *)propertyKeys {
  139. NSSet *cachedKeys = objc_getAssociatedObject(self, MTLModelCachedPropertyKeysKey);
  140. if (cachedKeys != nil) return cachedKeys;
  141. NSMutableSet *keys = [NSMutableSet set];
  142. [self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
  143. NSString *key = @(property_getName(property));
  144. if ([self storageBehaviorForPropertyWithKey:key] != MTLPropertyStorageNone) {
  145. [keys addObject:key];
  146. }
  147. }];
  148. // It doesn't really matter if we replace another thread's work, since we do
  149. // it atomically and the result should be the same.
  150. objc_setAssociatedObject(self, MTLModelCachedPropertyKeysKey, keys, OBJC_ASSOCIATION_COPY);
  151. return keys;
  152. }
  153. + (NSSet *)transitoryPropertyKeys {
  154. NSSet *transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
  155. if (transitoryPropertyKeys == nil) {
  156. [self generateAndCacheStorageBehaviors];
  157. transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
  158. }
  159. return transitoryPropertyKeys;
  160. }
  161. + (NSSet *)permanentPropertyKeys {
  162. NSSet *permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
  163. if (permanentPropertyKeys == nil) {
  164. [self generateAndCacheStorageBehaviors];
  165. permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
  166. }
  167. return permanentPropertyKeys;
  168. }
  169. - (NSDictionary *)dictionaryValue {
  170. NSSet *keys = [self.class.transitoryPropertyKeys setByAddingObjectsFromSet:self.class.permanentPropertyKeys];
  171. return [self dictionaryWithValuesForKeys:keys.allObjects];
  172. }
  173. + (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey {
  174. objc_property_t property = class_getProperty(self.class, propertyKey.UTF8String);
  175. if (property == NULL) return MTLPropertyStorageNone;
  176. mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
  177. @onExit {
  178. free(attributes);
  179. };
  180. BOOL hasGetter = [self instancesRespondToSelector:attributes->getter];
  181. BOOL hasSetter = [self instancesRespondToSelector:attributes->setter];
  182. if (!attributes->dynamic && attributes->ivar == NULL && !hasGetter && !hasSetter) {
  183. return MTLPropertyStorageNone;
  184. } else if (attributes->readonly && attributes->ivar == NULL) {
  185. if ([self isEqual:MTLModel.class]) {
  186. return MTLPropertyStorageNone;
  187. } else {
  188. // Check superclass in case the subclass redeclares a property that
  189. // falls through
  190. return [self.superclass storageBehaviorForPropertyWithKey:propertyKey];
  191. }
  192. } else {
  193. return MTLPropertyStoragePermanent;
  194. }
  195. }
  196. #pragma mark Merging
  197. - (void)mergeValueForKey:(NSString *)key fromModel:(NSObject<MTLModel> *)model {
  198. NSParameterAssert(key != nil);
  199. SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
  200. if (![self respondsToSelector:selector]) {
  201. if (model != nil) {
  202. [self setValue:[model valueForKey:key] forKey:key];
  203. }
  204. return;
  205. }
  206. IMP imp = [self methodForSelector:selector];
  207. void (*function)(id, SEL, id<MTLModel>) = (__typeof__(function))imp;
  208. function(self, selector, model);
  209. }
  210. - (void)mergeValuesForKeysFromModel:(id<MTLModel>)model {
  211. NSSet *propertyKeys = model.class.propertyKeys;
  212. for (NSString *key in self.class.propertyKeys) {
  213. if (![propertyKeys containsObject:key]) continue;
  214. [self mergeValueForKey:key fromModel:model];
  215. }
  216. }
  217. #pragma mark Validation
  218. - (BOOL)validate:(NSError **)error {
  219. for (NSString *key in self.class.propertyKeys) {
  220. id value = [self valueForKey:key];
  221. BOOL success = MTLValidateAndSetValue(self, key, value, NO, error);
  222. if (!success) return NO;
  223. }
  224. return YES;
  225. }
  226. #pragma mark NSCopying
  227. - (instancetype)copyWithZone:(NSZone *)zone {
  228. MTLModel *copy = [[self.class allocWithZone:zone] init];
  229. [copy setValuesForKeysWithDictionary:self.dictionaryValue];
  230. return copy;
  231. }
  232. #pragma mark NSObject
  233. - (NSString *)description {
  234. NSDictionary *permanentProperties = [self dictionaryWithValuesForKeys:self.class.permanentPropertyKeys.allObjects];
  235. return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, permanentProperties];
  236. }
  237. - (NSUInteger)hash {
  238. NSUInteger value = 0;
  239. for (NSString *key in self.class.permanentPropertyKeys) {
  240. value ^= [[self valueForKey:key] hash];
  241. }
  242. return value;
  243. }
  244. - (BOOL)isEqual:(MTLModel *)model {
  245. if (self == model) return YES;
  246. if (![model isMemberOfClass:self.class]) return NO;
  247. for (NSString *key in self.class.permanentPropertyKeys) {
  248. id selfValue = [self valueForKey:key];
  249. id modelValue = [model valueForKey:key];
  250. BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]);
  251. if (!valuesEqual) return NO;
  252. }
  253. return YES;
  254. }
  255. @end