diff --git a/Classes/JSONAPIResourceCollection.h b/Classes/JSONAPIResourceCollection.h new file mode 100644 index 0000000..38a391f --- /dev/null +++ b/Classes/JSONAPIResourceCollection.h @@ -0,0 +1,32 @@ +// +// JSONAPIResourceCollection.h +// JSONAPI +// +// Created by Julian Krumow on 13.01.16. +// Copyright © 2016 Josh Holtz. All rights reserved. +// + +#import + +@interface JSONAPIResourceCollection : NSObject + +@property (nonatomic) NSMutableArray *resources; +@property (nonatomic) NSString *selfLink; +@property (nonatomic) NSString *relatedLink; + +- (instancetype)initWithArray:(NSArray *)array; + +- (id)firstObject; +- (id)lastObject; + +- (NSUInteger)count; +- (void)addObject:(id)object; + +- (id)objectAtIndexedSubscript:(NSUInteger)idx; +- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx; + +- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block; +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])stackbuf + count:(NSUInteger)len; +@end diff --git a/Classes/JSONAPIResourceCollection.m b/Classes/JSONAPIResourceCollection.m new file mode 100644 index 0000000..249dc3b --- /dev/null +++ b/Classes/JSONAPIResourceCollection.m @@ -0,0 +1,84 @@ +// +// JSONAPIResourceCollection.m +// JSONAPI +// +// Created by Julian Krumow on 13.01.16. +// Copyright © 2016 Josh Holtz. All rights reserved. +// + +#import "JSONAPIResourceCollection.h" + +@interface JSONAPIResourceCollection () + +@end + +@implementation JSONAPIResourceCollection + +- (instancetype)initWithArray:(NSArray *)array +{ + self = [super init]; + if (self) { + _resources = [[NSMutableArray alloc] initWithArray:array]; + } + return self; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _resources = [[NSMutableArray alloc] initWithCapacity:0]; + } + return self; +} + +- (id)mutableCopyWithZone:(NSZone *)zone +{ + JSONAPIResourceCollection *copy = [JSONAPIResourceCollection new]; + copy.selfLink = self.selfLink.copy; + copy.relatedLink = self.relatedLink.copy; + copy.resources = self.resources.mutableCopy; + return copy; +} + +- (id)firstObject +{ + return self.resources.firstObject; +} +- (id)lastObject +{ + return self.resources.lastObject; +} + +- (NSUInteger)count +{ + return self.resources.count; +} + +- (void)addObject:(id)object +{ + [self.resources addObject:object]; +} + +- (id)objectAtIndexedSubscript:(NSUInteger)idx +{ + return self.resources[idx]; +} +- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx +{ + self.resources[idx] = obj; +} + +- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block +{ + [self.resources enumerateObjectsUsingBlock:block]; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])stackbuf + count:(NSUInteger)len +{ + return [self.resources countByEnumeratingWithState:state objects:stackbuf count:len]; +} + +@end diff --git a/Classes/JSONAPIResourceDescriptor.h b/Classes/JSONAPIResourceDescriptor.h index 840842f..51c93d4 100644 --- a/Classes/JSONAPIResourceDescriptor.h +++ b/Classes/JSONAPIResourceDescriptor.h @@ -29,7 +29,6 @@ * This is required for any model resource. */ @property (strong) NSString *idProperty; - @property (strong) NSString *selfLinkProperty; /** diff --git a/Classes/JSONAPIResourceParser.h b/Classes/JSONAPIResourceParser.h index b236c0c..ebdb704 100644 --- a/Classes/JSONAPIResourceParser.h +++ b/Classes/JSONAPIResourceParser.h @@ -8,6 +8,7 @@ #import #import "JSONAPIResource.h" +#import "JSONAPIResourceCollection.h" @class JSONAPI; diff --git a/Classes/JSONAPIResourceParser.m b/Classes/JSONAPIResourceParser.m index c0f6a47..fee3063 100644 --- a/Classes/JSONAPIResourceParser.m +++ b/Classes/JSONAPIResourceParser.m @@ -97,44 +97,59 @@ + (NSDictionary*)dictionaryFor:(NSObject *)resource { [dictionary setValue:ID forKey:@"id"]; } } - + NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init]; // Loops through all keys to map to properties NSDictionary *properties = [descriptor properties]; for (NSString *key in properties) { JSONAPIPropertyDescriptor *property = [properties objectForKey:key]; - + id value = [resource valueForKey:key]; if (value) { - if ([value isKindOfClass:[NSArray class]]) { - NSArray *valueArray = value; - if (valueArray.count > 0) { - NSMutableArray *dictionaryArray = [[NSMutableArray alloc] initWithCapacity:valueArray.count]; + if ([value isKindOfClass:[JSONAPIResourceCollection class]]) { + JSONAPIResourceCollection *collection = (JSONAPIResourceCollection *)value; + + if (linkage == nil) { + linkage = [[NSMutableDictionary alloc] init]; + } + NSMutableDictionary *collectionDictionary = [[NSMutableDictionary alloc] init]; + + if (collection.selfLink || collection.relatedLink) { + NSMutableDictionary *links = [[NSMutableDictionary alloc] init]; + if (collection.selfLink) { + links[@"self"] = collection.selfLink.mutableCopy; + } + if (collection.relatedLink) { + links[@"related"] = collection.relatedLink.mutableCopy; + } + collectionDictionary[@"links"] = links; + } + + [linkage setValue:collectionDictionary forKey:[property jsonName]]; + + if (collection.count > 0) { + NSMutableArray *dataArray = [[NSMutableArray alloc] initWithCapacity:collection.count]; - if ([property resourceType] || [((NSArray *)value).firstObject conformsToProtocol:@protocol(JSONAPIResource)]) { - if (linkage == nil) { - linkage = [[NSMutableDictionary alloc] init]; - } - - for (id valueElement in valueArray) { - [dictionaryArray addObject:[self link:valueElement from:resource withKey:[property jsonName]]]; + if ([property resourceType] || [collection.firstObject conformsToProtocol:@protocol(JSONAPIResource)]) { + + for (id element in collection) { + [dataArray addObject:[self link:element from:resource withKey:[property jsonName]]]; } + collectionDictionary[@"data"] = dataArray; - NSDictionary *dataDictionary = @{@"data" : dictionaryArray}; - [linkage setValue:dataDictionary forKey:[property jsonName]]; } else { NSFormatter *format = [property formatter]; - for (id valueElement in valueArray) { + for (id element in collection) { if (format) { - [dictionaryArray addObject:[format stringForObjectValue:valueElement]]; + [dataArray addObject:[format stringForObjectValue:element]]; } else { - [dictionaryArray addObject:valueElement]; + [dataArray addObject:element]; } } - [attributes setValue:dictionaryArray forKey:[property jsonName]]; + [attributes setValue:dataArray forKey:[property jsonName]]; } } } else { @@ -164,24 +179,24 @@ + (NSDictionary*)dictionaryFor:(NSObject *)resource { if (linkage) { [dictionary setValue:linkage forKey:@"relationships"]; } - - // TODO: Need to also add in all other links - if (resource.selfLink) { - dictionary[@"links"] = @{ @"self": resource.selfLink }; - } - + + // TODO: Need to also add in all other links + if (resource.selfLink) { + dictionary[@"links"] = @{ @"self": resource.selfLink }; + } + return dictionary; } + (void)set:(NSObject *)resource withDictionary:dictionary { NSString *error; - + JSONAPIResourceDescriptor *descriptor = [[resource class] descriptor]; NSDictionary *relationships = [dictionary objectForKey:@"relationships"]; NSDictionary *attributes = [dictionary objectForKey:@"attributes"]; NSDictionary *links = [dictionary objectForKey:@"links"]; - + id ID = [dictionary objectForKey:@"id"]; NSFormatter *format = [descriptor idFormatter]; if (format) { @@ -197,7 +212,7 @@ + (void)set:(NSObject *)resource withDictionary:dictionary { NSString *selfLink = links[@"self"]; [resource setValue:selfLink forKey:descriptor.selfLinkProperty]; } - + // Loops through all keys to map to properties NSDictionary *properties = [descriptor properties]; for (NSString *key in properties) { @@ -260,12 +275,19 @@ + (void)set:(NSObject *)resource withDictionary:dictionary { + (id)jsonAPILink:(NSDictionary*)dictionary { id linkage = dictionary[@"data"]; if ([linkage isKindOfClass:[NSArray class]]) { - NSMutableArray *linkArray = [[NSMutableArray alloc] initWithCapacity:[linkage count]]; + + JSONAPIResourceCollection *collection = [JSONAPIResourceCollection new]; + + if (dictionary[@"links"]) { + collection.selfLink = dictionary[@"links"][@"self"]; + collection.relatedLink = dictionary[@"links"][@"related"]; + } + for (NSDictionary *linkElement in linkage) { - [linkArray addObject:[JSONAPIResourceParser parseResource:linkElement]]; + [collection addObject:[JSONAPIResourceParser parseResource:linkElement]]; } - return linkArray; + return collection; } else { return [JSONAPIResourceParser parseResource:linkage]; @@ -289,17 +311,18 @@ + (void)link:(NSObject *)resource withIncluded:(JSONAPI*)jsonAP Class valueClass = nil; if (propertyDescriptor.resourceType) { valueClass = propertyDescriptor.resourceType; - } else if ([value conformsToProtocol:@protocol(JSONAPIResource)] || [value isKindOfClass:[NSArray class]]) { + } else if ([value conformsToProtocol:@protocol(JSONAPIResource)] || [value isKindOfClass:[JSONAPIResourceCollection class]]) { valueClass = [value class]; } // ordinary attribute if (valueClass == nil) { continue; - // has many - } else if ([value isKindOfClass:[NSArray class]]) { - NSMutableArray *matched = [value mutableCopy]; - [value enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + // has many + } else if ([value isKindOfClass:[JSONAPIResourceCollection class]]) { + JSONAPIResourceCollection *collection = (JSONAPIResourceCollection *)value; + JSONAPIResourceCollection *matched = [value mutableCopy]; + [collection enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if ([obj conformsToProtocol:@protocol(JSONAPIResource)]) { NSObject *res = obj; id includedValue = included[[[res.class descriptor] type]]; @@ -311,9 +334,9 @@ + (void)link:(NSObject *)resource withIncluded:(JSONAPI*)jsonAP } } }]; - + [resource setValue:matched forKey:key]; - // has one + // has one } else if (value != nil) { if ([value conformsToProtocol:@protocol(JSONAPIResource)]) { id res = value; @@ -340,7 +363,7 @@ + (NSArray*)relatedResourcesFor:(NSObject *)resource { JSONAPIPropertyDescriptor *property = [properties objectForKey:key]; if (property.resourceType) { id value = [resource valueForKey:key]; - if ([value isKindOfClass:[NSArray class]]) { + if ([value isKindOfClass:[JSONAPIResourceCollection class]]) { [related addObjectsFromArray:value]; } else { [related addObject:value]; @@ -371,7 +394,7 @@ + (NSDictionary*)link:(NSObject *)resource from:(NSObject publisher; -@property (nonatomic, strong) NSArray *attachments; +@property (nonatomic, strong) JSONAPIResourceCollection *attachments; @end diff --git a/Project/JSONAPITests/JSONAPITests.m b/Project/JSONAPITests/JSONAPITests.m index a2807f7..6521166 100644 --- a/Project/JSONAPITests/JSONAPITests.m +++ b/Project/JSONAPITests/JSONAPITests.m @@ -93,6 +93,9 @@ - (void)testDataArticleAuthorAndComments { XCTAssertNotNil(article.author, @"Article's author should not be nil"); XCTAssertNotNil(article.comments, @"Article's comments should not be nil"); XCTAssertEqual(article.comments.count, 2, @"Article should contain 2 comments"); + XCTAssertTrue([article.comments.selfLink isEqualToString:@"http://example.com/articles/1/relationships/comments"], @"Comments selfLink should be 'http://example.com/articles/1/relationships/comments'"); + XCTAssertTrue([article.comments.relatedLink isEqualToString:@"http://example.com/articles/1/comments"], @"Comments related should be 'http://example.com/articles/1/comments'"); + XCTAssertEqualObjects(article.author.firstName, @"Dan", @"Article's author firstname should be 'Dan'"); XCTAssertEqualObjects(firstComment.text, @"First!", @"Article's first comment should be 'First!'"); XCTAssertEqualObjects(firstComment.author.firstName, @"Dan", @"Article's first comment author should be 'Dan'"); @@ -168,19 +171,25 @@ - (void)testSerializeComplex { newArticle.title = @"Title"; newArticle.author = newAuthor; newArticle.date = [NSDate date]; - newArticle.comments = [[NSArray alloc] initWithObjects:firstComment, secondComment, nil]; + + newArticle.selfLink = @"http://example.com/articles/1"; + newArticle.comments = [[JSONAPIResourceCollection alloc] initWithArray:@[firstComment, secondComment]]; + newArticle.comments.selfLink = @"http://example.com/articles/1/relationships/comments"; + newArticle.comments.relatedLink = @"http://example.com/articles/1/comments"; NSDictionary *json = [JSONAPIResourceParser dictionaryFor:newArticle]; XCTAssertEqualObjects(json[@"type"], @"articles", @"Did not create Article!"); + XCTAssertEqualObjects(json[@"links"][@"self"], @"http://example.com/articles/1", @"Self link should be 'http://example.com/articles/1'!"); XCTAssertNotNil(json[@"relationships"], @"Did not create links!"); XCTAssertNotNil(json[@"relationships"][@"author"], @"Did not create links!"); XCTAssertNotNil(json[@"relationships"][@"author"][@"data"], @"Did not create links!"); XCTAssertEqualObjects(json[@"relationships"][@"author"][@"data"][@"id"], newAuthor.ID, @"Wrong link ID!."); XCTAssertNil(json[@"relationships"][@"author"][@"first-name"], @"Bad link!"); - XCTAssertNotNil(json[@"relationships"][@"comments"], @"Did not create links!"); XCTAssertTrue([json[@"relationships"][@"comments"][@"data"] isKindOfClass:[NSArray class]], @"Comments data should be array!."); XCTAssertEqual([json[@"relationships"][@"comments"][@"data"] count], 2, @"Comments should have 2 elements!."); + XCTAssertEqualObjects(json[@"relationships"][@"comments"][@"links"][@"self"], @"http://example.com/articles/1/relationships/comments", @"Self link should be 'http://example.com/articles/1/relationships/comments'!"); + XCTAssertEqualObjects(json[@"relationships"][@"comments"][@"links"][@"related"], @"http://example.com/articles/1/comments", @"Related link should be 'http://example.com/articles/1/comments'!"); } - (void)testCreate { diff --git a/Project/JSONAPITests/main_example.json b/Project/JSONAPITests/main_example.json index 9339969..f653b2e 100644 --- a/Project/JSONAPITests/main_example.json +++ b/Project/JSONAPITests/main_example.json @@ -1,83 +1,104 @@ { - "meta": { - "hehe": "hoho" - }, - "links": { - "self": "http://example.com/articles", - "next": "http://example.com/articles?page[offset]=2", - "last": "http://example.com/articles?page[offset]=10" - }, - "data": [{ - "type": "articles", - "id": "1", - "attributes": { - "title": "JSON API paints my bikeshed!", - "versions": [ - "2015-09-01T12:15:00.000Z", - "2015-08-01T06:15:00.000Z" - ] - }, - "relationships": { - "author": { - "links": { - "self": "http://example.com/articles/1/relationships/author", - "related": "http://example.com/articles/1/author" - }, - "data": { "type": "people", "id": "9" } - }, - "comments": { - "links": { - "self": "http://example.com/articles/1/relationships/comments", - "related": "http://example.com/articles/1/comments" - }, - "data": [ - { "type": "comments", "id": "5" }, - { "type": "comments", "id": "12" } - ] - } - }, - "links": { - "self": "http://example.com/articles/1" - } - }], - "included": [{ - "type": "people", - "id": "9", - "attributes": { - "first-name": "Dan", - "last-name": "Gebhardt", - "twitter": "dgeb" - }, - "links": { - "self": "http://example.com/people/9" - } - }, { - "type": "comments", - "id": "5", - "attributes": { - "body": "First!" - }, - "relationships": { - "author": { - "data": { "type": "people", "id": "9" } - } - }, - "links": { - "self": "http://example.com/comments/5" - } - }, { - "type": "comments", - "id": "12", - "attributes": { - "body": "I like XML better" - }, - "relationships": { - "author": { - "data": { "type": "people", "id": "9" } - } - }, - "links": { - "self": "http://example.com/comments/12" - } - }] -} \ No newline at end of file + "meta":{ + "hehe":"hoho" + }, + "links":{ + "self":"http://example.com/articles", + "next":"http://example.com/articles?page[offset]=2", + "last":"http://example.com/articles?page[offset]=10" + }, + "data":[ + { + "type":"articles", + "id":"1", + "attributes":{ + "title":"JSON API paints my bikeshed!", + "versions":[ + "2015-09-01T12:15:00.000Z", + "2015-08-01T06:15:00.000Z" + ] + }, + "relationships":{ + "author":{ + "links":{ + "self":"http://example.com/articles/1/relationships/author", + "related":"http://example.com/articles/1/author" + }, + "data":{ + "type":"people", + "id":"9" + } + }, + "comments":{ + "links":{ + "self":"http://example.com/articles/1/relationships/comments", + "related":"http://example.com/articles/1/comments" + }, + "data":[ + { + "type":"comments", + "id":"5" + }, + { + "type":"comments", + "id":"12" + } + ] + } + }, + "links":{ + "self":"http://example.com/articles/1" + } + } + ], + "included":[ + { + "type":"people", + "id":"9", + "attributes":{ + "first-name":"Dan", + "last-name":"Gebhardt", + "twitter":"dgeb" + }, + "links":{ + "self":"http://example.com/people/9" + } + }, + { + "type":"comments", + "id":"5", + "attributes":{ + "body":"First!" + }, + "relationships":{ + "author":{ + "data":{ + "type":"people", + "id":"9" + } + } + }, + "links":{ + "self":"http://example.com/comments/5" + } + }, + { + "type":"comments", + "id":"12", + "attributes":{ + "body":"I like XML better" + }, + "relationships":{ + "author":{ + "data":{ + "type":"people", + "id":"9" + } + } + }, + "links":{ + "self":"http://example.com/comments/12" + } + } + ] +} diff --git a/README.md b/README.md index 093f715..1b8422a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ For some full examples on how to use everything, please see the tests - https:// Version | Changes --- | --- -**1.0.6** | Improved resource parsing and added parsing of `selfLinks` (https://github.com/joshdholtz/jsonapi-ios/pull/35). Thanks to [ artcom](https://github.com/ artcom) for helping! Also removed the need to define `setIdProperty` and `setSelfLinkProperty` in every resource (automatically mapped in the init of `JSONAPIResourceDescriptor`) +**1.0.6** | Improved resource parsing and added parsing of `selfLinks` (https://github.com/joshdholtz/jsonapi-ios/pull/35). Thanks to [artcom](https://github.com/artcom) for helping! Also removed the need to define `setIdProperty` and `setSelfLinkProperty` in every resource (automatically mapped in the init of `JSONAPIResourceDescriptor`) **1.0.5** | Fix 1-to-many relationships serialization according to JSON API v1.0 (https://github.com/joshdholtz/jsonapi-ios/pull/34). Thanks to [RafaelKayumov](https://github.com/RafaelKayumov) for helping! **1.0.4** | Add support for empty to-one relationship according to JSON API v1.0 (https://github.com/joshdholtz/jsonapi-ios/pull/33). Thanks to [RafaelKayumov](https://github.com/RafaelKayumov) for helping! **1.0.3** | Add ability to map different types of objects (https://github.com/joshdholtz/jsonapi-ios/pull/32). Thanks to [ealeksandrov](https://github.com/ealeksandrov) for helping!