diff --git a/LoopBack.podspec b/LoopBack.podspec index 01d11e0..e2e9190 100644 --- a/LoopBack.podspec +++ b/LoopBack.podspec @@ -10,6 +10,6 @@ Pod::Spec.new do |s| s.requires_arc = true s.ios.deployment_target = '6.1' - s.ios.frameworks = 'UIKit', 'Foundation', 'MobileCoreServices', 'SystemConfiguration' + s.ios.frameworks = 'UIKit', 'Foundation', 'MobileCoreServices', 'SystemConfiguration', 'CoreLocation' end diff --git a/LoopBack.xcodeproj/project.pbxproj b/LoopBack.xcodeproj/project.pbxproj index be5b953..268f1c8 100644 --- a/LoopBack.xcodeproj/project.pbxproj +++ b/LoopBack.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ 47F8E813185B7A4E0088BB73 /* LBPushNotification.h in Headers */ = {isa = PBXBuildFile; fileRef = 47F8E811185B7A4E0088BB73 /* LBPushNotification.h */; settings = {ATTRIBUTES = (Public, ); }; }; 47F8E814185B7A4E0088BB73 /* LBPushNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 47F8E812185B7A4E0088BB73 /* LBPushNotification.m */; }; 8915F0631BF72CD100540BD3 /* module.modulemap in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8915F0621BF72CB500540BD3 /* module.modulemap */; }; + 893C8CBF1C1ABFD20050045C /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 893C8CBE1C1ABFD20050045C /* CoreLocation.framework */; }; 895B41801AB16D660000A9D7 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B278D50187B584F00FFC135 /* MobileCoreServices.framework */; }; 895B41811AB16D670000A9D7 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B278D50187B584F00FFC135 /* MobileCoreServices.framework */; }; 895B41821AB16D6A0000A9D7 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B278D4A187B584A00FFC135 /* SystemConfiguration.framework */; }; @@ -276,15 +277,15 @@ 47C48B931962120E00995044 /* SLRESTContract.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLRESTContract.h; sourceTree = ""; }; 47C48B941962120E00995044 /* SLRESTContract.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLRESTContract.m; sourceTree = ""; }; 47C48BB61962123900995044 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 47C48BB91962123900995044 /* contract-class.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "contract-class.js"; sourceTree = ""; }; - 47C48BBA1962123900995044 /* contract.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = contract.js; sourceTree = ""; }; - 47C48BBB1962123900995044 /* index.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = index.js; sourceTree = ""; }; - 47C48BBC1962123900995044 /* nonroot.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = nonroot.js; sourceTree = ""; }; + 47C48BB91962123900995044 /* contract-class.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = "contract-class.js"; sourceTree = ""; tabWidth = 2; }; + 47C48BBA1962123900995044 /* contract.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = contract.js; sourceTree = ""; tabWidth = 2; }; + 47C48BBB1962123900995044 /* index.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = index.js; sourceTree = ""; tabWidth = 2; }; + 47C48BBC1962123900995044 /* nonroot.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = nonroot.js; sourceTree = ""; tabWidth = 2; }; 47C48BBF1962123900995044 /* certificate.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = certificate.pem; sourceTree = ""; }; 47C48BC01962123900995044 /* privatekey.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = privatekey.pem; sourceTree = ""; }; - 47C48BC11962123900995044 /* simple-class.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "simple-class.js"; sourceTree = ""; }; - 47C48BC21962123900995044 /* simple.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = simple.js; sourceTree = ""; }; - 47C48BC31962123900995044 /* ssl-config.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "ssl-config.js"; sourceTree = ""; }; + 47C48BC11962123900995044 /* simple-class.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = "simple-class.js"; sourceTree = ""; tabWidth = 2; }; + 47C48BC21962123900995044 /* simple.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = simple.js; sourceTree = ""; tabWidth = 2; }; + 47C48BC31962123900995044 /* ssl-config.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = "ssl-config.js"; sourceTree = ""; tabWidth = 2; }; 47C48BC41962123900995044 /* SLRemotingTests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "SLRemotingTests-Info.plist"; sourceTree = ""; }; 47C48BC61962123900995044 /* SLRESTAdapterNonRootTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLRESTAdapterNonRootTests.m; sourceTree = ""; }; 47C48BC71962123900995044 /* SLRESTAdapterSSLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLRESTAdapterSSLTests.m; sourceTree = ""; }; @@ -294,11 +295,12 @@ 47F8E811185B7A4E0088BB73 /* LBPushNotification.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LBPushNotification.h; sourceTree = ""; }; 47F8E812185B7A4E0088BB73 /* LBPushNotification.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LBPushNotification.m; sourceTree = ""; }; 8915F0621BF72CB500540BD3 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 893C8CBE1C1ABFD20050045C /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; 8973753C1AB2A60A00FB31A2 /* SLStreamParam.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLStreamParam.h; sourceTree = ""; }; 8973753D1AB2A60A00FB31A2 /* SLStreamParam.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLStreamParam.m; sourceTree = ""; }; 89C410321B4D4B0200B2ED51 /* LBPersistedModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LBPersistedModel.h; sourceTree = ""; }; 89C410331B4D4B0200B2ED51 /* LBPersistedModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LBPersistedModel.m; sourceTree = ""; }; - 89D3C9521AB17258005A1B90 /* index.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = index.js; sourceTree = ""; }; + 89D3C9521AB17258005A1B90 /* index.js */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = index.js; sourceTree = ""; tabWidth = 2; }; 89D3DB031AB17263005A1B90 /* f1.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = f1.txt; sourceTree = ""; }; 89D3DB071AB17263005A1B90 /* f1.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = f1.txt; sourceTree = ""; }; 89F4F36B1B53E6A80070D8EB /* LBModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LBModelTests.m; sourceTree = ""; }; @@ -341,10 +343,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0B278D51187B584F00FFC135 /* MobileCoreServices.framework in Frameworks */, - 0B278D4B187B584A00FFC135 /* SystemConfiguration.framework in Frameworks */, B3D220FB17722AE800B7CB63 /* Foundation.framework in Frameworks */, 89EB399F1AB16ED900B1EB9D /* UIKit.framework in Frameworks */, + 0B278D51187B584F00FFC135 /* MobileCoreServices.framework in Frameworks */, + 0B278D4B187B584A00FFC135 /* SystemConfiguration.framework in Frameworks */, + 893C8CBF1C1ABFD20050045C /* CoreLocation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -547,6 +550,7 @@ B3D2214317725DAB00B7CB63 /* UIKit.framework */, 0B278D50187B584F00FFC135 /* MobileCoreServices.framework */, 0B278D4A187B584A00FFC135 /* SystemConfiguration.framework */, + 893C8CBE1C1ABFD20050045C /* CoreLocation.framework */, ); name = Frameworks; sourceTree = ""; @@ -1159,6 +1163,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -1189,6 +1194,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; diff --git a/LoopBack/LBModel.h b/LoopBack/LBModel.h index 6c3fa40..994b242 100644 --- a/LoopBack/LBModel.h +++ b/LoopBack/LBModel.h @@ -7,6 +7,68 @@ #import "SLRemoting.h" +/** + * Blocks of this type are executed for a successful method invocation that doesn't return + * any value as a callback argument. + */ +typedef void (^LBModelVoidSuccessBlock)(); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * a BOOL value as a callback argument. + */ +typedef void (^LBModelBoolSuccessBlock)(BOOL boolean); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * an NSInteger value as a callback argument. + */ +typedef void (^LBModelNumberSuccessBlock)(NSInteger number); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * a NSString value as a callback argument. + */ +typedef void (^LBModelStringSuccessBlock)(NSString *string); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * an LBModel instance as a callback argument. + */ +@class LBModel; +typedef void (^LBModelObjectSuccessBlock)(LBModel *model); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * an NSDictionary instance as a callback argument. + */ +typedef void (^LBModelDictionarySuccessBlock)(NSDictionary *dictionary); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * an NSArray instance as a callback argument. + */ +typedef void (^LBModelArraySuccessBlock)(NSArray *array); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * an NSDate instance as a callback argument. + */ +typedef void (^LBModelDateSuccessBlock)(NSDate *date); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * an NSData instance as a callback argument. + */ +typedef void (^LBModelDataSuccessBlock)(NSData *data); + +/** + * Blocks of this type are executed for a successful method invocation that returns + * a CLLocation instance as a callback argument. + */ +typedef void (^LBModelLocationSuccessBlock)(CLLocation *location); + + /** * A local representative of a single model instance on the server. The data is * immediately accessible locally. diff --git a/LoopBack/LBModel.m b/LoopBack/LBModel.m index 8b6782a..c699e39 100644 --- a/LoopBack/LBModel.m +++ b/LoopBack/LBModel.m @@ -5,11 +5,9 @@ * @copyright (c) 2013 StrongLoop. All rights reserved. */ -#import "LBModel.h" - #import -#define NSSelectorForSetter(key) NSSelectorFromString([NSString stringWithFormat:@"set%@:", [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]]]) +#import "LBModel.h" @interface LBModel() { @@ -20,7 +18,6 @@ - (NSMutableDictionary *)_overflow; @end -static NSDateFormatter *jsonDateFormatter = nil; @implementation LBModel @@ -31,12 +28,6 @@ - (instancetype)initWithRepository:(SLRepository *)repository parameters:(NSDict __overflow = [NSMutableDictionary dictionary]; } - if (jsonDateFormatter == nil) { - jsonDateFormatter = [[NSDateFormatter alloc] init]; - [jsonDateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"]; - [jsonDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; - } - return self; } @@ -63,9 +54,6 @@ - (NSDictionary *)toDictionary { NSString *propertyName = [NSString stringWithCString:property_getName(properties[i]) encoding:NSUTF8StringEncoding]; id obj = [self valueForKey:propertyName]; - if ([obj isKindOfClass:[NSDate class]]) { - obj = [jsonDateFormatter stringFromDate:obj]; - } [dict setValue:obj forKey:propertyName]; } free(properties); @@ -105,7 +93,7 @@ - (SLRESTContract *)contract { } - (LBModel *)modelWithDictionary:(NSDictionary *)dictionary { - LBModel __block *model = (LBModel *)[[self.modelClass alloc] initWithRepository:self parameters:dictionary]; + LBModel *model = (LBModel *)[[self.modelClass alloc] initWithRepository:self parameters:dictionary]; [[model _overflow] addEntriesFromDictionary:dictionary]; @@ -122,9 +110,21 @@ - (LBModel *)modelWithDictionary:(NSDictionary *)dictionary { } const char *type = property_getAttributes(property); - // if the property type is NSDate, convert the string to a date object - if ([obj isKindOfClass:[NSString class]] && strncmp(type, "T@\"NSDate\",", 11) == 0) { - obj = [jsonDateFormatter dateFromString:obj]; + if ([obj isKindOfClass:[NSString class]]) { + // if the property type is NSDate, convert the string to a date object + if (strncmp(type, "T@\"NSDate\",", 11) == 0) { + obj = [SLObject dateFromEncodedProperty:obj]; + } + } else if ([obj isKindOfClass:[NSDictionary class]]) { + // if the property type is NSMutableData, convert the json object to a data object + if (strncmp(type, "T@\"NSMutableData\",", 18) == 0 || + strncmp(type, "T@\"NSData\",", 11) == 0) { + obj = [SLObject dataFromEncodedProperty:obj]; + } + // if the property type is CLLocation, convert the json object to a location object + else if (strncmp(type, "T@\"CLLocation\",", 15) == 0) { + obj = [SLObject locationFromEncodedProperty:obj]; + } } @try { diff --git a/LoopBack/LBPersistedModel.h b/LoopBack/LBPersistedModel.h index 2a44784..9302dbe 100644 --- a/LoopBack/LBPersistedModel.h +++ b/LoopBack/LBPersistedModel.h @@ -7,12 +7,23 @@ #import "LBModel.h" + +/** + * Blocks of this type are executed for a successful method invocation that returns + * an LBPersistedModel instance as a callback argument. + */ @class LBPersistedModel; typedef void (^LBPersistedModelObjectSuccessBlock)(LBPersistedModel *model); -typedef void (^LBPersistedModelArraySuccessBlock)(NSArray *array); -typedef void (^LBPersistedModelVoidSuccessBlock)(); -typedef void (^LBPersistedModelBoolSuccessBlock)(BOOL boolean); -typedef void (^LBPersistedModelNumberSuccessBlock)(NSInteger number); + +// The following typedefs are left for backward compatibility. +typedef LBModelVoidSuccessBlock LBPersistedModelVoidSuccessBlock + __attribute((deprecated("use LBModelVoidSuccessBlock"))); +typedef LBModelBoolSuccessBlock LBPersistedModelBoolSuccessBlock + __attribute((deprecated("use LBModelBoolSuccessBlock"))); +typedef LBModelNumberSuccessBlock LBPersistedModelNumberSuccessBlock + __attribute((deprecated("use LBModelNumberSuccessBlock"))); +typedef LBModelArraySuccessBlock LBPersistedModelArraySuccessBlock + __attribute((deprecated("use LBModelArraySuccessBlock"))); /** * A local representative of a single persisted model instance on the server. @@ -27,7 +38,7 @@ typedef void (^LBPersistedModelNumberSuccessBlock)(NSInteger number); * Blocks of this type are executed when LBPersistedModel::saveWithSuccess:failure: is * successful. */ -typedef LBPersistedModelVoidSuccessBlock LBPersistedModelSaveSuccessBlock; +typedef LBModelVoidSuccessBlock LBPersistedModelSaveSuccessBlock; /** * Saves the LBPersistedModel to the server. * @@ -43,7 +54,7 @@ typedef LBPersistedModelVoidSuccessBlock LBPersistedModelSaveSuccessBlock; * Blocks of this type are executed when LBPersistedModel::destroyWithSuccess:failure: is * successful. */ -typedef LBPersistedModelVoidSuccessBlock LBPersistedModelDestroySuccessBlock; +typedef LBModelVoidSuccessBlock LBPersistedModelDestroySuccessBlock; /** * Destroys the LBPersistedModel from the server. * @@ -88,7 +99,7 @@ typedef LBPersistedModelObjectSuccessBlock LBPersistedModelFindSuccessBlock; * Blocks of this type are executed when * LBPersistedModelRepository::allWithSuccess:failure: is successful. */ -typedef LBPersistedModelArraySuccessBlock LBPersistedModelAllSuccessBlock; +typedef LBModelArraySuccessBlock LBPersistedModelAllSuccessBlock; /** * Finds and downloads all models of this type on and from the server. * diff --git a/LoopBack/LBPersistedModel.m b/LoopBack/LBPersistedModel.m index 9340aef..94c84bc 100644 --- a/LoopBack/LBPersistedModel.m +++ b/LoopBack/LBPersistedModel.m @@ -7,10 +7,6 @@ #import "LBPersistedModel.h" -#import - -#define NSSelectorForSetter(key) NSSelectorFromString([NSString stringWithFormat:@"set%@:", [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]]]) - @interface LBPersistedModel() { id __id; diff --git a/LoopBack/LBRESTAdapter.m b/LoopBack/LBRESTAdapter.m index 7a3aedc..3fcd0a3 100644 --- a/LoopBack/LBRESTAdapter.m +++ b/LoopBack/LBRESTAdapter.m @@ -50,13 +50,12 @@ - (LBModelRepository *)repositoryWithModelName:(NSString *)name persisted:(BOOL) - (LBModelRepository *)repositoryWithClass:(Class)type { if (type == nil) { @throw [NSException exceptionWithName:NSInvalidArgumentException - reason:[NSString stringWithFormat:@"Argument cannot be nil"] + reason:@"Argument cannot be nil" userInfo:nil]; } if (![type isSubclassOfClass:[LBModelRepository class]]) { @throw [NSException exceptionWithName:NSInvalidArgumentException - reason:[NSString stringWithFormat: - @"Argument needs to be a subclass of LBModelRepository"] + reason:@"Argument needs to be a subclass of LBModelRepository" userInfo:nil]; } if (![type respondsToSelector:@selector(repository)]) { diff --git a/LoopBack/module.modulemap b/LoopBack/module.modulemap index ef36938..799ea80 100644 --- a/LoopBack/module.modulemap +++ b/LoopBack/module.modulemap @@ -6,4 +6,5 @@ framework module LoopBack { link framework "MobileCoreServices" link framework "SystemConfiguration" + link framework "CoreLocation" } diff --git a/LoopBackTests/LBPersistedModelSubclassingTests.m b/LoopBackTests/LBPersistedModelSubclassingTests.m index 7aac271..41ca069 100644 --- a/LoopBackTests/LBPersistedModelSubclassingTests.m +++ b/LoopBackTests/LBPersistedModelSubclassingTests.m @@ -17,13 +17,17 @@ @interface Widget : LBPersistedModel @property (nonatomic, copy) NSString *name; -// a Number property can be accessed via either of NSNumber or the primitive type NSInteger. +// a Number property can be accessed via NSNumber or numeric primitive types. @property (nonatomic) NSNumber *bars; @property NSInteger bars2; -// a Boolean property can be accessed via either of NSNumber or the primitive type BOOL. +// a Boolean property can be accessed via NSNumber or the primitive type BOOL. @property (nonatomic) NSNumber *flag; @property BOOL flag2; +@property (nonatomic) NSDictionary *data; +@property (nonatomic) NSArray *stringArray; @property (nonatomic) NSDate *date; +@property (nonatomic) NSData *buffer; +@property (nonatomic) CLLocation *geopoint; @end @@ -41,6 +45,66 @@ + (instancetype)repository { return [self repositoryWithClassName:@"widgets"]; } +- (SLRESTContract *)contract { + SLRESTContract *contract = [super contract]; + + [contract addItem:[SLRESTContractItem itemWithPattern:[NSString stringWithFormat:@"/%@/testDate", self.className] verb:@"GET"] + forMethod:[NSString stringWithFormat:@"%@.testDate", self.className]]; + + [contract addItem:[SLRESTContractItem itemWithPattern:[NSString stringWithFormat:@"/%@/testBuffer", self.className] verb:@"GET"] + forMethod:[NSString stringWithFormat:@"%@.testBuffer", self.className]]; + + [contract addItem:[SLRESTContractItem itemWithPattern:[NSString stringWithFormat:@"/%@/testGeoPoint", self.className] verb:@"GET"] + forMethod:[NSString stringWithFormat:@"%@.testGeoPoint", self.className]]; + + return contract; +} + +- (void)testMethodWithDate:(NSDate *)date + success:(LBModelDateSuccessBlock)success + failure:(SLFailureBlock)failure { + + [self invokeStaticMethod:@"testDate" + parameters:@{ @"date": date } + success:^(id value) { + NSDictionary *ret = value[@"date"]; + NSAssert(ret, @"Invalid format: No value for key 'date' found: %@", value); + NSDate *date = [SLObject dateFromEncodedArgument:ret]; + success(date); + } + failure:failure]; +} + +- (void)testMethodWithData:(NSData *)data + success:(LBModelDataSuccessBlock)success + failure:(SLFailureBlock)failure { + + [self invokeStaticMethod:@"testBuffer" + parameters:@{ @"buffer": data } + success:^(id value) { + NSDictionary *ret = value[@"buffer"]; + NSAssert(ret, @"Invalid format: No value for key 'buffer' found: %@", value); + NSData *data = [SLObject dataFromEncodedArgument:ret]; + success(data); + } + failure:failure]; +} + +- (void)testmethodWithLocation:(CLLocation *)location + success:(LBModelLocationSuccessBlock)success + failure:(SLFailureBlock)failure { + + [self invokeStaticMethod:@"testGeoPoint" + parameters:@{ @"geopoint": location } + success:^(id value) { + NSDictionary *ret = value[@"geopoint"]; + NSAssert(ret, @"Invalid format: No value for key 'geopoint' found: %@", value); + CLLocation *location = [SLObject locationFromEncodedArgument:ret]; + success(location); + } + failure:failure]; +} + @end @interface TestRepository : LBPersistedModelRepository @@ -72,6 +136,10 @@ + (id)defaultTestSuite { [suite addTest:[self testCaseWithSelector:@selector(testAll)]]; [suite addTest:[self testCaseWithSelector:@selector(testUpdate)]]; [suite addTest:[self testCaseWithSelector:@selector(testRemove)]]; + [suite addTest:[self testCaseWithSelector:@selector(testPropertyDataTypes)]]; + [suite addTest:[self testCaseWithSelector:@selector(testMethodDateArgAndRetValue)]]; + [suite addTest:[self testCaseWithSelector:@selector(testMethodBufferArgAndRetValue)]]; + [suite addTest:[self testCaseWithSelector:@selector(testMethodGeoPointArgAndRetValue)]]; return suite; } @@ -92,38 +160,20 @@ - (void)testRepositoryNotOverridden { } - (void)testCreate { - Widget *model = (Widget*)[self.repository modelWithDictionary:@{ - @"name" : @"Foobar", - @"bars" : @123, - @"bars2": @123, - @"flag" : @YES, - @"flag2": @YES, - @"date" : @"1970-01-01T00:00:00.000Z" + Widget *widget = (Widget*)[self.repository modelWithDictionary: @{ + @"name": @"Foobar", + @"bars": @1 }]; - XCTAssertNil(model._id, @"Invalid id"); - XCTAssertEqualObjects(model.name, @"Foobar", @"Invalid name."); - XCTAssertEqualObjects(model.bars, @123, @"Invalid bars."); - XCTAssertEqual(model.bars2, 123, @"Invalid bars2."); - XCTAssertEqualObjects(model.flag, @YES, @"Invalid flag."); - XCTAssertEqual(model.flag2, YES, @"Invalid flag2."); - XCTAssertEqualObjects(model.date, [NSDate dateWithTimeIntervalSince1970:0], @"Invalid date."); - - model.bars = @456; - model.bars2 = 456; - model.flag = @NO; - model.flag2 = NO; - model.date = [NSDate dateWithTimeIntervalSince1970:123]; + XCTAssertEqualObjects(widget.name, @"Foobar", @"Invalid name."); + XCTAssertEqualObjects(widget.bars, @1, @"Invalid bars."); + XCTAssertNil(widget._id, @"Invalid id"); ASYNC_TEST_START - [model saveWithSuccess:^{ - lastId = model._id; - XCTAssertNotNil(model._id, @"Invalid id"); - XCTAssertEqualObjects(model.bars, @456, @"Invalid bars."); - XCTAssertEqual(model.bars2, 456, @"Invalid bars2."); - XCTAssertEqualObjects(model.flag, @NO, @"Invalid flag."); - XCTAssertEqual(model.flag2, NO, @"Invalid flag2."); - XCTAssertEqualObjects(model.date, [NSDate dateWithTimeIntervalSince1970:123], @"Invalid date."); + [widget saveWithSuccess:^{ + NSLog(@"Completed with: %@", widget._id); + XCTAssertNotNil(widget._id, @"Invalid id"); + lastId = widget._id; ASYNC_TEST_SIGNAL } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END @@ -132,13 +182,14 @@ - (void)testCreate { - (void)testFind { ASYNC_TEST_START [self.repository findById:@2 - success:^(LBPersistedModel *model) { - XCTAssertNotNil(model, @"No model found with ID 2"); - XCTAssertTrue([[model class] isSubclassOfClass:[Widget class]], @"Invalid class."); - XCTAssertEqualObjects(((Widget *)model).name, @"Bar", @"Invalid name"); - XCTAssertEqualObjects(((Widget *)model).bars, @1, @"Invalid bars"); - ASYNC_TEST_SIGNAL - } failure:ASYNC_TEST_FAILURE_BLOCK]; + success:^(LBPersistedModel *model) { + XCTAssertNotNil(model, @"No model found with ID 2"); + XCTAssertTrue([model isMemberOfClass:[Widget class]], @"Invalid class."); + Widget *widget = (Widget *)model; + XCTAssertEqualObjects(widget.name, @"Bar", @"Invalid name"); + XCTAssertEqualObjects(widget.bars, @1, @"Invalid bars"); + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END } @@ -147,8 +198,8 @@ - (void)testAll { [self.repository allWithSuccess:^(NSArray *models) { XCTAssertNotNil(models, @"No models returned."); XCTAssertTrue([models count] >= 2, @"Invalid # of models returned: %lu", (unsigned long)[models count]); - XCTAssertTrue([[models[0] class] isSubclassOfClass:[Widget class]], @"Invalid class."); - XCTAssertTrue([[models[1] class] isSubclassOfClass:[Widget class]], @"Invalid class."); + XCTAssertTrue([models[0] isMemberOfClass:[Widget class]], @"Invalid class."); + XCTAssertTrue([models[1] isMemberOfClass:[Widget class]], @"Invalid class."); XCTAssertEqualObjects(((Widget *)models[0]).name, @"Foo", @"Invalid name."); XCTAssertEqualObjects(((Widget *)models[0]).bars, @0, @"Invalid bars"); XCTAssertEqualObjects(((Widget *)models[1]).name, @"Bar", @"Invalid name"); @@ -159,54 +210,136 @@ - (void)testAll { } - (void)testUpdate { - __block NSString *savedName; - __block NSDate *savedDate; + Widget *widget = (Widget*)[self.repository modelWithDictionary:@{ + @"name": @"Foobar", + @"bars": @1 + }]; ASYNC_TEST_START - LBPersistedModelFindSuccessBlock verify = ^(LBPersistedModel *model) { - XCTAssertNotNil(model, @"No model found with ID 2"); - XCTAssertTrue([[model class] isSubclassOfClass:[Widget class]], @"Invalid class."); - Widget *widget = (Widget *)model; - XCTAssertEqualObjects(widget.name, @"Barfoo", @"Invalid name"); - XCTAssertEqualObjects(widget.bars, @1, @"Invalid bars"); - XCTAssertEqualObjects(widget.date, [NSDate dateWithTimeIntervalSince1970:0], @"Invalid date"); + [widget saveWithSuccess:^{ + NSNumber *tempId = widget._id; + // find the model just created + [self.repository findById:tempId success:^(LBPersistedModel *model) { + XCTAssertNotNil(model, @"No model found"); + XCTAssertTrue([model isMemberOfClass:[Widget class]], @"Invalid class."); + Widget *widget = (Widget *)model; + // update + widget.name = @"Barfoo"; + [widget saveWithSuccess:^() { + // find again + [self.repository findById:tempId success:^(LBPersistedModel *model) { + XCTAssertNotNil(model, @"No model found"); + XCTAssertTrue([model isMemberOfClass:[Widget class]], @"Invalid class."); + Widget *widget = (Widget *)model; + // verify + XCTAssertEqualObjects(widget.name, @"Barfoo", @"Invalid name"); + XCTAssertEqualObjects(widget.bars, @1, @"Invalid bars"); + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; + ASYNC_TEST_END +} - widget.name = savedName; - widget.date = savedDate; - [model saveWithSuccess:^{ - ASYNC_TEST_SIGNAL +- (void)testRemove { + ASYNC_TEST_START + [self.repository findById:lastId success:^(LBPersistedModel *model) { + [model destroyWithSuccess:^{ + [self.repository findById:lastId success:^(LBPersistedModel *model) { + XCTFail(@"Model found after removal"); + } failure:^(NSError *err) { + ASYNC_TEST_SIGNAL + }]; } failure:ASYNC_TEST_FAILURE_BLOCK]; - }; + } failure:ASYNC_TEST_FAILURE_BLOCK]; + ASYNC_TEST_END +} - LBPersistedModelSaveSuccessBlock findAgain = ^() { - [self.repository findById:@2 success:verify failure:ASYNC_TEST_FAILURE_BLOCK]; - }; +- (void)testPropertyDataTypes { + Widget *widget = (Widget*)[self.repository modelWithDictionary: nil]; + widget.name = @"Foobar"; + widget.bars = @123; + widget.bars2 = 456; + widget.flag = @YES; + widget.flag2 = NO; + widget.data = @{ @"data1": @1, @"data2": @2 }; + widget.stringArray = @[ @"one", @"two", @"three" ]; + widget.date = [NSDate dateWithTimeIntervalSince1970:123]; + const char bufferBytes[] = { 12, 34, 56 }; + NSData *testData = [NSData dataWithBytes:bufferBytes length:sizeof(bufferBytes)]; + widget.buffer = testData; + widget.geopoint = [[CLLocation alloc] initWithLatitude:12.3 longitude:45.6]; - LBPersistedModelFindSuccessBlock update = ^(LBPersistedModel *model) { - XCTAssertNotNil(model, @"No model found with ID 2"); - XCTAssertTrue([[model class] isSubclassOfClass:[Widget class]], @"Invalid class."); - Widget *widget = (Widget *)model; - savedName = widget.name; - savedDate = widget.date; - widget.name = @"Barfoo"; - widget.date = [NSDate dateWithTimeIntervalSince1970:0]; - [model saveWithSuccess:findAgain failure:ASYNC_TEST_FAILURE_BLOCK]; - }; - - [self.repository findById:@2 success:update failure:ASYNC_TEST_FAILURE_BLOCK]; + ASYNC_TEST_START + [widget saveWithSuccess:^{ + NSNumber *tempId = widget._id; + [self.repository findById:tempId success:^(LBPersistedModel *model) { + XCTAssertNotNil(model, @"No model found"); + XCTAssertTrue([model isMemberOfClass:[Widget class]], @"Invalid class."); + Widget *widget = (Widget *)model; + XCTAssertEqual(widget._id, tempId, @"Invalid id"); + XCTAssertEqualObjects(widget.name, @"Foobar", @"Invalid name."); + XCTAssertEqualObjects(widget.bars, @123, @"Invalid bars."); + XCTAssertEqual(widget.bars2, 456, @"Invalid bars2."); + XCTAssertEqualObjects(widget.flag, @YES, @"Invalid flag."); + XCTAssertEqual(widget.flag2, NO, @"Invalid flag2."); + XCTAssertEqualObjects(widget.data, (@{ @"data1": @1, @"data2": @2 }), @"Invalid data."); + XCTAssertEqualObjects(widget.stringArray, (@[ @"one", @"two", @"three" ]), @"Invalid array."); + XCTAssertEqualObjects(widget.date, [NSDate dateWithTimeIntervalSince1970:123], @"Invalid date."); + XCTAssertEqualObjects(widget.buffer, testData, @"Invalid buffer."); + XCTAssertEqual(widget.geopoint.coordinate.latitude, 12.3, @"Invalid latitude."); + XCTAssertEqual(widget.geopoint.coordinate.longitude, 45.6, @"Invalid longitude."); + + [widget destroyWithSuccess:^{ + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END } -- (void)testRemove { +// The following three tests check special handling for method argument encoding/decoding +// for Date, Buffer and GeoPoint data types. +// Although such encoding/decoding are done in SLObject, these tests are located here for the +// convenience of testing data type handling together with the above property encoding/decoding. + +- (void)testMethodDateArgAndRetValue { ASYNC_TEST_START - [self.repository findById:lastId - success:^(LBPersistedModel *model) { - [model destroyWithSuccess:^{ - ASYNC_TEST_SIGNAL - } failure:ASYNC_TEST_FAILURE_BLOCK]; - } failure:ASYNC_TEST_FAILURE_BLOCK]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:123]; + [self.repository testMethodWithDate:(NSDate *)date success:^(NSDate *retDate) { + // The test methods returns the date advanced from the given date by 1 second. + XCTAssertEqualObjects(retDate, [NSDate dateWithTimeIntervalSince1970:124], @"Invalid date."); + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END } +- (void)testMethodBufferArgAndRetValue { + ASYNC_TEST_START + const char bufferBytes[] = { 01, 02, 03 }; + NSMutableData *data = [NSMutableData dataWithBytes:bufferBytes length:sizeof(bufferBytes)]; + [self.repository testMethodWithData:(NSData *)data success:^(NSData *retData) { + // The test method returns the given buffer after incrementing all the bytes by 1. + const char expectedBytes[] = { 02, 03, 04 }; + NSMutableData *expectedData = [NSMutableData dataWithBytes:expectedBytes length:sizeof(expectedBytes)]; + XCTAssertEqualObjects(retData, expectedData, @"Invalid data."); + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; + ASYNC_TEST_END +} + +- (void)testMethodGeoPointArgAndRetValue { + ASYNC_TEST_START + CLLocation *location = [[CLLocation alloc] initWithLatitude:1.23 longitude:4.56]; + [self.repository testmethodWithLocation:location success:^(CLLocation *retLocation) { + // The test method returns a location object with lat +1 and lng +1 of the given object. + // Note that only the lat and the lng values are sent to/from the server. + XCTAssertEqual(retLocation.coordinate.latitude, 2.23, @"Invalid latitude."); + XCTAssertEqual(retLocation.coordinate.longitude, 5.56, @"Invalid latitude."); + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; + ASYNC_TEST_END +} @end diff --git a/LoopBackTests/LBPersistedModelTests.m b/LoopBackTests/LBPersistedModelTests.m index 12c3ff0..03f318b 100644 --- a/LoopBackTests/LBPersistedModelTests.m +++ b/LoopBackTests/LBPersistedModelTests.m @@ -52,17 +52,20 @@ - (void)tearDown { } - (void)testCreate { - LBPersistedModel __block *model = (LBPersistedModel*)[self.repository modelWithDictionary:@{ @"name": @"Foobar", @"bars": @1 }]; + LBPersistedModel *model = (LBPersistedModel*)[self.repository modelWithDictionary: @{ + @"name": @"Foobar", + @"bars": @1 + }]; - XCTAssertEqualObjects(@"Foobar", model[@"name"], @"Invalid name."); - XCTAssertEqualObjects(@1, model[@"bars"], @"Invalid bars."); + XCTAssertEqualObjects(model[@"name"], @"Foobar", @"Invalid name."); + XCTAssertEqualObjects(model[@"bars"], @1, @"Invalid bars."); XCTAssertNil(model._id, @"Invalid id"); ASYNC_TEST_START [model saveWithSuccess:^{ NSLog(@"Completed with: %@", model._id); - lastId = model._id; XCTAssertNotNil(model._id, @"Invalid id"); + lastId = model._id; ASYNC_TEST_SIGNAL } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END @@ -70,14 +73,13 @@ - (void)testCreate { - (void)testFind { ASYNC_TEST_START - [self.repository findById:@2 - success:^(LBPersistedModel *model) { - XCTAssertNotNil(model, @"No model found with ID 2"); - XCTAssertTrue([[model class] isSubclassOfClass:[LBPersistedModel class]], @"Invalid class."); - XCTAssertEqualObjects(model[@"name"], @"Bar", @"Invalid name"); - XCTAssertEqualObjects(model[@"bars"], @1, @"Invalid bars"); - ASYNC_TEST_SIGNAL - } failure:ASYNC_TEST_FAILURE_BLOCK]; + [self.repository findById:@2 success:^(LBPersistedModel *model) { + XCTAssertNotNil(model, @"No model found with ID 2"); + XCTAssertTrue([model isMemberOfClass:[LBPersistedModel class]], @"Invalid class."); + XCTAssertEqualObjects(model[@"name"], @"Bar", @"Invalid name"); + XCTAssertEqualObjects(model[@"bars"], @1, @"Invalid bars"); + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END } @@ -86,7 +88,7 @@ - (void)testAll { [self.repository allWithSuccess:^(NSArray *models) { XCTAssertNotNil(models, @"No models returned."); XCTAssertTrue([models count] >= 2, @"Invalid # of models returned: %lu", (unsigned long)[models count]); - XCTAssertTrue([[models[0] class] isSubclassOfClass:[LBPersistedModel class]], @"Invalid class."); + XCTAssertTrue([models[0] isMemberOfClass:[LBPersistedModel class]], @"Invalid class."); XCTAssertEqualObjects(models[0][@"name"], @"Foo", @"Invalid name"); XCTAssertEqualObjects(models[0][@"bars"], @0, @"Invalid bars"); XCTAssertEqualObjects(models[1][@"name"], @"Bar", @"Invalid name"); @@ -99,10 +101,10 @@ - (void)testAll { - (void)testFindWithFilter { ASYNC_TEST_START [self.repository findWithFilter:@{@"where": @{ @"name" : @"Foo" }} - success:^(NSArray *models) { + success:^(NSArray *models) { XCTAssertNotNil(models, @"No models returned."); XCTAssertTrue([models count] >= 1, @"Invalid # of models returned: %lu", (unsigned long)[models count]); - XCTAssertTrue([[models[0] class] isSubclassOfClass:[LBPersistedModel class]], @"Invalid class."); + XCTAssertTrue([models[0] isMemberOfClass:[LBPersistedModel class]], @"Invalid class."); XCTAssertEqualObjects(models[0][@"name"], @"Foo", @"Invalid name"); XCTAssertEqualObjects(models[0][@"bars"], @0, @"Invalid bars"); ASYNC_TEST_SIGNAL @@ -128,56 +130,54 @@ - (void)testFindOne { - (void)testFindOneWithFilter { ASYNC_TEST_START [self.repository findOneWithFilter: @{@"where": @{ @"name" : @"Foo" }} - success:^(LBPersistedModel *model) { + success:^(LBPersistedModel *model) { XCTAssertNotNil(model, @"No models returned."); XCTAssertEqualObjects(model[@"name"], @"Foo", @"Invalid name"); XCTAssertEqualObjects(model[@"bars"], @0, @"Invalid bars"); ASYNC_TEST_SIGNAL - } failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END } - (void)testUpdate { - ASYNC_TEST_START - LBPersistedModelFindSuccessBlock verify = ^(LBPersistedModel *model) { - XCTAssertNotNil(model, @"No model found with ID 2"); - XCTAssertEqualObjects(model[@"name"], @"Barfoo", @"Invalid name"); - XCTAssertEqualObjects(model[@"bars"], @1, @"Invalid bars"); + LBPersistedModel *model = (LBPersistedModel*)[self.repository modelWithDictionary: @{ + @"name": @"Foobar", + @"bars": @1 + }]; - model[@"name"] = @"Bar"; - [model saveWithSuccess:^{ - ASYNC_TEST_SIGNAL + ASYNC_TEST_START + [model saveWithSuccess:^{ + NSNumber *tempId = model._id; + // find the model just created + [self.repository findById:tempId success:^(LBPersistedModel *model) { + XCTAssertNotNil(model, @"No model found"); + // update + model[@"name"] = @"Barfoo"; + [model saveWithSuccess:^() { + // find again + [self.repository findById:tempId success:^(LBPersistedModel *model) { + // verify + XCTAssertNotNil(model, @"No model found with ID 2"); + XCTAssertEqualObjects(model[@"name"], @"Barfoo", @"Invalid name"); + XCTAssertEqualObjects(model[@"bars"], @1, @"Invalid bars"); + ASYNC_TEST_SIGNAL + } failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; } failure:ASYNC_TEST_FAILURE_BLOCK]; - }; - - LBPersistedModelSaveSuccessBlock findAgain = ^() { - [self.repository findById:@2 success:verify failure:ASYNC_TEST_FAILURE_BLOCK]; - }; - - LBPersistedModelFindSuccessBlock update = ^(LBPersistedModel *model) { - XCTAssertNotNil(model, @"No model found with ID 2"); - model[@"name"] = @"Barfoo"; - [model saveWithSuccess:findAgain failure:ASYNC_TEST_FAILURE_BLOCK]; - }; - - [self.repository findById:@2 success:update failure:ASYNC_TEST_FAILURE_BLOCK]; + } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END } - (void)testRemove { ASYNC_TEST_START [self.repository findById:lastId success:^(LBPersistedModel *model) { - [model destroyWithSuccess:^{ - [self.repository findById:lastId success:^(LBPersistedModel *model) { XCTFail(@"Model found after removal"); } failure:^(NSError *err) { ASYNC_TEST_SIGNAL }]; - } failure:ASYNC_TEST_FAILURE_BLOCK]; - } failure:ASYNC_TEST_FAILURE_BLOCK]; ASYNC_TEST_END } diff --git a/LoopBackTests/bin/run-unit-tests b/LoopBackTests/bin/run-unit-tests index 21a63e1..13602a9 100755 --- a/LoopBackTests/bin/run-unit-tests +++ b/LoopBackTests/bin/run-unit-tests @@ -26,12 +26,19 @@ node LoopBackTests/server/index.js & node SLRemotingTests/server/index.js & # run the unit tests on iPhone simulator +DESTINATION_SIMULATOR='iPhone 6s' +DESTINATION_SIMULATOR_CI='iPhone Retina (4-inch 64-bit)' +# CI's xcode only has old simulators +if [[ -n "${JENKINS_HOME}" ]]; then + DESTINATION_SIMULATOR=${DESTINATION_SIMULATOR_CI} +fi + xcodebuild \ -verbose \ -project LoopBack.xcodeproj \ -scheme LoopBack \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone Retina (4-inch 64-bit),OS=latest' \ + -destination "platform=iOS Simulator,name=${DESTINATION_SIMULATOR},OS=latest" \ ${XCODEBUILD_ARGS} \ test diff --git a/LoopBackTests/server/index.js b/LoopBackTests/server/index.js index 247ee66..a2267ae 100644 --- a/LoopBackTests/server/index.js +++ b/LoopBackTests/server/index.js @@ -30,18 +30,63 @@ var Widget = app.model('widget', { type: Boolean, required: false }, + data: { + type: Object, + required: false + }, + stringArray: { + type: [String], + required: false + }, date: { type: Date, required: false }, - data: { - type: Object, + buffer: { + type: Buffer, + required: false + }, + geopoint: { + type: 'geopoint', required: false } }, dataSource: 'Memory' }); +Widget.testDate = function(date, callback) { + // advance the time of the given date by 1 second and return it + var ret = new Date(date.getTime() + 1000); + callback(null, ret); +}; +Widget.testDate.shared = true; +Widget.testDate.accepts = [{ arg: 'date', type: 'date' }]; +Widget.testDate.returns = [{ arg: 'date', type: 'date' }]; +Widget.testDate.http = { verb: 'get' }; + +Widget.testBuffer = function(buffer, callback) { + // increment all the bytes of the given buffer by 1 and return it + for (var i = 0; i < buffer.length; i++) { + buffer[i]++; + } + callback(null, buffer); +}; +Widget.testBuffer.shared = true; +Widget.testBuffer.accepts = [{ arg: 'buffer', type: 'buffer' }]; +Widget.testBuffer.returns = [{ arg: 'buffer', type: 'buffer' }]; +Widget.testBuffer.http = { verb: 'get' }; + +Widget.testGeoPoint = function(geopoint, callback) { + // add 1 to both lat and lng of the given geopoint and return it + geopoint.lat += 1; + geopoint.lng += 1; + callback(null, geopoint); +}; +Widget.testGeoPoint.shared = true; +Widget.testGeoPoint.accepts = [{ arg: 'geopoint', type: 'geopoint' }]; +Widget.testGeoPoint.returns = [{ arg: 'geopoint', type: 'geopoint' }]; +Widget.testGeoPoint.http = { verb: 'get' }; + var lbpn = require('loopback-component-push'); var PushModel = lbpn.createPushModel(app, { dataSource: app.dataSources.Memory }); var Installation = lbpn.Installation; @@ -57,18 +102,28 @@ var ds = app.dataSource('storage', { var container = loopback.createModel({ name: 'container', base: 'Model' }); app.model(container, { dataSource: 'storage' }); +var GeoPoint = require('loopback-datasource-juggler/lib/geo').GeoPoint; + Widget.destroyAll(function () { Widget.create({ name: 'Foo', bars: 0, data: { - quux: true - } + data1: 1, + data2: 2 + }, + array: [ + 'one', + 'two', + 'three' + ], + date: new Date('January 1, 1970 00:00:00.000Z'), + buffer: new Buffer('010203', 'hex'), + geopoint: new GeoPoint({lat: 10.32424, lng: 5.84978}) }); Widget.create({ name: 'Bar', - bars: 1, - date: '2000-01-02T03:04:05.006Z' + bars: 1 }); }); diff --git a/SLRemoting/SLObject.h b/SLRemoting/SLObject.h index 6b3422c..5e47d27 100644 --- a/SLRemoting/SLObject.h +++ b/SLRemoting/SLObject.h @@ -6,6 +6,7 @@ */ #import +#import // for CLLocation #import "SLAdapter.h" @@ -115,6 +116,48 @@ extern NSString *SLObjectInvalidRepositoryDescription; success:(SLSuccessBlock)success failure:(SLFailureBlock)failure; +/** + * Converts encoded Date value to an NSDate object following the argument encoding rule. + * + * @param value The argument value to be converted. + */ ++ (NSDate *)dateFromEncodedArgument:(NSDictionary *)value; + +/** + * Converts encoded Buffer value to an NSData object following the argument encoding rule. + * + * @param value The argument value to be converted. + */ ++ (NSData *)dataFromEncodedArgument:(NSDictionary *)value; + +/** + * Converts encoded GeoPoint value to a CLLocation object following the argument encoding rule. + * + * @param value The argument value to be converted. + */ ++ (CLLocation *)locationFromEncodedArgument:(NSDictionary *)value; + +/** + * Converts encoded Date value to an NSDate object following the property encoding rule. + * + * @param value The property value to be converted. + */ ++ (NSDate *)dateFromEncodedProperty:(NSString *)value; + +/** + * Converts encoded Buffer value to an NSData object following the property encoding rule. + * + * @param value The property value to be converted. + */ ++ (NSMutableData *)dataFromEncodedProperty:(NSDictionary *)value; + +/** + * Converts encoded GeoPoint value to a CLLocation object following the property encoding rule. + * + * @param value The property value to be converted. + */ ++ (CLLocation *)locationFromEncodedProperty:(NSDictionary *)value; + @end /** diff --git a/SLRemoting/SLObject.m b/SLRemoting/SLObject.m index 29308e0..7c4ea51 100644 --- a/SLRemoting/SLObject.m +++ b/SLRemoting/SLObject.m @@ -20,6 +20,128 @@ @interface SLRepository() @end + +@implementation NSDate (SLObjectAdditions) + +static NSDateFormatter *jsonDateFormatterISO8601 = nil; // for property encoding +static NSDateFormatter *jsonDateFormatterDefault = nil; // for argument encoding + ++ (void)initJSONFormatterIfNecessary { + static dispatch_once_t pred; + dispatch_once(&pred, ^{ + // Ref: http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns + + jsonDateFormatterISO8601 = [[NSDateFormatter alloc] init]; + // 2012-12-22T21:39:22.123Z + [jsonDateFormatterISO8601 setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; + [jsonDateFormatterISO8601 setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + jsonDateFormatterDefault = [[NSDateFormatter alloc] init]; + // Wed Dec 17 2003 03:24:00 GMT+0900 + [jsonDateFormatterDefault setDateFormat:@"eee MMM dd yyyy HH:mm:ss 'GMT'ZZZ"]; + [jsonDateFormatterDefault setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + }); +} + ++ (NSDate *)dateFromJSONString:(NSString *)jsonString { + [self initJSONFormatterIfNecessary]; + + NSDate *date = [jsonDateFormatterISO8601 dateFromString:jsonString]; + if (date == nil) { + // Get rid of the "(JST)" part of "Thu Jan 01 1970 09:00:00 GMT+0900 (JST)" if exists. + NSString *jsonNoTimeZone = [[jsonString componentsSeparatedByString:@"("] objectAtIndex:0]; + date = [jsonDateFormatterDefault dateFromString:jsonNoTimeZone]; + } + return date; +} + +- (NSString *)convertToJSONString { + [self.class initJSONFormatterIfNecessary]; + + return [jsonDateFormatterISO8601 stringFromDate:self]; +} + +@end + +@implementation NSArray (SLObjectAdditions) + +- (NSMutableData*) convertToMutableData { + NSUInteger n = [self count]; + NSMutableData *data = [NSMutableData dataWithLength: n]; + char *p = [data mutableBytes]; + for (int i = 0; i < [self count]; i++) { + *p++ = [[self objectAtIndex:i] charValue]; + } + return data; +} + +@end + +@implementation NSMutableData (SLObjectAdditions) + ++ (NSMutableData *)dataFromJSONObject:(NSDictionary *)jsonObject { + // jsonObject is assumed to be a Node.js Buffer object + if ([jsonObject count] != 2 || + ![(NSString *)(jsonObject[@"type"]) isEqualToString:@"Buffer"] || + ![jsonObject[@"data"] isKindOfClass:[NSArray class]]) { + + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat: + @"Argument doesn't follow the Buffer serialization format: %@", + jsonObject] + userInfo:nil]; + } + NSArray *array = jsonObject[@"data"]; + NSMutableData *data = [array convertToMutableData]; + + return data; +} + +@end + +@implementation NSData (SLObjectAdditions) + +- (NSDictionary *)convertToJSONObject { + NSMutableDictionary *jsonObject = [NSMutableDictionary dictionary]; + [jsonObject setObject:@"Buffer" forKey:@"type"]; + + NSMutableArray *array = [NSMutableArray array]; + [self enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) { + for (NSInteger i = 0; i < byteRange.length; i++) { + [array addObject:[NSNumber numberWithChar:((char*)bytes)[i]]]; + } + }]; + [jsonObject setObject:array forKey:@"data"]; + + return jsonObject; +} + +@end + +@implementation CLLocation (SLObjectAdditions) + ++ (CLLocation *)locationFromJSONObject:(NSDictionary *)jsonObject { + if ([jsonObject count] != 2 || jsonObject[@"lat"] == nil || jsonObject[@"lng"] == nil) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat: + @"Argument doesn't follow the GeoPoint serialization format: %@", + jsonObject] + userInfo:nil]; + } + CLLocationDegrees lat = ((NSNumber *)jsonObject[@"lat"]).doubleValue; + CLLocationDegrees lng = ((NSNumber *)jsonObject[@"lng"]).doubleValue; + CLLocation *location = [[CLLocation alloc] initWithLatitude:lat longitude:lng]; + + return location; +} + +- (NSDictionary *)convertToJSONObject { + return @{ @"lat": @( self.coordinate.latitude ), @"lng": @( self.coordinate.longitude ) }; +} + +@end + + @implementation SLObject NSString *SLObjectInvalidRepositoryDescription = @"Invalid repository."; @@ -35,7 +157,7 @@ - (instancetype)initWithRepository:(SLRepository *)repository if (self) { self.repository = repository; - self.creationParameters = parameters; + self.creationParameters = [SLObject convertArguments:parameters]; } return self; @@ -53,7 +175,7 @@ - (void)invokeMethod:(NSString *)name [self.repository.adapter invokeInstanceMethod:path constructorParameters:self.creationParameters - parameters:parameters + parameters:[SLObject convertArguments:parameters] bodyParameters:nil outputStream:nil success:success @@ -73,8 +195,8 @@ - (void)invokeMethod:(NSString *)name [self.repository.adapter invokeInstanceMethod:path constructorParameters:self.creationParameters - parameters:parameters - bodyParameters:bodyParameters + parameters:[SLObject convertArguments:parameters] + bodyParameters:[SLObject convertProperties:bodyParameters] outputStream:nil success:success failure:failure]; @@ -94,13 +216,97 @@ - (void)invokeMethod:(NSString *)name [self.repository.adapter invokeInstanceMethod:path constructorParameters:self.creationParameters - parameters:parameters + parameters:[SLObject convertArguments:parameters] bodyParameters:nil outputStream:outputStream success:success failure:failure]; } ++ (NSDictionary *)convertArguments:(NSDictionary *)prop { + NSMutableDictionary *converted = [NSMutableDictionary dictionary]; + for(id key in prop) { + id value = [prop objectForKey:key]; + if ([value isKindOfClass:[NSDate class]]) { + NSString *jsonString = [value convertToJSONString]; + value = @{ @"$type": @"date", @"$data": jsonString }; + } else if ([value isKindOfClass:[NSData class]]) { + NSString *base64string = [value base64EncodedStringWithOptions:0]; + value = @{ @"$type": @"base64", @"$data": base64string }; + } else if ([value isKindOfClass:[CLLocation class]]) { + value = [value convertToJSONObject]; + } + [converted setValue:value forKey:key]; + } + return converted; +} + ++ (NSDictionary *)convertProperties:(NSDictionary *)prop { + NSMutableDictionary *converted = [NSMutableDictionary dictionary]; + for(id key in prop) { + id value = [prop objectForKey:key]; + if ([value isKindOfClass:[NSDate class]]) { + value = [value convertToJSONString]; + } else if ([value isKindOfClass:[NSData class]]) { + value = [value convertToJSONObject]; + } else if ([value isKindOfClass:[CLLocation class]]) { + value = [value convertToJSONObject]; + } + [converted setValue:value forKey:key]; + } + return converted; +} + ++ (NSDate *)dateFromEncodedArgument:(NSDictionary *)value { + + if ([value count] != 2 || + ![(NSString *)(value[@"$type"]) isEqualToString:@"date"] || + ![value[@"$data"] isKindOfClass:[NSString class]]) { + + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat: + @"Argument doesn't follow the Date serialization format: %@", + value] + userInfo:nil]; + } + NSString *dateString = (NSString *)value[@"$data"]; + NSDate *date = [NSDate dateFromJSONString:dateString]; + return date; +} + ++ (NSData *)dataFromEncodedArgument:(NSDictionary *)value { + + if ([value count] != 2 || + ![(NSString *)(value[@"$type"]) isEqualToString:@"base64"] || + ![value[@"$data"] isKindOfClass:[NSString class]]) { + + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat: + @"Argument doesn't follow the Buffer serialization format: %@", + value] + userInfo:nil]; + } + NSString *base64String = (NSString *)value[@"$data"]; + NSData *data = [[NSData alloc]initWithBase64EncodedString:base64String options:0]; + return data; +} + ++ (CLLocation *)locationFromEncodedArgument:(NSDictionary *)value { + return [CLLocation locationFromJSONObject:value]; +} + ++ (NSDate *)dateFromEncodedProperty:(NSString *)value { + return [NSDate dateFromJSONString:value]; +} + ++ (NSMutableData *)dataFromEncodedProperty:(NSDictionary *)value { + return [NSMutableData dataFromJSONObject:value]; +} + ++ (CLLocation *)locationFromEncodedProperty:(NSDictionary *)value { + return [CLLocation locationFromJSONObject:value]; +} + @end @implementation SLRepository @@ -130,7 +336,7 @@ - (void)invokeStaticMethod:(NSString *)name NSString *path = [NSString stringWithFormat:@"%@.%@", self.className, name]; [self.adapter invokeStaticMethod:path - parameters:parameters + parameters:[SLObject convertArguments:parameters] bodyParameters:nil outputStream:nil success:success @@ -145,8 +351,8 @@ - (void)invokeStaticMethod:(NSString *)name NSString *path = [NSString stringWithFormat:@"%@.%@", self.className, name]; [self.adapter invokeStaticMethod:path - parameters:parameters - bodyParameters:bodyParameters + parameters:[SLObject convertArguments:parameters] + bodyParameters:[SLObject convertProperties:bodyParameters] outputStream:nil success:success failure:failure]; @@ -160,7 +366,7 @@ - (void)invokeStaticMethod:(NSString *)name NSString *path = [NSString stringWithFormat:@"%@.%@", self.className, name]; [self.adapter invokeStaticMethod:path - parameters:parameters + parameters:[SLObject convertArguments:parameters] bodyParameters:nil outputStream:outputStream success:success