MTLJSONAdapter.m
11 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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
//
// MTLJSONAdapter.m
// Mantle
//
// Created by Justin Spahr-Summers on 2013-02-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "MTLJSONAdapter.h"
#import "MTLModel.h"
#import "MTLReflection.h"
NSString * const MTLJSONAdapterErrorDomain = @"MTLJSONAdapterErrorDomain";
const NSInteger MTLJSONAdapterErrorNoClassFound = 2;
const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary = 3;
const NSInteger MTLJSONAdapterErrorInvalidJSONMapping = 4;
// An exception was thrown and caught.
const NSInteger MTLJSONAdapterErrorExceptionThrown = 1;
// Associated with the NSException that was caught.
static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapterThrownException";
@interface MTLJSONAdapter ()
// The MTLModel subclass being parsed, or the class of `model` if parsing has
// completed.
@property (nonatomic, strong, readonly) Class modelClass;
// A cached copy of the return value of +JSONKeyPathsByPropertyKey.
@property (nonatomic, copy, readonly) NSDictionary *JSONKeyPathsByPropertyKey;
// Looks up the NSValueTransformer that should be used for the given key.
//
// key - The property key to transform from or to. This argument must not be nil.
//
// Returns a transformer to use, or nil to not transform the property.
- (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
@end
@implementation MTLJSONAdapter
#pragma mark Convenience methods
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
MTLJSONAdapter *adapter = [[self alloc] initWithJSONDictionary:JSONDictionary modelClass:modelClass error:error];
return adapter.model;
}
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error {
if (JSONArray == nil || ![JSONArray isKindOfClass:NSArray.class]) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing JSON array", @""),
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%@ could not be created because an invalid JSON array was provided: %@", @""), NSStringFromClass(modelClass), JSONArray.class],
};
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
}
return nil;
}
NSMutableArray *models = [NSMutableArray arrayWithCapacity:JSONArray.count];
for (NSDictionary *JSONDictionary in JSONArray){
MTLModel *model = [self modelOfClass:modelClass fromJSONDictionary:JSONDictionary error:error];
if (model == nil) return nil;
[models addObject:model];
}
return models;
}
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model {
MTLJSONAdapter *adapter = [[self alloc] initWithModel:model];
return adapter.JSONDictionary;
}
+ (NSArray *)JSONArrayFromModels:(NSArray *)models {
NSParameterAssert(models != nil);
NSParameterAssert([models isKindOfClass:NSArray.class]);
NSMutableArray *JSONArray = [NSMutableArray arrayWithCapacity:models.count];
for (MTLModel<MTLJSONSerializing> *model in models) {
NSDictionary *JSONDictionary = [self JSONDictionaryFromModel:model];
if (JSONDictionary == nil) return nil;
[JSONArray addObject:JSONDictionary];
}
return JSONArray;
}
#pragma mark Lifecycle
- (id)init {
NSAssert(NO, @"%@ must be initialized with a JSON dictionary or model object", self.class);
return nil;
}
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error {
NSParameterAssert(modelClass != nil);
NSParameterAssert([modelClass isSubclassOfClass:MTLModel.class]);
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
if (JSONDictionary == nil || ![JSONDictionary isKindOfClass:NSDictionary.class]) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing JSON dictionary", @""),
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%@ could not be created because an invalid JSON dictionary was provided: %@", @""), NSStringFromClass(modelClass), JSONDictionary.class],
};
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
}
return nil;
}
if ([modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
modelClass = [modelClass classForParsingJSONDictionary:JSONDictionary];
if (modelClass == nil) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not parse JSON", @""),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"No model class could be found to parse the JSON dictionary.", @"")
};
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorNoClassFound userInfo:userInfo];
}
return nil;
}
NSAssert([modelClass isSubclassOfClass:MTLModel.class], @"Class %@ returned from +classForParsingJSONDictionary: is not a subclass of MTLModel", modelClass);
NSAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)], @"Class %@ returned from +classForParsingJSONDictionary: does not conform to <MTLJSONSerializing>", modelClass);
}
self = [super init];
if (self == nil) return nil;
_modelClass = modelClass;
_JSONKeyPathsByPropertyKey = [[modelClass JSONKeyPathsByPropertyKey] copy];
NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:JSONDictionary.count];
NSSet *propertyKeys = [self.modelClass propertyKeys];
for (NSString *mappedPropertyKey in self.JSONKeyPathsByPropertyKey) {
if (![propertyKeys containsObject:mappedPropertyKey]) {
NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
return nil;
}
id value = self.JSONKeyPathsByPropertyKey[mappedPropertyKey];
if (![value isKindOfClass:NSString.class] && value != NSNull.null) {
NSAssert(NO, @"%@ must either map to a JSON key path or NSNull, got: %@.",mappedPropertyKey, value);
return nil;
}
}
for (NSString *propertyKey in propertyKeys) {
NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
if (JSONKeyPath == nil) continue;
id value;
@try {
value = [JSONDictionary valueForKeyPath:JSONKeyPath];
} @catch (NSException *ex) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid JSON dictionary", nil),
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%1$@ could not be parsed because an invalid JSON dictionary was provided for key path \"%2$@\"", nil), modelClass, JSONKeyPath],
MTLJSONAdapterThrownExceptionErrorKey: ex
};
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
}
return nil;
}
if (value == nil) continue;
@try {
NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
if (transformer != nil) {
// Map NSNull -> nil for the transformer, and then back for the
// dictionary we're going to insert into.
if ([value isEqual:NSNull.null]) value = nil;
value = [transformer transformedValue:value] ?: NSNull.null;
}
dictionaryValue[propertyKey] = value;
} @catch (NSException *ex) {
NSLog(@"*** Caught exception %@ parsing JSON key path \"%@\" from: %@", ex, JSONKeyPath, JSONDictionary);
// Fail fast in Debug builds.
#if DEBUG
@throw ex;
#else
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: ex.description,
NSLocalizedFailureReasonErrorKey: ex.reason,
MTLJSONAdapterThrownExceptionErrorKey: ex
};
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorExceptionThrown userInfo:userInfo];
}
return nil;
#endif
}
}
_model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
if (_model == nil) return nil;
return self;
}
- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model {
NSParameterAssert(model != nil);
self = [super init];
if (self == nil) return nil;
_model = model;
_modelClass = model.class;
_JSONKeyPathsByPropertyKey = [[model.class JSONKeyPathsByPropertyKey] copy];
return self;
}
#pragma mark Serialization
- (NSDictionary *)JSONDictionary {
NSDictionary *dictionaryValue = self.model.dictionaryValue;
NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] initWithCapacity:dictionaryValue.count];
[dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
if (JSONKeyPath == nil) return;
NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
if ([transformer.class allowsReverseTransformation]) {
// Map NSNull -> nil for the transformer, and then back for the
// dictionaryValue we're going to insert into.
if ([value isEqual:NSNull.null]) value = nil;
value = [transformer reverseTransformedValue:value] ?: NSNull.null;
}
NSArray *keyPathComponents = [JSONKeyPath componentsSeparatedByString:@"."];
// Set up dictionaries at each step of the key path.
id obj = JSONDictionary;
for (NSString *component in keyPathComponents) {
if ([obj valueForKey:component] == nil) {
// Insert an empty mutable dictionary at this spot so that we
// can set the whole key path afterward.
[obj setValue:[NSMutableDictionary dictionary] forKey:component];
}
obj = [obj valueForKey:component];
}
[JSONDictionary setValue:value forKeyPath:JSONKeyPath];
}];
return JSONDictionary;
}
- (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
NSParameterAssert(key != nil);
SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
if ([self.modelClass respondsToSelector:selector]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self.modelClass methodSignatureForSelector:selector]];
invocation.target = self.modelClass;
invocation.selector = selector;
[invocation invoke];
__unsafe_unretained id result = nil;
[invocation getReturnValue:&result];
return result;
}
if ([self.modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
return [self.modelClass JSONTransformerForKey:key];
}
return nil;
}
- (NSString *)JSONKeyPathForPropertyKey:(NSString *)key {
NSParameterAssert(key != nil);
id JSONKeyPath = self.JSONKeyPathsByPropertyKey[key];
if ([JSONKeyPath isEqual:NSNull.null]) return nil;
if (JSONKeyPath == nil) {
return key;
} else {
return JSONKeyPath;
}
}
@end
@implementation MTLJSONAdapter (Deprecated)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary {
return [self modelOfClass:modelClass fromJSONDictionary:JSONDictionary error:NULL];
}
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass {
return [self initWithJSONDictionary:JSONDictionary modelClass:modelClass error:NULL];
}
#pragma clang diagnostic pop
@end