diff --git a/Common/SLAdapter.h b/Common/SLAdapter.h index 74d7d3a..9298244 100644 --- a/Common/SLAdapter.h +++ b/Common/SLAdapter.h @@ -34,7 +34,7 @@ typedef NS_ENUM(NSUInteger, SLHTTPMethodType) @property (strong, nonatomic, setter=setEmail:) NSString *userEmail; @property (strong, nonatomic, setter=setPassword:) NSString *userPassword; @property (strong, nonatomic, setter=setOrganization:) NSString *userOrganization; - +@property (strong, nonatomic) SLSerializer *serializer; /** SHA-1 encoding of a plain text string. @@ -100,7 +100,7 @@ typedef NS_ENUM(NSUInteger, SLHTTPMethodType) /** Serializes the record and send it to the server. */ -- (PMKPromise *) createRecord:(Class)modelClass withId:(SLNid)nid withStore:(SLStore *)store; +- (PMKPromise *) createRecord:(SLModel *)record withStore:(SLStore *)store; /* Serializes the record update and send it to the server. */ @@ -128,7 +128,7 @@ typedef NS_ENUM(NSUInteger, SLHTTPMethodType) /** Proxies to the serializer's serialize method. */ -- (NSDictionary *) serialize:(NSDictionary *)options; +- (NSDictionary *) serialize:(SLModel *)record withOptions:(NSDictionary *)options; /** Builds a URL for a given type and optional ID. diff --git a/Common/SLAdapter.m b/Common/SLAdapter.m index 7e5a009..035c9e3 100644 --- a/Common/SLAdapter.m +++ b/Common/SLAdapter.m @@ -15,7 +15,7 @@ #import "SLObjectIdTransform.h" @interface SLAdapter () { - + } @property (strong, nonatomic) AFHTTPRequestOperationManager* httpManager; @@ -23,7 +23,7 @@ @interface SLAdapter () { @end @implementation SLAdapter -@synthesize userEmail = _userEmail, userPassword = _userPassword, userOrganization = _userOrganization, host, httpManager; +@synthesize userEmail = _userEmail, userPassword = _userPassword, userOrganization = _userOrganization, host, httpManager, serializer; - (instancetype) init { @@ -35,6 +35,7 @@ - (instancetype) init _userPassword = nil; _userOrganization = nil; host = nil; + serializer = [[SLSerializer alloc] init]; //httpManager = [AFHTTPRequestOperationManager manager]; httpManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:nil]; httpManager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"application/json"]; @@ -290,7 +291,6 @@ - (PMKPromise *) authenticateWithUserEmail:(NSString *)theEmail NSDictionary *response = (NSDictionary *)responseObject; // - SLSerializer *serializer = [[SLSerializer alloc] init]; NSDictionary *serialized = [serializer extractSingle:[SLUser class] withPayload:response withStore:[SLStore sharedStore]]; fulfiller(PMKManifold(serialized, operation)); @@ -312,6 +312,13 @@ - (PMKPromise *) authenticateWithUserEmail:(NSString *)theEmail }]; } +- (PMKPromise *) createRecord:(SLModel *)record withStore:(SLStore *)store; +{ + NSDictionary *options = @{}; + NSDictionary *data = [self serialize:record withOptions:options]; + NSString *path = [NSString stringWithFormat:@"%@/", [[record class] type]]; + return [self performRequestWithMethod:SLHTTPMethodPOST withPath:path withParameters:data]; +} - (PMKPromise *) findAll:(Class)modelClass withStore:(SLStore *)store { @@ -324,7 +331,7 @@ - (PMKPromise *) findMany:(Class)modelClass withIds:(NSArray *)ids withStore:(SL NSLog(@"ids: %@", ids); // Map IDs to ObjectId NSMutableArray *nids = [NSMutableArray array]; - for (NSDictionary *i in ids) + for (NSString *i in ids) { NSDictionary *nid = [SLObjectIdTransform serialize:i]; [nids addObject:nid]; @@ -352,5 +359,10 @@ - (PMKPromise *) findQuery:(Class)modelClass withQuery:(NSDictionary *)query wit }]; } +- (NSDictionary *) serialize:(SLModel *)record withOptions:(NSDictionary *)options +{ + return [serializer serialize:record withOptions:options]; +} + @end diff --git a/Common/SLModel.h b/Common/SLModel.h index 63cc10b..5fd4e83 100644 --- a/Common/SLModel.h +++ b/Common/SLModel.h @@ -82,6 +82,8 @@ */ - (instancetype) initInContext:(NSManagedObjectContext *)context; ++ (instancetype) initInContext:(NSManagedObjectContext *)context; + /** Returns an object initialized with the specific `SLNid` nid. @@ -138,20 +140,14 @@ /** - Return the {type} of this node. + Return the {type} of this model. */ - (NSString *) type; /** Returns the current value of the `SLValue` of the node's data with the key `attr`. */ -- (id) get:(NSString *)attr; - -/** - Update a single attribute. Updating a node sets it's internal boolean, - {isSaved}, false. - */ -- (void) update:(NSString *)attr value:(id)value; +- (PMKPromise *) get:(NSString *)attr; /** @@ -245,14 +241,19 @@ - (NSDictionary *) serialize:(NSDictionary *)options; /** - Get all model Attributes by name. - */ -+ (NSDictionary *) attributesByName; - -/** - Get all model Attributes by name. + Push some data for a given type into the store. + + This method expects normalized data: + + The ID is a key named id (an ID is mandatory) + The names of attributes are the ones you used in your model's DS.attrs. + Your relationships must be: + represented as IDs or Arrays of IDs + represented as model instances + represented as URLs, under the links key + */ -+ (NSDictionary *) relationshipsByName; +- (SLModel *) pushWithData:(NSDictionary *)datum; @end diff --git a/Common/SLModel.m b/Common/SLModel.m index 9e7e45a..ab55f19 100644 --- a/Common/SLModel.m +++ b/Common/SLModel.m @@ -54,6 +54,12 @@ - (instancetype) initInContext:(NSManagedObjectContext *)context return self; } + ++ (instancetype) initInContext:(NSManagedObjectContext *)context +{ + return [[self alloc] initInContext:context]; +} + + (instancetype) initWithId:(SLNid)nid { NSLog(@"SLModel initWithId: %@", nid); NSManagedObjectContext *localContext = [SLStore sharedStore].context; @@ -131,117 +137,6 @@ + (NSString *) type userInfo:nil]; } - -+ (PMKPromise *) readById:(SLNid)nid -{ - NSDictionary *filters = @{ - @"filter":@{ - @"fields": [NSNumber numberWithBool: TRUE], - @"rels": [NSNumber numberWithBool: TRUE] - } - }; - return [self readById:nid withFilters:filters]; -} - -+ (PMKPromise *) readById:(SLNid)nid withFilters:(NSDictionary *)filters -{ - return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { - - SLAdapter *manager = [[self class] sharedAPIManager]; - - [MagicalRecord saveWithBlock:^(NSManagedObjectContext *context) { - - SLRequestCallback completionBlock = ^(NSError *error, id operation, id responseObject) { - //NSLog(@"SLRequestCallback completionBlock!"); - //NSLog(@"<%@>: %@", [responseObject class], responseObject); - - // Process & Read Node - SLModel *record = [[self class] initWithId:(SLNid) responseObject[@"id"] inContext:context]; - record.syncState = @(SLSyncStateSynced); - [record setupData:responseObject]; - // Return - [context MR_saveToPersistentStoreAndWait]; - // - fulfiller(record); - }; - - NSString *thePath = [NSString stringWithFormat:@"%@/%@", [[self class] type],nid]; - [manager performRequestWithMethod:SLHTTPMethodGET withPath:thePath withParameters:filters] - .then(completionBlock) - .catch(rejecter); - - } completion:^(BOOL success, NSError *error) { - // Return - fulfiller( [[self class] MR_findFirstByAttribute:@"nid" withValue:nid] ); - }]; - - }]; -} - -+ (PMKPromise *) readAll -{ - NSDictionary *filters = @{ - @"filter":@{ - @"fields": [NSNumber numberWithBool: TRUE], - @"rels": [NSNumber numberWithBool: TRUE] - } - }; - return [self readAllWithFilters:filters]; -} - -+ (PMKPromise *) readAllWithFilters:(NSDictionary *)filters -{ - SLAdapter *manager = [[self class] sharedAPIManager]; - NSLog(@"Manager: %@", manager); - return [self readAllWithAPIManager:manager withFilters:filters]; -} - -+ (PMKPromise *) readAllWithAPIManager:(SLAdapter *)manager withFilters:(NSDictionary *)filters -{ - return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { - - __block NSManagedObjectContext *context = [SLStore sharedStore].context; - - [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) { - - NSLog(@"Inside saving block"); - - [manager performRequestWithMethod:SLHTTPMethodGET withPath:[[self class] type] withParameters:filters] - .then(^(id responseObject, id operation) { - NSLog(@"SLRequestCallback completionBlock."); - NSLog(@"<%@>: %@", [responseObject class], responseObject); - NSMutableArray *nodes = [NSMutableArray array]; - NSArray *arr = ((NSDictionary *)responseObject)[@"nodes"]; - for (NSDictionary* curr in arr) - { - NSLog(@"curr: %@", curr); - SLModel *record = [[self class] initWithId:(SLNid) curr[@"id"] inContext:context]; - record.syncState = @(SLSyncStateSynced); - [record setupData:curr]; - [nodes addObject: record]; - NSLog(@"Node: %@",record); - - } - // callback(nodes); // returning in the completion block - [context MR_saveToPersistentStoreAndWait]; - [localContext save:nil]; - - // - NSLog(@"Done!!!"); - NSLog(@"%@ %@", context, localContext); - - // Return all nodes! - fulfiller( [[self class] MR_findAll] ); - - }) - .catch(rejecter); - - }]; - - - }]; -} - + (instancetype) createWithData:(NSDictionary *)theData withRels:(NSArray *)theRels { SLModel *record = [[[self class] alloc] init]; @@ -266,99 +161,6 @@ + (instancetype) create return [[self class] createWithData:nil withRels:nil]; } -+ (PMKPromise *) deleteWithId:(SLNid)nid -{ - return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { - SLRequestCallback completionBlock = ^(NSError *error, id operation, id responseObject) { - NSLog(@"SLRequestCallback completionBlock!"); - NSLog(@"<%@>: %@", [responseObject class], responseObject); - - // TODO: Check if successful - if (error != nil) { - fulfiller(PMKManifold(responseObject, operation)); - } else { - rejecter(error); - } - }; - NSString *thePath = [NSString stringWithFormat:@"%@/%@", [[self class] type],nid]; - NSLog(@"theDeletePath: %@", thePath); - [[[self class] sharedAPIManager] performRequestWithMethod:SLHTTPMethodDELETE - withPath:thePath withParameters:nil].then(completionBlock); - }]; -} - -+ (PMKPromise *) deleteWithNode:(SLModel *)node -{ - return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { - - [[self class] deleteWithId:node.nid].then(^() { - node.nid = SLNidNodeNotCreated; // Remove - fulfiller(node); - }).catch(rejecter); - }]; -} - -+ (PMKPromise *) deleteWithNodeArray:(NSArray *)nodes -{ - return [[self class] deleteWithNodeArray:nodes withProgressCallback:nil]; -} - -+ (PMKPromise *) deleteWithNodeArray:(NSArray *)nodes withProgressCallback:(void (^)(NSUInteger idx, id item)) progress { - - return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { - - __block NSUInteger completed = 0; - __block NSUInteger totalNodes = [nodes count]; - __block void (^ completionCallback)() = ^{ - // Check if all nodes have been processed (removed) - if (completed >= totalNodes) - { - // All nodes processed, check for completion callback - fulfiller(nodes); - } - }; - SLModel *node; - for (node in nodes) - { - [node remove] - .then(^() - { - if (progress != nil) { - progress(completed, node); - } - completed++; - completionCallback(); - }).catch(^(NSError *error) { - rejecter(error); - }); - } - - }]; -} - -- (PMKPromise *) pushWithAPIManager:(SLAdapter *)manager -{ - return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { - // - NSString *thePath; - if (self.nid == SLNidNodeNotCreated) - { - // New (CREATE) - NSLog(@"CREATE %@", self); - thePath = [NSString stringWithFormat:@"%@", [[self class] type]]; - } else - { - // Update (UPDATE) - NSLog(@"UPDATE %@", self); - thePath = [NSString stringWithFormat:@"%@/%@", [[self class] type], self.nid]; - } - NSLog(@"Save Path: %@", thePath); - NSLog(@"Manager: %@", manager); - [manager performRequestWithMethod:SLHTTPMethodPOST withPath:thePath withParameters:nil] - .then(fulfiller).catch(rejecter); - - }]; -} + (NSEntityDescription *) entity { @@ -400,23 +202,6 @@ - (NSDictionary *) serializeData { return [NSDictionary dictionaryWithDictionary:theData]; } - -- (PMKPromise *) remove -{ - return [[self class] deleteWithNode:self]; -} - -//- (void) didChangeValueForKey:(NSString *)key { -// NSLog(@"didChangeValueForKey %@", key); -// if ( -// ( ![key isEqual: @"syncState"] ) && -// [[self syncState] isEqual: @(SLSyncStateSynced)] -// ) { -// [self setSyncState:@(SLSyncStatePendingUpdate)]; -// } -//} -// - + (instancetype) createRecord:(NSDictionary *)properties { return [[SLStore sharedStore] createRecord:[self class] withProperties:properties]; @@ -465,19 +250,7 @@ - (NSDictionary *) serialize:(NSDictionary *)options - (PMKPromise *) save { - return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { - - // Check if already CREATED - if (true) - { - // return [self.store createRecord:[self class] withProperties:<#(NSDictionary *)#>]; - } - else - { - - } - - }]; + return [[SLStore sharedStore] saveRecord:self]; } - (instancetype) setupData:(NSDictionary *)data @@ -506,4 +279,30 @@ - (instancetype) setupData:(NSDictionary *)data return self; } +- (SLModel *) pushWithData:(NSDictionary *)datum +{ + return [[SLStore sharedStore] push:[self class] withData:datum]; +} + ++ (void) eachAttribute:(void(^)(NSString *key, NSAttributeDescription *attribute))callback +{ + NSDictionary *attributes = [self attributesByName]; + for (NSString *key in attributes) + { + NSAttributeDescription *attribute = [attributes objectForKey:key]; + callback(key, attribute); + } +} + ++ (void) eachRelationship:(void(^)(NSString *key, NSRelationshipDescription *relationship))callback +{ + + NSDictionary *relationships = [self relationshipsByName]; + for (NSString *key in relationships) + { + NSRelationshipDescription *relationship = [relationships objectForKey:key]; + callback(key, relationship); + } +} + @end diff --git a/Common/SLModelProtocol.h b/Common/SLModelProtocol.h index bcc3445..668c013 100644 --- a/Common/SLModelProtocol.h +++ b/Common/SLModelProtocol.h @@ -40,6 +40,8 @@ @required + (instancetype) initWithId:(SLNid)nid inContext:(NSManagedObjectContext *)context; +@required ++ (instancetype) initInContext:(NSManagedObjectContext *)context; /** Attribute to Key mappings for the Model. @@ -65,17 +67,32 @@ includeId: true if the record's ID should be included in the JSON representation. */ +@required - (NSDictionary *) serialize:(NSDictionary *)options; /** Get all model Attributes by name. */ +@required + (NSDictionary *) attributesByName; /** Get all model Attributes by name. */ +@required + (NSDictionary *) relationshipsByName; +/** + Iterate over all of the attributes with a callback. + */ +@required ++ (void) eachAttribute:(void(^)(NSString *key, NSAttributeDescription *attribute))callback; + +/** + Iterate over all of the relationships with a callback. + */ +@required ++ (void) eachRelationship:(void(^)(NSString *key, NSRelationshipDescription *relationship))callback; + @end diff --git a/Common/SLSerializer.h b/Common/SLSerializer.h index eabe2b7..6dd379c 100644 --- a/Common/SLSerializer.h +++ b/Common/SLSerializer.h @@ -14,16 +14,7 @@ /** */ --(void)registerTransform:(SLTransform *)transform forClass:(Class)cls; - -/** - Create a JSON representation of the record, using the serialization strategy of the store's adapter. - - serialize takes an optional hash as a parameter, currently supported options are: - - includeId: true if the record's ID should be included in the JSON representation. - */ -- (NSDictionary *) serialize:(SLModel *)record withOptions:(NSDictionary *)options; +-(void)registerTransform:(SLTransform *)transform forClass:(Class)cls DEPRECATED_ATTRIBUTE; /** Called when the server has returned a payload representing multiple records, such as in response to a findAll or findQuery. @@ -150,4 +141,94 @@ */ - (NSString *)keyForRelationship:(NSString *)relationship; + +/** + Create a JSON representation of the record, using the serialization strategy of the store's adapter. + + serialize takes an optional hash as a parameter, currently supported options are: + + includeId: true if the record's ID should be included in the JSON representation. + */ +- (NSDictionary *) serialize:(SLModel *)record withOptions:(NSDictionary *)options; + +/** + `serializeAttribute` can be used to customize how `DS.attr` + properties are serialized + + For example if you wanted to ensure all your attributes were always + serialized as properties on an `attributes` object you could + write: + + ```javascript + App.ApplicationSerializer = DS.JSONSerializer.extend({ + serializeAttribute: function(record, json, key, attributes) { + json.attributes = json.attributes || {}; + this._super(record, json.attributes, key, attributes); + } + }); + ``` + + @method serializeAttribute + @param {DS.Model} record + @param {Object} json + @param {String} key + @param {Object} attribute + */ +- (NSDictionary *) serializeAttribute:(SLModel *)record withKey:(NSString *)key withData:(NSDictionary *)data; + +/** + `serializeBelongsTo` can be used to customize how `DS.belongsTo` + properties are serialized. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serializeBelongsTo: function(record, json, relationship) { + var key = relationship.key; + + var belongsTo = get(record, key); + + key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo") : key; + + json[key] = Ember.isNone(belongsTo) ? belongsTo : belongsTo.toJSON(); + } + }); + ``` + + @method serializeBelongsTo + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + +- (NSDictionary *) serializeBelongsTo:(SLModel *)record withKey:(NSString *)key withData:(NSDictionary *)data; + +/** + `serializeHasMany` can be used to customize how `DS.hasMany` + properties are serialized. + + Example + + ```javascript + App.PostSerializer = DS.JSONSerializer.extend({ + serializeHasMany: function(record, json, relationship) { + var key = relationship.key; + if (key === 'comments') { + return; + } else { + this._super.apply(this, arguments); + } + } + }); + ``` + + @method serializeHasMany + @param {DS.Model} record + @param {Object} json + @param {Object} relationship + */ + +- (NSDictionary *) serializeHasMany:(SLModel *)record withKey:(NSString *)key withData:(NSDictionary *)data; + @end diff --git a/Common/SLSerializer.m b/Common/SLSerializer.m index 9fe91cc..e8c8516 100644 --- a/Common/SLSerializer.m +++ b/Common/SLSerializer.m @@ -25,12 +25,6 @@ -(void)registerTransform:(SLTransform *)transform forClass:(Class)cls [self.registeredTransforms setValue:transform forKey:clsName]; } -- (NSDictionary *) serialize:(SLModel *)record withOptions:(NSDictionary *)options -{ - // Iterate through all attributes and use the correct Transform to serialize. - return @{}; -} - - (NSDictionary *)extractSingle:(Class)modelClass withPayload:(NSDictionary *)payload withStore:(SLStore *)store { NSDictionary *results = [self normalize:modelClass withPayload:payload]; @@ -173,4 +167,33 @@ - (NSDictionary *)normalizeRelationships:(Class)modelClass withPayload:(NSDictio return [NSDictionary dictionaryWithDictionary:results]; } + +- (NSDictionary *) serialize:(SLModel *)record withOptions:(NSDictionary *)options +{ + NSMutableDictionary *serialized = [NSMutableDictionary dictionary]; + + // TODO: Implement option, `excludeId`. + + Class modelClass = [record class]; + + // Attributes + [modelClass eachAttribute:^(NSString *key, NSAttributeDescription *attribute) { + + [self serializeAttribute:record withKey:key withData:serialized]; + }]; + + // Relationships + [modelClass eachRelationship:^(NSString *key, NSRelationshipDescription *relationship) { + + if ([relationship isToMany]) { + [self serializeHasMany:record withKey:key withData:serialized]; + } else { + [self serializeBelongsTo:record withKey:key withData:serialized]; + } + }]; + + // Iterate through all attributes and use the correct Transform to serialize. + return [NSDictionary dictionaryWithDictionary:serialized]; +} + @end diff --git a/Common/SLStore.h b/Common/SLStore.h index 2e9cbca..625c347 100644 --- a/Common/SLStore.h +++ b/Common/SLStore.h @@ -122,4 +122,9 @@ */ - (NSArray *) pushMany:(Class)modelClass withData:(NSArray *)data; +/** + Save the record and persist any changes to the record to an extenal source via the adapter. +*/ +- (PMKPromise *) saveRecord:(SLModel *)record; + @end diff --git a/Common/SLStore.m b/Common/SLStore.m index 9c260e4..2dd3649 100644 --- a/Common/SLStore.m +++ b/Common/SLStore.m @@ -39,13 +39,13 @@ - (instancetype) init } return self; } -// -//- (SLModel *) createRecord:(Class)modelClass withProperties:(NSDictionary *)properties -//{ -// SLModel *record = [modelClass init]; -// [record setupData:properties]; -// return record; -//} + +- (SLModel *) createRecord:(Class)modelClass withProperties:(NSDictionary *)properties +{ + SLModel *record = [modelClass initInContext:self.context]; + [record setupData:properties]; + return record; +} - (PMKPromise *) findAll:(Class)modelClass { @@ -95,6 +95,35 @@ - (PMKPromise *) find:(Class)modelClass withQuery:(NSDictionary *)query; }]; } +- (PMKPromise *)saveRecord:(SLModel *)record +{ + return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) { + + // Check if already CREATED + if (record.isInserted) + { + // Create record + [self.adapter createRecord:record withStore:self] + .then(fulfiller) + .catch(rejecter); + } + // Check if should be deleted + else if (record.isUpdated) + { + // Delete record + [self.adapter deleteRecord:record withStore:self] + .then(fulfiller) + .catch(rejecter); + + } else { + // UPDATE existing record + [self.adapter updateRecord:record withStore:self] + .then(fulfiller) + .catch(rejecter); + } + }]; +} + - (SLModel *) push:(Class)modelClass withData:(NSDictionary *)datum { // @@ -215,4 +244,9 @@ - (NSArray *) deserializeRecordIds:(NSArray *)ids withRelationship:(NSRelationsh return [NSArray arrayWithArray:arr]; } +- (NSDictionary *) serialize:(SLModel *)record withOptions:(NSDictionary *) options +{ + return [self.adapter serialize:record withOptions:options]; +} + @end diff --git a/Streamlyne-iOS-SDKTests/Streamlyne_iOS_SDKTests.m b/Streamlyne-iOS-SDKTests/Streamlyne_iOS_SDKTests.m index 6206987..0f1f028 100644 --- a/Streamlyne-iOS-SDKTests/Streamlyne_iOS_SDKTests.m +++ b/Streamlyne-iOS-SDKTests/Streamlyne_iOS_SDKTests.m @@ -493,14 +493,18 @@ - (void) testCreateAttributeDatum StartBlock(); // Create - SLAttributeDatum *attributeDatum = [SLAttributeDatum createRecord:@{}]; + SLAttributeDatum *attributeDatum = [SLAttributeDatum createRecord:@{ + }]; // Save [attributeDatum save] .then(^(SLAttributeDatum *attributeDatum) { NSLog(@"Datum: %@", attributeDatum); + EndBlock(); }) .catch(^(NSError *error){ NSLog(@"%@", error); + EndBlock(); + XCTFail(@"%@", error); });