Skip to content

Instantly share code, notes, and snippets.

@jonsterling
Created May 13, 2012 02:55
Show Gist options
  • Save jonsterling/2672219 to your computer and use it in GitHub Desktop.
Save jonsterling/2672219 to your computer and use it in GitHub Desktop.
Safe keypaths without macros!
NSString *safe = self.keys.url.port.stringValue;
NSString *unsafe = @"url.port.stringValue";
assert([safe isEqualToString:unsafe]);
@interface NSObject (SafeKeypaths)
+ (instancetype)keys;
- (instancetype)keys;
@end
@interface JSKeypathRecorder : NSString {
NSMutableString *_path;
}
- (void)appendPathComponent:(NSString *)component;
@end
@implementation NSObject (SafeKeypaths)
+ (instancetype)keys { return [JSKeypathRecorder new]; }
- (instancetype)keys { return self.class.keys; }
@end
@implementation JSKeypathRecorder {
NSMutableString *_path;
}
- (id)init {
if ((self = [super init]))
_path = [NSMutableString new];
return self;
}
#pragma mark - NSString Primitives
- (NSUInteger)length {
return [_path length];
}
- (unichar)characterAtIndex:(NSUInteger)index {
return [_path characterAtIndex:index];
}
#pragma mark - Auxiliary
- (void)appendPathComponent:(NSString *)component {
[_path appendFormat:@"%@%@", _path.length > 0 ? @"." : @"",component];
}
#pragma mark - Invocation Recording
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[self appendPathComponent:NSStringFromSelector(anInvocation.selector)];
anInvocation.returnValue = (id __strong*)(&self);
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (signature)
return signature;
// if we don't already have a signature, let's use one that is the
// same as all the messages we intend to accept
return [NSNumber instanceMethodSignatureForSelector:@selector(stringValue)];
}
@end
@jonsterling
Copy link
Author

There are some limitations to this approach. You can decide whether or not it's worth it.

Return values may have to be casted:

// will fail, because NSString* != NSNumber*
NSString *path1 = NSURL.keys.port; 

// will succeed
NSString *path2 = (id)NSURL.keys.port;

This means that path components returning primitives will never work, despite the fact that at runtime, they are boxed by KVC:

// will fail, because NSString* != NSUInteger
NSString *path3 = NSURL.keys.port.stringValue.length;

// will also fail, because there is no safe cast NSUInteger => NSString*
NSString *path4 = (id)NSURL.keys.port.stringValue.length;

@jonsterling
Copy link
Author

By the way, this also makes your keypaths refactor-safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment