-
Notifications
You must be signed in to change notification settings - Fork 223
Expand file tree
/
Copy pathJSObjectionUtils.m
More file actions
197 lines (167 loc) · 9.49 KB
/
JSObjectionUtils.m
File metadata and controls
197 lines (167 loc) · 9.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#import <objc/runtime.h>
#import "JSObjectionUtils.h"
#import "JSObjectionInjector.h"
#import "JSObjection.h"
#import "NSObject+Objection.h"
static NSString *const JSObjectionException = @"JSObjectionException";
NSString *const JSObjectionInitializerKey = @"initializer";
NSString *const JSObjectionDefaultArgumentsKey = @"arguments";
static JSObjectionPropertyInfo FindClassOrProtocolForProperty(objc_property_t property) {
NSString *attributes = [NSString stringWithCString: property_getAttributes(property) encoding: NSASCIIStringEncoding];
NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSASCIIStringEncoding];
NSRange startRange = [attributes rangeOfString:@"T@\""];
if (startRange.location == NSNotFound) {
@throw [NSException exceptionWithName:JSObjectionException reason:[NSString stringWithFormat:@"Unable to determine class type for property declaration: '%@'", propertyName] userInfo:nil];
}
NSString *startOfClassName = [attributes substringFromIndex:startRange.length];
NSRange endRange = [startOfClassName rangeOfString:@"\""];
if (endRange.location == NSNotFound) {
@throw [NSException exceptionWithName:JSObjectionException reason:[NSString stringWithFormat:@"Unable to determine class type for property declaration: '%@'", propertyName] userInfo:nil];
}
NSString *classOrProtocolName = [startOfClassName substringToIndex:endRange.location];
id classOrProtocol = nil;
JSObjectionPropertyInfo propertyInfo;
if ([classOrProtocolName hasPrefix:@"<"] && [classOrProtocolName hasSuffix:@">"]) {
classOrProtocolName = [classOrProtocolName stringByReplacingOccurrencesOfString:@"<" withString:@""];
classOrProtocolName = [classOrProtocolName stringByReplacingOccurrencesOfString:@">" withString:@""];
classOrProtocol = objc_getProtocol([classOrProtocolName UTF8String]);
propertyInfo.type = JSObjectionTypeProtocol;
} else {
if ([classOrProtocolName hasSuffix:@">"]) {
classOrProtocolName = [classOrProtocolName substringToIndex:[classOrProtocolName rangeOfString:@"<"].location];
}
classOrProtocol = NSClassFromString(classOrProtocolName);
propertyInfo.type = JSObjectionTypeClass;
}
if(!classOrProtocol) {
@throw [NSException exceptionWithName:JSObjectionException reason:[NSString stringWithFormat:@"Unable get class for name '%@' for property '%@'", classOrProtocolName, propertyName] userInfo:nil];
}
propertyInfo.value = classOrProtocol;
return propertyInfo;
}
static NSSet* BuildDependenciesForClass(Class klass, NSSet *requirements) {
Class superClass = class_getSuperclass([klass class]);
if([superClass respondsToSelector:@selector(objectionRequires)]) {
NSSet *parentsRequirements = [superClass objectionRequires];
NSMutableSet *dependencies = [NSMutableSet setWithSet:parentsRequirements];
[dependencies unionSet:requirements];
requirements = dependencies;
}
return requirements;
}
static NSDictionary* BuildNamedDependenciesForClass(Class klass, NSDictionary *namedRequirements) {
Class superClass = class_getSuperclass([klass class]);
if([superClass respondsToSelector:@selector(objectionRequiresNames)]) {
NSDictionary *parentsNamedRequirements = [superClass objectionRequiresNames];
NSMutableDictionary *namedDependencies = [NSMutableDictionary dictionaryWithDictionary:parentsNamedRequirements];
[namedDependencies addEntriesFromDictionary:namedRequirements];
namedRequirements = namedDependencies;
}
return namedRequirements;
}
static NSDictionary* BuildInitializer(SEL selector, NSArray *defaultArguments) {
return [NSDictionary dictionaryWithObjectsAndKeys:
NSStringFromSelector(selector), JSObjectionInitializerKey,
defaultArguments, JSObjectionDefaultArgumentsKey
, nil];
}
static NSArray* TransformVariadicArgsToArray(va_list va_arguments) {
NSMutableArray *arguments = [NSMutableArray array];
id object;
while ((object = va_arg( va_arguments, id ))) {
[arguments addObject:object];
}
return [arguments copy];
}
static objc_property_t GetProperty(Class klass, NSString *propertyName) {
objc_property_t property = class_getProperty(klass, (const char *)[propertyName UTF8String]);
if (property == NULL) {
@throw [NSException exceptionWithName:JSObjectionException reason:[NSString stringWithFormat:@"Unable to find property declaration: '%@' for class '%@'", propertyName, NSStringFromClass(klass)] userInfo:nil];
}
return property;
}
static id BuildObjectWithInitializer(Class klass, SEL initializer, NSArray *arguments) {
NSMethodSignature *signature = [klass methodSignatureForSelector:initializer];
__autoreleasing id instance = nil;
BOOL isClassMethod = signature != nil && initializer != @selector(init);
if (!isClassMethod) {
instance = [klass alloc];
signature = [klass instanceMethodSignatureForSelector:initializer];
}
if (signature) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:isClassMethod ? klass : instance];
[invocation setSelector:initializer];
for (int i = 0; i < arguments.count; i++) {
__unsafe_unretained id argument = [arguments objectAtIndex:i];
[invocation setArgument:&argument atIndex:i + 2];
}
[invocation invoke];
[invocation getReturnValue:&instance];
return instance;
} else {
@throw [NSException exceptionWithName:JSObjectionException reason:[NSString stringWithFormat:@"Could not find initializer '%@' on %@", NSStringFromSelector(initializer), NSStringFromClass(klass)] userInfo:nil];
}
return nil;
}
static void _getPropertyInfo(Class klass, NSString *propertyName, JSObjectionPropertyInfo *propertyInfo, id *desiredClassOrProtocol) {
(*propertyInfo) = [JSObjection propertyForClass:klass andProperty:propertyName];
(*desiredClassOrProtocol) = propertyInfo->value;
// Ensure that the class is initialized before attempting to retrieve it.
// Using +load would force all registered classes to be initialized so we are
// lazily initializing them.
if ((*propertyInfo).type == JSObjectionTypeClass) {
[*desiredClassOrProtocol class];
}
}
static void _validateObjectReturnedFromInjector(id *theObject, JSObjectionPropertyInfo propertyInfo, id desiredClassOrProtocol, NSString *propertyName) {
if(*theObject == nil && propertyInfo.type == JSObjectionTypeProtocol) {
@throw [NSException exceptionWithName:@"JSObjectionException"
reason:[NSString stringWithFormat:@"Cannot find an instance that is bound to the protocol '%@' to assign to the property '%@'", NSStringFromProtocol(desiredClassOrProtocol), propertyName]
userInfo:nil];
} else if (*theObject == nil) {
*theObject = [NSNull null];
}
}
static void InjectDependenciesIntoProperties(JSObjectionInjector *injector, Class klass, id object) {
if ([klass respondsToSelector:@selector(objectionRequires)]) {
NSSet *properties = [klass objectionRequires];
NSMutableDictionary *propertiesDictionary = [NSMutableDictionary dictionaryWithCapacity:properties.count];
for (NSString *propertyName in properties) {
JSObjectionPropertyInfo propertyInfo;
id desiredClassOrProtocol;
_getPropertyInfo(klass, propertyName, &propertyInfo, &desiredClassOrProtocol);
id theObject = [injector getObject:desiredClassOrProtocol];
_validateObjectReturnedFromInjector(&theObject, propertyInfo, desiredClassOrProtocol, propertyName);
[propertiesDictionary setObject:theObject forKey:propertyName];
}
[object setValuesForKeysWithDictionary:propertiesDictionary];
}
if ([klass respondsToSelector:@selector(objectionRequiresNames)]) {
NSDictionary *namedProperties = [klass objectionRequiresNames];
NSMutableDictionary *propertiesDictionary = [NSMutableDictionary dictionaryWithCapacity:namedProperties.count];
for (NSString *namedPropertyKey in [namedProperties allKeys]) {
NSString* propertyName = [namedProperties valueForKey:namedPropertyKey];
JSObjectionPropertyInfo propertyInfo;
id desiredClassOrProtocol;
_getPropertyInfo(klass, propertyName, &propertyInfo, &desiredClassOrProtocol);
id theObject = [injector getObject:desiredClassOrProtocol named:namedPropertyKey];
_validateObjectReturnedFromInjector(&theObject, propertyInfo, desiredClassOrProtocol, propertyName);
[propertiesDictionary setObject:theObject forKey:propertyName];
}
[object setValuesForKeysWithDictionary:propertiesDictionary];
}
if ([object respondsToSelector:@selector(awakeFromObjection)]) {
[object awakeFromObjection];
}
}
const struct JSObjectionUtils JSObjectionUtils = {
.findClassOrProtocolForProperty = FindClassOrProtocolForProperty,
.propertyForClass = GetProperty,
.buildDependenciesForClass = BuildDependenciesForClass,
.buildNamedDependenciesForClass = BuildNamedDependenciesForClass,
.buildInitializer = BuildInitializer,
.transformVariadicArgsToArray = TransformVariadicArgsToArray,
.buildObjectWithInitializer = BuildObjectWithInitializer,
.injectDependenciesIntoProperties = InjectDependenciesIntoProperties
};