This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
335 lines
9.9 KiB
Objective-C
335 lines
9.9 KiB
Objective-C
//
|
|
// MTLModel.m
|
|
// Mantle
|
|
//
|
|
// Created by Justin Spahr-Summers on 2012-09-11.
|
|
// Copyright (c) 2012 GitHub. All rights reserved.
|
|
//
|
|
|
|
#import "MTLEXTRuntimeExtensions.h"
|
|
#import "MTLEXTScope.h"
|
|
#import "MTLModel.h"
|
|
#import "MTLReflection.h"
|
|
#import "NSError+MTLModelException.h"
|
|
#import <objc/runtime.h>
|
|
|
|
// Used to cache the reflection performed in +propertyKeys.
|
|
static void *MTLModelCachedPropertyKeysKey = &MTLModelCachedPropertyKeysKey;
|
|
|
|
// Associated in +generateAndCachePropertyKeys with a set of all transitory
|
|
// property keys.
|
|
static void *MTLModelCachedTransitoryPropertyKeysKey = &MTLModelCachedTransitoryPropertyKeysKey;
|
|
|
|
// Associated in +generateAndCachePropertyKeys with a set of all permanent
|
|
// property keys.
|
|
static void *MTLModelCachedPermanentPropertyKeysKey = &MTLModelCachedPermanentPropertyKeysKey;
|
|
|
|
// 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 ()
|
|
|
|
// Inspects all properties of returned by +propertyKeys using
|
|
// +storageBehaviorForPropertyWithKey and caches the results.
|
|
+ (void)generateAndCacheStorageBehaviors;
|
|
|
|
// Returns a set of all property keys for which
|
|
// +storageBehaviorForPropertyWithKey returned MTLPropertyStorageTransitory.
|
|
+ (NSSet *)transitoryPropertyKeys;
|
|
|
|
// Returns a set of all property keys for which
|
|
// +storageBehaviorForPropertyWithKey returned MTLPropertyStoragePermanent.
|
|
+ (NSSet *)permanentPropertyKeys;
|
|
|
|
// 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
|
|
|
|
+ (void)generateAndCacheStorageBehaviors {
|
|
NSMutableSet *transitoryKeys = [NSMutableSet set];
|
|
NSMutableSet *permanentKeys = [NSMutableSet set];
|
|
|
|
for (NSString *propertyKey in self.propertyKeys) {
|
|
switch ([self storageBehaviorForPropertyWithKey:propertyKey]) {
|
|
case MTLPropertyStorageNone:
|
|
break;
|
|
|
|
case MTLPropertyStorageTransitory:
|
|
[transitoryKeys addObject:propertyKey];
|
|
break;
|
|
|
|
case MTLPropertyStoragePermanent:
|
|
[permanentKeys addObject:propertyKey];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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, MTLModelCachedTransitoryPropertyKeysKey, transitoryKeys, OBJC_ASSOCIATION_COPY);
|
|
objc_setAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey, permanentKeys, OBJC_ASSOCIATION_COPY);
|
|
}
|
|
|
|
+ (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) {
|
|
NSString *key = @(property_getName(property));
|
|
|
|
if ([self storageBehaviorForPropertyWithKey:key] != MTLPropertyStorageNone) {
|
|
[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;
|
|
}
|
|
|
|
+ (NSSet *)transitoryPropertyKeys {
|
|
NSSet *transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
|
|
|
|
if (transitoryPropertyKeys == nil) {
|
|
[self generateAndCacheStorageBehaviors];
|
|
transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
|
|
}
|
|
|
|
return transitoryPropertyKeys;
|
|
}
|
|
|
|
+ (NSSet *)permanentPropertyKeys {
|
|
NSSet *permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
|
|
|
|
if (permanentPropertyKeys == nil) {
|
|
[self generateAndCacheStorageBehaviors];
|
|
permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
|
|
}
|
|
|
|
return permanentPropertyKeys;
|
|
}
|
|
|
|
- (NSDictionary *)dictionaryValue {
|
|
NSSet *keys = [self.class.transitoryPropertyKeys setByAddingObjectsFromSet:self.class.permanentPropertyKeys];
|
|
|
|
return [self dictionaryWithValuesForKeys:keys.allObjects];
|
|
}
|
|
|
|
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey {
|
|
objc_property_t property = class_getProperty(self.class, propertyKey.UTF8String);
|
|
|
|
if (property == NULL) return MTLPropertyStorageNone;
|
|
|
|
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
|
|
@onExit {
|
|
free(attributes);
|
|
};
|
|
|
|
BOOL hasGetter = [self instancesRespondToSelector:attributes->getter];
|
|
BOOL hasSetter = [self instancesRespondToSelector:attributes->setter];
|
|
if (!attributes->dynamic && attributes->ivar == NULL && !hasGetter && !hasSetter) {
|
|
return MTLPropertyStorageNone;
|
|
} else if (attributes->readonly && attributes->ivar == NULL) {
|
|
if ([self isEqual:MTLModel.class]) {
|
|
return MTLPropertyStorageNone;
|
|
} else {
|
|
// Check superclass in case the subclass redeclares a property that
|
|
// falls through
|
|
return [self.superclass storageBehaviorForPropertyWithKey:propertyKey];
|
|
}
|
|
} else {
|
|
return MTLPropertyStoragePermanent;
|
|
}
|
|
}
|
|
|
|
#pragma mark Merging
|
|
|
|
- (void)mergeValueForKey:(NSString *)key fromModel:(NSObject<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;
|
|
}
|
|
|
|
IMP imp = [self methodForSelector:selector];
|
|
void (*function)(id, SEL, id<MTLModel>) = (__typeof__(function))imp;
|
|
function(self, selector, model);
|
|
}
|
|
|
|
- (void)mergeValuesForKeysFromModel:(id<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 {
|
|
MTLModel *copy = [[self.class allocWithZone:zone] init];
|
|
[copy setValuesForKeysWithDictionary:self.dictionaryValue];
|
|
return copy;
|
|
}
|
|
|
|
#pragma mark NSObject
|
|
|
|
- (NSString *)description {
|
|
NSDictionary *permanentProperties = [self dictionaryWithValuesForKeys:self.class.permanentPropertyKeys.allObjects];
|
|
|
|
return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, permanentProperties];
|
|
}
|
|
|
|
- (NSUInteger)hash {
|
|
NSUInteger value = 0;
|
|
|
|
for (NSString *key in self.class.permanentPropertyKeys) {
|
|
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.permanentPropertyKeys) {
|
|
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
|