From c971e53ae427b5f5ffcdb4e2d130ec5ce302936f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 2 Jul 2014 08:20:53 -0700 Subject: [PATCH] Add SLRemoting --- SLRemoting/SLAdapter.h | 150 +++++++++++++++++++++++ SLRemoting/SLAdapter.m | 76 ++++++++++++ SLRemoting/SLObject.h | 135 ++++++++++++++++++++ SLRemoting/SLObject.m | 94 ++++++++++++++ SLRemoting/SLRESTAdapter.h | 30 +++++ SLRemoting/SLRESTAdapter.m | 192 +++++++++++++++++++++++++++++ SLRemoting/SLRESTContract.h | 203 +++++++++++++++++++++++++++++++ SLRemoting/SLRESTContract.m | 164 +++++++++++++++++++++++++ SLRemoting/SLRemoting-Prefix.pch | 7 ++ SLRemoting/SLRemoting.h | 17 +++ SLRemoting/SLRemotingUtils.h | 41 +++++++ SLRemoting/SLRemotingUtils.m | 12 ++ 12 files changed, 1121 insertions(+) create mode 100644 SLRemoting/SLAdapter.h create mode 100644 SLRemoting/SLAdapter.m create mode 100644 SLRemoting/SLObject.h create mode 100644 SLRemoting/SLObject.m create mode 100644 SLRemoting/SLRESTAdapter.h create mode 100644 SLRemoting/SLRESTAdapter.m create mode 100644 SLRemoting/SLRESTContract.h create mode 100644 SLRemoting/SLRESTContract.m create mode 100644 SLRemoting/SLRemoting-Prefix.pch create mode 100644 SLRemoting/SLRemoting.h create mode 100644 SLRemoting/SLRemotingUtils.h create mode 100644 SLRemoting/SLRemotingUtils.m diff --git a/SLRemoting/SLAdapter.h b/SLRemoting/SLAdapter.h new file mode 100644 index 0000000..87df511 --- /dev/null +++ b/SLRemoting/SLAdapter.h @@ -0,0 +1,150 @@ +/** + * @file SLAdapter.h + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import + +/** + * Blocks of this type are executed for any successful method invocation, i.e. + * one where the remote method called the callback as `callback(null, value)`. + * + * **Example:** + * @code + * [... + * success:^(id value) { + * NSLog(@"The result was: %@", value); + * } + * ...]; + * @endcode + * + * @param value The top-level value returned by the remote method, typed + * appropriately: an NSNumber for all Numbers, an + * NSDictionary for all Objects, etc. + */ +typedef void (^SLSuccessBlock)(id value); + +/** + * Blocks of this type are executed for any failed method invocation, i.e. one + * where the remote method called the callback as `callback(error, null)` or + * just `callback(error)`. + * + * **Example:** + * @code + * [... + * success:^(id value) { + * NSLog(@"The result was: %@", value); + * } + * ...]; + * @endcode + * + * @param error The error received, as a properly-formatted + * NSError. + */ +typedef void (^SLFailureBlock)(NSError *error); + +/** + * An error description for SLAdapters that are not connected to any server. + * Errors with this description will be passed to the SLFailureBlock associated + * with a request made of a disconnected Adapter. + */ +extern NSString *SLAdapterNotConnectedErrorDescription; + +/** + * The entry point to all networking accomplished with LoopBack. Adapters + * encapsulate information consistent to all networked operations, such as base + * URL, port, etc. + */ +@interface SLAdapter : NSObject + +/** YES if the SLAdapter is connected to a server, NO otherwise. */ +@property (readonly, nonatomic) BOOL connected; + +/** A flag to control if invalid SSL certificates are allowed */ +@property (readonly, nonatomic) BOOL allowsInvalidSSLCertificate; + +/** + * Returns a new, disconnected Adapter. + * + * @return A disconnected Adapter. + */ ++ (instancetype)adapter; + +/** + * Returns a new Adapter connected to `url`. + * + * @param url The URL to connect to. + * @return A connected Adapter. + */ ++ (instancetype)adapterWithURL:(NSURL *)url; + +/** + * Returns a new Adapter connected to `url`. + * + * @param url The URL to connect to. + * @param allowsInvalidSSLCertificate Is invalid SSL certificate allowed? + * @return A connected Adapter. + */ ++ (instancetype)adapterWithURL:(NSURL *)url allowsInvalidSSLCertificate : (BOOL) allowsInvalidSSLCertificate; + +/** + * Initializes a new Adapter, connecting it to `url`. + * + * @param url The URL to connect to. + * @return The connected Adapter. + */ +- (instancetype)initWithURL:(NSURL *)url allowsInvalidSSLCertificate : (BOOL) allowsInvalidSSLCertificate ; + +/** + * Connects the Adapter to `url`. + * + * @param url The URL to connect to. + */ +- (void)connectToURL:(NSURL *)url; + +/** + * Invokes a remotable method exposed statically on the server. + * + * Unlike SLAdapter::invokeInstanceMethod:constructorParameters:parameters:success:failure:, + * no object needs to be created on the server. + * + * @param method The method to invoke, e.g. `module.doSomething`. + * @param parameters The parameters to invoke with. + * @param success An SLSuccessBlock to be executed when the invocation + * succeeds. + * @param failure An SLFailureBlock to be executed when the invocation + * fails. + */ +- (void)invokeStaticMethod:(NSString *)method + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure; + +/** + * Invokes a remotable method exposed within a prototype on the server. + * + * This should be thought of as a two-step process. First, the server loads or + * creates an object with the appropriate type. Then and only then is the method + * invoked on that object. The two parameter dictionaries correspond to these + * two steps: `creationParameters` for the former, and `parameters` for the + * latter. + * + * @param method The method to invoke, e.g. + * `MyClass.prototype.doSomething`. + * @param constructorParameters The parameters the virual object should be + * created with. + * @param parameters The parameters to invoke with. + * @param success An SLSuccessBlock to be executed when the + * invocation succeeds. + * @param failure An SLFailureBlock to be executed when the + * invocation fails. + */ +- (void)invokeInstanceMethod:(NSString *)method + constructorParameters:(NSDictionary *)constructorParameters + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure; + +@end diff --git a/SLRemoting/SLAdapter.m b/SLRemoting/SLAdapter.m new file mode 100644 index 0000000..e19ec42 --- /dev/null +++ b/SLRemoting/SLAdapter.m @@ -0,0 +1,76 @@ +/** + * @file SLAdapter.m + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import "SLAdapter.h" + +NSString *SLAdapterNotConnectedErrorDescription = @"Adapter not connected."; + +@interface SLAdapter() + +@property (readwrite, nonatomic) BOOL connected; +@property (readwrite, nonatomic) BOOL allowsInvalidSSLCertificate; + +@end + +@implementation SLAdapter + ++ (instancetype)adapter { + return [self adapterWithURL:nil]; +} + ++ (instancetype)adapterWithURL:(NSURL *)url { + return [self adapterWithURL:url allowsInvalidSSLCertificate:NO]; +} + ++ (instancetype)adapterWithURL:(NSURL *)url allowsInvalidSSLCertificate : (BOOL) allowsInvalidSSLCertificate { + return [[self alloc] initWithURL:url allowsInvalidSSLCertificate:allowsInvalidSSLCertificate]; +} + +- (instancetype)init { + return [self initWithURL:nil]; +} + +- (instancetype)initWithURL:(NSURL *)url { + return [self initWithURL:url allowsInvalidSSLCertificate:NO]; +} + +- (instancetype)initWithURL:(NSURL *)url allowsInvalidSSLCertificate : (BOOL) allowsInvalidSSLCertificate { + self = [super init]; + + if (self) { + self.allowsInvalidSSLCertificate = allowsInvalidSSLCertificate; + self.connected = NO; + + if(url) { + [self connectToURL:url]; + } + } + + return self; +} + +- (void)connectToURL:(NSURL *)url { + // TODO(schoon) - Break out and document error description. + NSAssert(NO, @"Invalid Adapter."); +} + +- (void)invokeStaticMethod:(NSString *)path + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + NSAssert(NO, @"Invalid Adapter."); +} + +- (void)invokeInstanceMethod:(NSString *)path + constructorParameters:(NSDictionary *)constructorParameters + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + NSAssert(NO, @"Invalid Adapter."); +} + +@end diff --git a/SLRemoting/SLObject.h b/SLRemoting/SLObject.h new file mode 100644 index 0000000..0be71fe --- /dev/null +++ b/SLRemoting/SLObject.h @@ -0,0 +1,135 @@ +/** + * @file SLObject.h + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import + +#import "SLAdapter.h" + +/** + * An error description for SLObjects with an invalid repository, which happens + * when SLObjects are created improperly. + */ +extern NSString *SLObjectInvalidRepositoryDescription; + +@class SLRepository; + +/** + * A local representative of a single virtual object. The behaviour of this + * object is defined through a prototype defined on the server, and the identity + * of this instance is defined through its `creationParameters`. + */ +@interface SLObject : NSObject + +/** The SLRepository defining the type of this object. */ +@property (readonly, nonatomic, weak) SLRepository *repository; + +/** + * The complete set of parameters to be used to identify/create this object on + * the server. + */ +@property (readonly, nonatomic, strong) NSDictionary *creationParameters; + +/** + * Returns a new object with the type defined by given repository. + * + * @param repository The repository this object is associated with. + * @param parameters The creationParameters of the new object. + * @return A new object. + */ ++ (instancetype)objectWithRepository:(SLRepository *)repository + parameters:(NSDictionary *)parameters; + +/** + * Initializes a new object with the type defined by the given repository. + * + * @param repository The repository this object is associated with. + * @param parameters The creationParameters of the new object. + * @return The new object. + */ +- (instancetype)initWithRepository:(SLRepository *)repository + parameters:(NSDictionary *)parameters; + +/** + * Invokes a remotable method exposed within instances of this class on the + * server. + * + * @see SLAdapter::invokeInstanceMethod:constructorParameters:parameters:success:failure: + * + * @param name The method to invoke (without the prototype), e.g. + * `doSomething`. + * @param parameters The parameters to invoke with. + * @param success An SLSuccessBlock to be executed when the invocation + * succeeds. + * @param failure An SLFailureBlock to be executed when the invocation + * fails. + */ +- (void)invokeMethod:(NSString *)name + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure; + +@end + +/** + * A local representative of classes ("prototypes" in JavaScript) defined and + * made remotable on the server. + */ +@interface SLRepository : NSObject + +/** The name given to this class on the server. */ +@property (readonly, nonatomic, copy) NSString *className; + +/** + * The SLAdapter that should be used for invoking methods, both for static + * methods on this repository and all methods on all instances of this class. + */ +@property (readwrite, nonatomic) SLAdapter *adapter; + +/** + * Returns a new Repository representing the named remote class. + * + * @param name The remote class name. + * @return A repository. + */ ++ (instancetype)repositoryWithClassName:(NSString *)name; + +/** + * Initializes a new Repository, associating it with the named remote class. + * + * @param name The remote class name. + * @return The repository. + */ +- (instancetype)initWithClassName:(NSString *)name; + +/** + * Returns a new SLObject as a virtual instance of this remote class. + * + * @param parameters The `creationParameters` of the new SLObject. + * @return A new SLObject based on this class. + */ +- (SLObject *)objectWithParameters:(NSDictionary *)parameters; + +/** + * Invokes a remotable method exposed statically within this class on the + * server. + * + * @see SLAdapter::invokeStaticMethod:parameters:success:failure: + * + * @param name The method to invoke (without the class name), e.g. + * `doSomething`. + * @param parameters The parameters to invoke with. + * @param success An SLSuccessBlock to be executed when the invocation + * succeeds. + * @param failure An SLFailureBlock to be executed when the invocation + * fails. + */ +- (void)invokeStaticMethod:(NSString *)name + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure; + +@end diff --git a/SLRemoting/SLObject.m b/SLRemoting/SLObject.m new file mode 100644 index 0000000..906c8b6 --- /dev/null +++ b/SLRemoting/SLObject.m @@ -0,0 +1,94 @@ +/** + * @file SLObject.m + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import "SLObject.h" + +@interface SLObject() + +@property (readwrite, nonatomic, weak) SLRepository *repository; +@property (readwrite, nonatomic, strong) NSDictionary *creationParameters; + +@end + +@interface SLRepository() + +@property (readwrite, nonatomic, copy) NSString *className; + +@end + +@implementation SLObject + +NSString *SLObjectInvalidRepositoryDescription = @"Invalid repository."; + ++ (instancetype)objectWithRepository:(SLRepository *)repository + parameters:(NSDictionary *)parameters { + return [[self alloc] initWithRepository:repository parameters:parameters]; +} + +- (instancetype)initWithRepository:(SLRepository *)repository + parameters:(NSDictionary *)parameters { + self = [super init]; + + if (self) { + self.repository = repository; + self.creationParameters = parameters; + } + + return self; +} + +- (void)invokeMethod:(NSString *)name + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + NSAssert(self.repository, SLObjectInvalidRepositoryDescription); + + NSString *path = [NSString stringWithFormat:@"%@.prototype.%@", + self.repository.className, + name]; + + [self.repository.adapter invokeInstanceMethod:path + constructorParameters:self.creationParameters + parameters:parameters + success:success + failure:failure]; +} + +@end + +@implementation SLRepository + ++ (instancetype)repositoryWithClassName:(NSString *)name { + return [[self alloc] initWithClassName:name]; +} + +- (instancetype)initWithClassName:(NSString *)name { + self = [super init]; + + if (self) { + self.className = name; + } + + return self; +} + +- (SLObject *)objectWithParameters:(NSDictionary *)parameters { + return [SLObject objectWithRepository:self parameters:parameters]; +} + +- (void)invokeStaticMethod:(NSString *)name + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + NSString *path = [NSString stringWithFormat:@"%@.%@", self.className, name]; + [self.adapter invokeStaticMethod:path + parameters:parameters + success:success + failure:failure]; +} + +@end diff --git a/SLRemoting/SLRESTAdapter.h b/SLRemoting/SLRESTAdapter.h new file mode 100644 index 0000000..020eae7 --- /dev/null +++ b/SLRemoting/SLRESTAdapter.h @@ -0,0 +1,30 @@ +/** + * @file SLRESTAdapter.h + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import "SLRemotingUtils.h" +#import "SLAdapter.h" +#import "SLRESTContract.h" + +/** + * A specific SLAdapter implementation for RESTful servers. + * + * In addition to implementing the SLAdapter interface, SLRESTAdapter contains a + * single SLRESTContract to map remote methods to custom HTTP routes. _This is + * only required if the HTTP settings have been customized on the server._ When + * in doubt, try without. + * + * @see SLRESTContract + */ +@interface SLRESTAdapter : SLAdapter + +/** A custom contract for fine-grained route configuration. */ +@property (readwrite, nonatomic, strong) SLRESTContract *contract; + +/** Set the given access token in the header for all RESTful interaction. */ +@property (nonatomic) NSString* accessToken; + +@end diff --git a/SLRemoting/SLRESTAdapter.m b/SLRemoting/SLRESTAdapter.m new file mode 100644 index 0000000..30e31c6 --- /dev/null +++ b/SLRemoting/SLRESTAdapter.m @@ -0,0 +1,192 @@ +/** + * @file SLRESTAdapter.m + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import "SLRESTAdapter.h" + +#import "SLAFHTTPClient.h" +#import "SLAFJSONRequestOperation.h" + +static NSString * const DEFAULT_DEV_BASE_URL = @"http://localhost:3001"; + +@interface SLRESTAdapter() { + SLAFHTTPClient *client; +} + +@property (readwrite, nonatomic) BOOL connected; + +- (void)requestPath:(NSString *)path + verb:(NSString *)verb + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure; + +- (void)requestMultipartPath:(NSString *)path + verb:(NSString *)verb + fileName:(NSString *)fileName + localURL:(NSString *)localURL + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure; + +@end + +@implementation SLRESTAdapter + +- (instancetype)initWithURL:(NSURL *)url allowsInvalidSSLCertificate : (BOOL) allowsInvalidSSLCertificate { + self = [super initWithURL:url allowsInvalidSSLCertificate:allowsInvalidSSLCertificate]; + + if (self) { + self.contract = [SLRESTContract contract]; + } + + return self; +} + +- (void)connectToURL:(NSURL *)url { + // Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected + if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) { + url = [url URLByAppendingPathComponent:@"/"]; + } + + client = [SLAFHTTPClient clientWithBaseURL:url]; + client.allowsInvalidSSLCertificate = self.allowsInvalidSSLCertificate; + + self.connected = YES; + + client.parameterEncoding = AFJSONParameterEncoding; + [client registerHTTPOperationClass:[SLAFJSONRequestOperation class]]; + [client setDefaultHeader:@"Accept" value:@"application/json"]; +} + +- (void)invokeStaticMethod:(NSString *)method + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + NSAssert(self.contract, @"Invalid contract."); + + NSString *verb = [self.contract verbForMethod:method]; + NSString *path = [self.contract urlForMethod:method parameters:parameters]; + + [self requestPath:path + verb:verb + parameters:parameters + success:success + failure:failure]; +} + +- (void)invokeInstanceMethod:(NSString *)method + constructorParameters:(NSDictionary *)constructorParameters + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + // TODO(schoon) - Break out and document error description. + NSAssert(self.contract, @"Invalid contract."); + + NSMutableDictionary *combinedParameters = [NSMutableDictionary dictionary]; + [combinedParameters addEntriesFromDictionary:constructorParameters]; + [combinedParameters addEntriesFromDictionary:parameters]; + + NSString *verb = [self.contract verbForMethod:method]; + NSString *path = [self.contract urlForMethod:method parameters:combinedParameters]; + + if ([self.contract multipartForMethod:method]) { + [self requestMultipartPath:path + verb:verb + fileName:parameters[@"name"] + localURL:parameters[@"localPath"] + success:success + failure:failure]; + } else { + [self requestPath:path + verb:verb + parameters:combinedParameters + success:success + failure:failure]; + } +} + +- (void)requestPath:(NSString *)path + verb:(NSString *)verb + parameters:(NSDictionary *)parameters + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + NSAssert(self.connected, SLAdapterNotConnectedErrorDescription); + + if ([[verb uppercaseString] isEqualToString:@"GET"]) { + client.parameterEncoding = AFFormURLParameterEncoding; + } else { + client.parameterEncoding = AFJSONParameterEncoding; + } + + // Remove the leading / so that the path is treated as relative to the baseURL + if ([path hasPrefix:@"/"]) { + path = [path substringFromIndex:1]; + } + + NSURLRequest *request = [client requestWithMethod:verb path:path parameters:parameters]; + SLAFHTTPRequestOperation *operation = [client HTTPRequestOperationWithRequest:request success:^(SLAFHTTPRequestOperation *operation, id responseObject) { + success(responseObject); + } failure:^(SLAFHTTPRequestOperation *operation, NSError *error) { + failure(error); + }]; + [client enqueueHTTPRequestOperation:operation]; +} + +- (void)requestMultipartPath:(NSString *)path + verb:(NSString *)verb + fileName:(NSString *)fileName + localURL:(NSString *)localURL + success:(SLSuccessBlock)success + failure:(SLFailureBlock)failure { + NSAssert(self.connected, SLAdapterNotConnectedErrorDescription); + + // Remove the leading / so that the path is treated as relative to the baseURL + if ([path hasPrefix:@"/"]) { + path = [path substringFromIndex:1]; + } + + NSURLRequest *request; + NSOutputStream *outStream = nil; + if ([[verb uppercaseString] isEqualToString:@"GET"]) { + path = [path stringByAppendingPathComponent:fileName]; + + request = [client requestWithMethod:verb path:path parameters:nil]; + outStream = [NSOutputStream outputStreamToFileAtPath:[localURL stringByAppendingPathComponent:fileName] append:NO]; + } else { + request = [client multipartFormRequestWithMethod:verb path:path parameters:nil constructingBodyWithBlock: ^(id formData) { + NSString* fullLocalPath = [localURL stringByAppendingPathComponent:fileName]; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullLocalPath error:NULL]; + [formData appendPartWithInputStream:[[NSInputStream alloc] initWithFileAtPath:fullLocalPath] + name:@"uploadfiles" + fileName:fileName + length:attributes.fileSize + mimeType:@"multipart/form-data"]; + }]; + } + + SLAFHTTPRequestOperation *operation = [client HTTPRequestOperationWithRequest:request success:^(SLAFHTTPRequestOperation *operation, id responseObject) { + success(responseObject); + } failure:^(SLAFHTTPRequestOperation *operation, NSError *error) { + failure(error); + }]; + if (outStream != nil) { + operation.outputStream = outStream; + } + + [client enqueueHTTPRequestOperation:operation]; +} + +- (NSString*)accessToken +{ + return [client defaultValueForHeader:@"Authorization"]; +} + +- (void)setAccessToken:(NSString *)accessToken +{ + [client setDefaultHeader:@"Authorization" value:accessToken]; +} + +@end diff --git a/SLRemoting/SLRESTContract.h b/SLRemoting/SLRESTContract.h new file mode 100644 index 0000000..af4c473 --- /dev/null +++ b/SLRemoting/SLRESTContract.h @@ -0,0 +1,203 @@ +/** + * @file SLRESTContract.h + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import + +/** The verb SLRemoting uses when no verb has been specified by the server. */ +extern NSString *SLRESTContractDefaultVerb; + +/** + * A single item within a larger SLRESTContract, encapsulation a single route's + * verb and pattern, e.g. GET /widgets/:id. + */ +@interface SLRESTContractItem : NSObject + +/** The pattern corresponding to this route, e.g. `/widgets/:id`. */ +@property (readonly, nonatomic, copy) NSString *pattern; +/** The verb corresponding to this route, e.g. `GET`. */ +@property (readonly, nonatomic, copy) NSString *verb; +/** Indication that this item is a multipart form mime type. */ +@property (readonly, nonatomic, assign) BOOL multipart; + +/** + * Returns a new item encapsulating the given pattern. + * + * @param pattern The pattern to represent. + * @return A new item. + */ ++ (instancetype)itemWithPattern:(NSString *)pattern; + +/** + * Initializes a new item to encapsulate the given pattern. + * + * @param pattern The pattern to represent. + * @return The item. + */ +- (instancetype)initWithPattern:(NSString *)pattern; + +/** + * Returns a new item encapsulating the given pattern and verb. + * + * @param pattern The pattern to represent. + * @param verb The verb to represent. + * @return A new item. + */ ++ (instancetype)itemWithPattern:(NSString *)pattern verb:(NSString *)verb; + +/** + * Initializes a new item encapsulating the given pattern and verb. + * + * @param pattern The pattern to represent. + * @param verb The verb to represent. + * @return A new item. + */ +- (instancetype)initWithPattern:(NSString *)pattern verb:(NSString *)verb; + +/** + * Returns a new item encapsulating the given pattern, verb and multipart setting. + * + * @param pattern The pattern to represent. + * @param verb The verb to represent. + * @param multiplart Indicates this item is a multipart mime type. + * @return A new item. + */ ++ (instancetype)itemWithPattern:(NSString *)pattern verb:(NSString *)verb multipart:(BOOL)multipart; + +/** + * Initializes a new item encapsulating the given pattern and verb. + * + * @param pattern The pattern to represent. + * @param verb The verb to represent. + * @param multiplart Indicates this item is a multipart mime type. + * @return A new item. + */ +- (instancetype)initWithPattern:(NSString *)pattern verb:(NSString *)verb multipart:(BOOL)multipart; + +@end + +/** + * A contract specifies how remote method names map to HTTP routes. + * + * For example, if a remote method on the server has been remapped like so: + * + * @code{.js} + * project.getObject = function (id, callback) { + * callback(null, { ... }); + * }; + * helper.method(project.getObject, { + * http: { verb: 'GET', path: '/:id'}, + * accepts: { name: 'id', type: 'string' } + * returns: { name: 'object', type: 'object' } + * }) + * @endcode + * + * The new route is GET /:id, instead of POST /project/getObject, so we + * need to update our contract on the client: + * + * @code{.m} + * [contract addItem:[SLRESTContractItem itemWithPattern:@"/:id" verb:@"GET"] + * forMethod:@"project.getObject"]; + * @endcode + */ +@interface SLRESTContract : NSObject + +/** + * A read-only representation of the internal contract. Used for + * SLRESTContract::addItemsFromContract:. + */ +@property (readonly, nonatomic) NSDictionary *dict; + +/** + * Returns a new, empty contract. + * + * @return A new, empty contract. + */ ++ (instancetype)contract; + +/** + * Adds a single item to this contract. The item can be shared among different + * contracts, managed by the sum of all contracts that contain it. Similarly, + * each item can be used for more than one method, like so: + * + * @code{.m} + * SLRESTContractItem *upsert = [SLRESTContractItem itemWithPattern:@"/widgets/:id" + * andVerb:@"PUT"]; + * [contract addItem:upsert forMethod:@"widgets.create"]; + * [contract addItem:upsert forMethod:@"widgets.update"]; + * @endcode + * + * @param item The item to add to this contract. + * @param method The method the item should represent. + */ +- (void)addItem:(SLRESTContractItem *)item forMethod:(NSString *)method; + +/** + * Adds all items from contract. + * + * @see addItem:forMethod: + * + * @param contract The contract to copy from. + */ +- (void)addItemsFromContract:(SLRESTContract *)contract; + +/** + * Resolves a specific method, replacing pattern fragments with the optional + * `parameters` as appropriate. + * + * @param method The method to resolve. + * @param parameters Pattern parameters. Can be `nil`. + * @return The complete, resolved URL. + */ +- (NSString *)urlForMethod:(NSString *)method + parameters:(NSDictionary *)parameters; + +/** + * Returns the HTTP verb for the given method string. + * + * @param method The method to resolve. + * @return The resolved verb. + */ +- (NSString *)verbForMethod:(NSString *)method; + +/** + * Returns the multipart setting for the given method string. + * + * @param method The method to resolve. + * @return The mutipart setting. + */ +- (BOOL)multipartForMethod:(NSString *)method; + +/** + * Generates a fallback URL for a method whose contract has not been customized. + * + * @param method The method to generate from. + * @return The resolved URL. + */ +- (NSString *)urlForMethodWithoutItem:(NSString *)method; + +/** + * Returns the custom pattern representing the given method string, or `nil` if + * no custom pattern exists. + * + * @param method The method to resolve. + * @return The custom pattern if one exists, `nil` otherwise. + */ +- (NSString *)patternForMethod:(NSString *)method; + +/** + * Returns a rendered URL pattern using the parameters provided. For example, + * `@"/widgets/:id"` + `@{ @"id": "57", @"price": @"42.00" }` begets + * `@"/widgets/57"`. + * + * @param pattern The pattern to render. + * @param parameters Values to render with. + * @return The rendered URL. + */ +- (NSString *)urlWithPattern:(NSString *)pattern + parameters:(NSDictionary *)parameters; + +@end diff --git a/SLRemoting/SLRESTContract.m b/SLRemoting/SLRESTContract.m new file mode 100644 index 0000000..be71d7c --- /dev/null +++ b/SLRemoting/SLRESTContract.m @@ -0,0 +1,164 @@ +/** + * @file SLRESTContract.m + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import "SLRESTContract.h" + +NSString *SLRESTContractDefaultVerb = @"POST"; + +@interface SLRESTContractItem() + +@property (readwrite, nonatomic, copy) NSString *pattern; +@property (readwrite, nonatomic, copy) NSString *verb; +@property (readwrite, nonatomic, assign) BOOL multipart; + +@end + +@interface SLRESTContract() + +@property (readwrite, nonatomic) NSDictionary *dict; + +@end + +@implementation SLRESTContractItem + ++ (instancetype)itemWithPattern:(NSString *)pattern { + return [[self alloc] initWithPattern:pattern]; +} + +- (instancetype)initWithPattern:(NSString *)pattern { + return [self initWithPattern:pattern verb:@"POST"]; +} + ++ (instancetype)itemWithPattern:(NSString *)pattern + verb:(NSString *)verb { + return [[self alloc] initWithPattern:pattern verb:verb]; +} + +- (instancetype)initWithPattern:(NSString *)pattern + verb:(NSString *)verb { + self = [super init]; + + if (self) { + self.pattern = pattern; + self.verb = verb; + } + + return self; +} + ++ (instancetype)itemWithPattern:(NSString *)pattern + verb:(NSString *)verb + multipart:(BOOL)multipart { + return [[self alloc] initWithPattern:pattern verb:verb multipart:multipart]; +} + +- (instancetype)initWithPattern:(NSString *)pattern + verb:(NSString *)verb + multipart:(BOOL)multipart { + self = [super init]; + + if (self) { + self.pattern = pattern; + self.verb = verb; + self.multipart = multipart; + } + + return self; +} + +@end + +@implementation SLRESTContract + ++ (instancetype)contract { + return [[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + + if (self) { + self.dict = [NSMutableDictionary dictionary]; + } + + return self; +} + +- (void)addItem:(SLRESTContractItem *)item + forMethod:(NSString *)method { + NSParameterAssert(item); + NSParameterAssert(method); + + ((NSMutableDictionary *)self.dict)[method] = item; +} + +- (void)addItemsFromContract:(SLRESTContract *)contract { + NSParameterAssert(contract); + + [(NSMutableDictionary *)self.dict addEntriesFromDictionary:contract.dict]; +} + +- (NSString *)urlForMethod:(NSString *)method + parameters:(NSDictionary *)parameters { + NSParameterAssert(method); + + NSString *pattern = [self patternForMethod:method]; + + if (pattern) { + return [self urlWithPattern:pattern parameters:parameters]; + } else { + return [self urlForMethodWithoutItem:method]; + } +} + +- (NSString *)verbForMethod:(NSString *)method { + NSParameterAssert(method); + + SLRESTContractItem *item = (SLRESTContractItem *)self.dict[method]; + + return item ? item.verb : @"POST"; +} + +- (BOOL)multipartForMethod:(NSString *)method +{ + NSParameterAssert(method); + + SLRESTContractItem *item = (SLRESTContractItem *)self.dict[method]; + + return item.multipart; +} + +- (NSString *)urlForMethodWithoutItem:(NSString *)method { + return [method stringByReplacingOccurrencesOfString:@"." withString:@"/"]; +} + +- (NSString *)patternForMethod:(NSString *)method { + NSParameterAssert(method); + + SLRESTContractItem *item = (SLRESTContractItem *)self.dict[method]; + + return item ? item.pattern : nil; +} + +- (NSString *)urlWithPattern:(NSString *)pattern + parameters:(NSDictionary *)parameters { + NSParameterAssert(pattern); + + if (!parameters) { + return pattern; + } + + NSString __block *url = pattern; + + [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + url = [url stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@":%@", key] withString:[NSString stringWithFormat:@"%@", obj]]; + }]; + + return url; +} + +@end diff --git a/SLRemoting/SLRemoting-Prefix.pch b/SLRemoting/SLRemoting-Prefix.pch new file mode 100644 index 0000000..8c52e75 --- /dev/null +++ b/SLRemoting/SLRemoting-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'SLRemoting' target in the 'SLRemoting' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/SLRemoting/SLRemoting.h b/SLRemoting/SLRemoting.h new file mode 100644 index 0000000..6401a8d --- /dev/null +++ b/SLRemoting/SLRemoting.h @@ -0,0 +1,17 @@ +/** + * @file SLRemoting.h + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#ifndef __SL_REMOTING_H +#define __SL_REMOTING_H + +#import "SLRemotingUtils.h" +#import "SLAdapter.h" +#import "SLRESTAdapter.h" +#import "SLRESTContract.h" +#import "SLObject.h" + +#endif // __SL_REMOTING_H diff --git a/SLRemoting/SLRemotingUtils.h b/SLRemoting/SLRemotingUtils.h new file mode 100644 index 0000000..99a9581 --- /dev/null +++ b/SLRemoting/SLRemotingUtils.h @@ -0,0 +1,41 @@ +/** + * @file SLRemotingUtils.h + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import + +/** + * Marks the start of an asynchronous unit test. + */ +#define ASYNC_TEST_START dispatch_semaphore_t sen_semaphore = dispatch_semaphore_create(0); + +/** + * Marks the end of an asynchronous unit test. + */ +#define ASYNC_TEST_END \ +while (dispatch_semaphore_wait(sen_semaphore, DISPATCH_TIME_NOW)) \ + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; + +/** + * Signals the completion of an asynchronous unit test. + */ +#define ASYNC_TEST_SIGNAL dispatch_semaphore_signal(sen_semaphore); + +/** + * Fails an asynchronous unit test, additionally signaling its completion. + */ +#define ASYNC_TEST_FAILURE_BLOCK \ +^(NSError *error) { \ + STFail(error.description); \ + ASYNC_TEST_SIGNAL \ +} + +/** + * A container for helper methods. + */ +@interface SLRemotingUtils : NSObject + +@end diff --git a/SLRemoting/SLRemotingUtils.m b/SLRemoting/SLRemotingUtils.m new file mode 100644 index 0000000..2e7fe11 --- /dev/null +++ b/SLRemoting/SLRemotingUtils.m @@ -0,0 +1,12 @@ +/** + * @file SLRemotingUtils.m + * + * @author Michael Schoonmaker + * @copyright (c) 2013 StrongLoop. All rights reserved. + */ + +#import "SLRemotingUtils.h" + +@implementation SLRemotingUtils + +@end