diff options
Diffstat (limited to 'src/objective-c/ProtoRPC')
-rw-r--r-- | src/objective-c/ProtoRPC/ProtoRPC.h | 118 | ||||
-rw-r--r-- | src/objective-c/ProtoRPC/ProtoRPC.m | 227 | ||||
-rw-r--r-- | src/objective-c/ProtoRPC/ProtoService.h | 35 | ||||
-rw-r--r-- | src/objective-c/ProtoRPC/ProtoService.m | 67 |
4 files changed, 439 insertions, 8 deletions
diff --git a/src/objective-c/ProtoRPC/ProtoRPC.h b/src/objective-c/ProtoRPC/ProtoRPC.h index 45d3526092..8ce3421cc1 100644 --- a/src/objective-c/ProtoRPC/ProtoRPC.h +++ b/src/objective-c/ProtoRPC/ProtoRPC.h @@ -21,6 +21,122 @@ #import "ProtoMethod.h" +NS_ASSUME_NONNULL_BEGIN + +@class GPBMessage; + +/** An object can implement this protocol to receive responses from server from a call. */ +@protocol GRPCProtoResponseHandler<NSObject> + +@required + +/** + * All the responses must be issued to a user-provided dispatch queue. This property specifies the + * dispatch queue to be used for issuing the notifications. + */ +@property(atomic, readonly) dispatch_queue_t dispatchQueue; + +@optional + +/** + * Issued when initial metadata is received from the server. + */ +- (void)didReceiveInitialMetadata:(nullable NSDictionary *)initialMetadata; + +/** + * Issued when a message is received from the server. The message is the deserialized proto object. + */ +- (void)didReceiveProtoMessage:(nullable GPBMessage *)message; + +/** + * Issued when a call finished. If the call finished successfully, \a error is nil and \a + * trainingMetadata consists any trailing metadata received from the server. Otherwise, \a error + * is non-nil and contains the corresponding error information, including gRPC error codes and + * error descriptions. + */ +- (void)didCloseWithTrailingMetadata:(nullable NSDictionary *)trailingMetadata + error:(nullable NSError *)error; + +@end + +/** A unary-request RPC call with Protobuf. */ +@interface GRPCUnaryProtoCall : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype) new NS_UNAVAILABLE; + +/** + * Users should not use this initializer directly. Call objects will be created, initialized, and + * returned to users by methods of the generated service. + */ +- (nullable instancetype)initWithRequestOptions:(GRPCRequestOptions *)requestOptions + message:(GPBMessage *)message + responseHandler:(id<GRPCProtoResponseHandler>)handler + callOptions:(nullable GRPCCallOptions *)callOptions + responseClass:(Class)responseClass NS_DESIGNATED_INITIALIZER; + +/** + * Start the call. This function must only be called once for each instance. + */ +- (void)start; + +/** + * Cancel the request of this call at best effort. It attempts to notify the server that the RPC + * should be cancelled, and issue didCloseWithTrailingMetadata:error: callback with error code + * CANCELED if no other error code has already been issued. + */ +- (void)cancel; + +@end + +/** A client-streaming RPC call with Protobuf. */ +@interface GRPCStreamingProtoCall : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype) new NS_UNAVAILABLE; + +/** + * Users should not use this initializer directly. Call objects will be created, initialized, and + * returned to users by methods of the generated service. + */ +- (nullable instancetype)initWithRequestOptions:(GRPCRequestOptions *)requestOptions + responseHandler:(id<GRPCProtoResponseHandler>)handler + callOptions:(nullable GRPCCallOptions *)callOptions + responseClass:(Class)responseClass NS_DESIGNATED_INITIALIZER; + +/** + * Start the call. This function must only be called once for each instance. + */ +- (void)start; + +/** + * Cancel the request of this call at best effort. It attempts to notify the server that the RPC + * should be cancelled, and issue didCloseWithTrailingMetadata:error: callback with error code + * CANCELED if no other error code has already been issued. + */ +- (void)cancel; + +/** + * Send a message to the server. The message should be a Protobuf message which will be serialized + * internally. + */ +- (void)writeMessage:(GPBMessage *)message; + +/** + * Finish the RPC request and half-close the call. The server may still send messages and/or + * trailers to the client. + */ +- (void)finish; + +@end + +NS_ASSUME_NONNULL_END + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" + __attribute__((deprecated("Please use GRPCProtoCall."))) @interface ProtoRPC : GRPCCall @@ -47,3 +163,5 @@ __attribute__((deprecated("Please use GRPCProtoCall."))) @interface ProtoRPC #pragma clang diagnostic pop @end + +#pragma clang diagnostic pop diff --git a/src/objective-c/ProtoRPC/ProtoRPC.m b/src/objective-c/ProtoRPC/ProtoRPC.m index 5dca971b08..0ab96a5ba2 100644 --- a/src/objective-c/ProtoRPC/ProtoRPC.m +++ b/src/objective-c/ProtoRPC/ProtoRPC.m @@ -23,9 +23,13 @@ #else #import <GPBProtocolBuffers.h> #endif +#import <GRPCClient/GRPCCall.h> #import <RxLibrary/GRXWriteable.h> #import <RxLibrary/GRXWriter+Transformations.h> +/** + * Generate an NSError object that represents a failure in parsing a proto class. + */ static NSError *ErrorForBadProto(id proto, Class expectedClass, NSError *parsingError) { NSDictionary *info = @{ NSLocalizedDescriptionKey : @"Unable to parse response from the server", @@ -41,6 +45,227 @@ static NSError *ErrorForBadProto(id proto, Class expectedClass, NSError *parsing return [NSError errorWithDomain:@"io.grpc" code:13 userInfo:info]; } +@implementation GRPCUnaryProtoCall { + GRPCStreamingProtoCall *_call; + GPBMessage *_message; +} + +- (instancetype)initWithRequestOptions:(GRPCRequestOptions *)requestOptions + message:(GPBMessage *)message + responseHandler:(id<GRPCProtoResponseHandler>)handler + callOptions:(GRPCCallOptions *)callOptions + responseClass:(Class)responseClass { + NSAssert(message != nil, @"message cannot be empty."); + NSAssert(responseClass != nil, @"responseClass cannot be empty."); + if (message == nil || responseClass == nil) { + return nil; + } + if ((self = [super init])) { + _call = [[GRPCStreamingProtoCall alloc] initWithRequestOptions:requestOptions + responseHandler:handler + callOptions:callOptions + responseClass:responseClass]; + _message = [message copy]; + } + return self; +} + +- (void)start { + [_call start]; + [_call writeMessage:_message]; + [_call finish]; +} + +- (void)cancel { + [_call cancel]; +} + +@end + +@interface GRPCStreamingProtoCall ()<GRPCResponseHandler> + +@end + +@implementation GRPCStreamingProtoCall { + GRPCRequestOptions *_requestOptions; + id<GRPCProtoResponseHandler> _handler; + GRPCCallOptions *_callOptions; + Class _responseClass; + + GRPCCall2 *_call; + dispatch_queue_t _dispatchQueue; +} + +- (instancetype)initWithRequestOptions:(GRPCRequestOptions *)requestOptions + responseHandler:(id<GRPCProtoResponseHandler>)handler + callOptions:(GRPCCallOptions *)callOptions + responseClass:(Class)responseClass { + NSAssert(requestOptions.host.length != 0 && requestOptions.path.length != 0 && + requestOptions.safety <= GRPCCallSafetyCacheableRequest, + @"Invalid callOptions."); + NSAssert(handler != nil, @"handler cannot be empty."); + if (requestOptions.host.length == 0 || requestOptions.path.length == 0 || + requestOptions.safety > GRPCCallSafetyCacheableRequest) { + return nil; + } + if (handler == nil) { + return nil; + } + + if ((self = [super init])) { + _requestOptions = [requestOptions copy]; + _handler = handler; + _callOptions = [callOptions copy]; + _responseClass = responseClass; + + // Set queue QoS only when iOS version is 8.0 or above and Xcode version is 9.0 or above +#if __IPHONE_OS_VERSION_MAX_ALLOWED < 110000 || __MAC_OS_X_VERSION_MAX_ALLOWED < 101300 + if (@available(iOS 8.0, macOS 10.10, *)) { + _dispatchQueue = dispatch_queue_create( + NULL, + dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0)); + } else { +#else + { +#endif + _dispatchQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + } + dispatch_set_target_queue(_dispatchQueue, handler.dispatchQueue); + + _call = [[GRPCCall2 alloc] initWithRequestOptions:_requestOptions + responseHandler:self + callOptions:_callOptions]; + } + return self; +} + +- (void)start { + GRPCCall2 *copiedCall; + @synchronized(self) { + copiedCall = _call; + } + [copiedCall start]; +} + +- (void)cancel { + GRPCCall2 *copiedCall; + @synchronized(self) { + copiedCall = _call; + _call = nil; + if ([_handler respondsToSelector:@selector(didCloseWithTrailingMetadata:error:)]) { + dispatch_async(_dispatchQueue, ^{ + id<GRPCProtoResponseHandler> copiedHandler = nil; + @synchronized(self) { + copiedHandler = self->_handler; + self->_handler = nil; + } + [copiedHandler didCloseWithTrailingMetadata:nil + error:[NSError errorWithDomain:kGRPCErrorDomain + code:GRPCErrorCodeCancelled + userInfo:@{ + NSLocalizedDescriptionKey : + @"Canceled by app" + }]]; + }); + } else { + _handler = nil; + } + } + [copiedCall cancel]; +} + +- (void)writeMessage:(GPBMessage *)message { + NSAssert([message isKindOfClass:[GPBMessage class]], @"Parameter message must be a GPBMessage"); + if (![message isKindOfClass:[GPBMessage class]]) { + NSLog(@"Failed to send a message that is non-proto."); + return; + } + + GRPCCall2 *copiedCall; + @synchronized(self) { + copiedCall = _call; + } + [copiedCall writeData:[message data]]; +} + +- (void)finish { + GRPCCall2 *copiedCall; + @synchronized(self) { + copiedCall = _call; + _call = nil; + } + [copiedCall finish]; +} + +- (void)didReceiveInitialMetadata:(NSDictionary *)initialMetadata { + @synchronized(self) { + if (initialMetadata != nil && + [_handler respondsToSelector:@selector(didReceiveInitialMetadata:)]) { + dispatch_async(_dispatchQueue, ^{ + id<GRPCProtoResponseHandler> copiedHandler = nil; + @synchronized(self) { + copiedHandler = self->_handler; + } + [copiedHandler didReceiveInitialMetadata:initialMetadata]; + }); + } + } +} + +- (void)didReceiveRawMessage:(NSData *)message { + if (message == nil) return; + + NSError *error = nil; + GPBMessage *parsed = [_responseClass parseFromData:message error:&error]; + @synchronized(self) { + if (parsed && [_handler respondsToSelector:@selector(didReceiveProtoMessage:)]) { + dispatch_async(_dispatchQueue, ^{ + id<GRPCProtoResponseHandler> copiedHandler = nil; + @synchronized(self) { + copiedHandler = self->_handler; + } + [copiedHandler didReceiveProtoMessage:parsed]; + }); + } else if (!parsed && + [_handler respondsToSelector:@selector(didCloseWithTrailingMetadata:error:)]) { + dispatch_async(_dispatchQueue, ^{ + id<GRPCProtoResponseHandler> copiedHandler = nil; + @synchronized(self) { + copiedHandler = self->_handler; + self->_handler = nil; + } + [copiedHandler + didCloseWithTrailingMetadata:nil + error:ErrorForBadProto(message, self->_responseClass, error)]; + }); + [_call cancel]; + _call = nil; + } + } +} + +- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error { + @synchronized(self) { + if ([_handler respondsToSelector:@selector(didCloseWithTrailingMetadata:error:)]) { + dispatch_async(_dispatchQueue, ^{ + id<GRPCProtoResponseHandler> copiedHandler = nil; + @synchronized(self) { + copiedHandler = self->_handler; + self->_handler = nil; + } + [copiedHandler didCloseWithTrailingMetadata:trailingMetadata error:error]; + }); + } + _call = nil; + } +} + +- (dispatch_queue_t)dispatchQueue { + return _dispatchQueue; +} + +@end + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" @implementation ProtoRPC { @@ -72,7 +297,7 @@ static NSError *ErrorForBadProto(id proto, Class expectedClass, NSError *parsing } // A writer that serializes the proto messages to send. GRXWriter *bytesWriter = [requestsWriter map:^id(GPBMessage *proto) { - if (![proto isKindOfClass:GPBMessage.class]) { + if (![proto isKindOfClass:[GPBMessage class]]) { [NSException raise:NSInvalidArgumentException format:@"Request must be a proto message: %@", proto]; } diff --git a/src/objective-c/ProtoRPC/ProtoService.h b/src/objective-c/ProtoRPC/ProtoService.h index 29c4e9be36..900ec8d0e1 100644 --- a/src/objective-c/ProtoRPC/ProtoService.h +++ b/src/objective-c/ProtoRPC/ProtoService.h @@ -21,18 +21,47 @@ @class GRPCProtoCall; @protocol GRXWriteable; @class GRXWriter; +@class GRPCCallOptions; +@class GRPCProtoCall; +@class GRPCUnaryProtoCall; +@class GRPCStreamingProtoCall; +@protocol GRPCProtoResponseHandler; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" __attribute__((deprecated("Please use GRPCProtoService."))) @interface ProtoService - : NSObject - - (instancetype)initWithHost : (NSString *)host packageName - : (NSString *)packageName serviceName : (NSString *)serviceName NS_DESIGNATED_INITIALIZER; + : NSObject + + - + (nullable instancetype)initWithHost : (nonnull NSString *)host packageName + : (nonnull NSString *)packageName serviceName : (nonnull NSString *)serviceName callOptions + : (nullable GRPCCallOptions *)callOptions NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithHost:(NSString *)host + packageName:(NSString *)packageName + serviceName:(NSString *)serviceName; - (GRPCProtoCall *)RPCToMethod:(NSString *)method requestsWriter:(GRXWriter *)requestsWriter responseClass:(Class)responseClass responsesWriteable:(id<GRXWriteable>)responsesWriteable; + +- (nullable GRPCUnaryProtoCall *)RPCToMethod:(nonnull NSString *)method + message:(nonnull id)message + responseHandler:(nonnull id<GRPCProtoResponseHandler>)handler + callOptions:(nullable GRPCCallOptions *)callOptions + responseClass:(nonnull Class)responseClass; + +- (nullable GRPCStreamingProtoCall *)RPCToMethod:(nonnull NSString *)method + responseHandler:(nonnull id<GRPCProtoResponseHandler>)handler + callOptions:(nullable GRPCCallOptions *)callOptions + responseClass:(nonnull Class)responseClass; + @end +#pragma clang diagnostic pop + /** * This subclass is empty now. Eventually we'll remove ProtoService class * to avoid potential naming conflict diff --git a/src/objective-c/ProtoRPC/ProtoService.m b/src/objective-c/ProtoRPC/ProtoService.m index 3e9cc5c0b2..80a1f2f226 100644 --- a/src/objective-c/ProtoRPC/ProtoService.m +++ b/src/objective-c/ProtoRPC/ProtoService.m @@ -18,6 +18,7 @@ #import "ProtoService.h" +#import <GRPCClient/GRPCCall.h> #import <RxLibrary/GRXWriteable.h> #import <RxLibrary/GRXWriter.h> @@ -31,6 +32,7 @@ NSString *_host; NSString *_packageName; NSString *_serviceName; + GRPCCallOptions *_callOptions; } - (instancetype)init { @@ -40,19 +42,41 @@ // Designated initializer - (instancetype)initWithHost:(NSString *)host packageName:(NSString *)packageName - serviceName:(NSString *)serviceName { - if (!host || !serviceName) { - [NSException raise:NSInvalidArgumentException - format:@"Neither host nor serviceName can be nil."]; + serviceName:(NSString *)serviceName + callOptions:(GRPCCallOptions *)callOptions { + NSAssert(host.length != 0 && packageName.length != 0 && serviceName.length != 0, + @"Invalid parameter."); + if (host.length == 0 || packageName.length == 0 || serviceName.length == 0) { + return nil; } if ((self = [super init])) { _host = [host copy]; _packageName = [packageName copy]; _serviceName = [serviceName copy]; + _callOptions = [callOptions copy]; } return self; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-designated-initializers" +// Do not call designated initializer here due to nullability incompatibility. This method is from +// old API and does not assert on nullability of the parameters. + +- (instancetype)initWithHost:(NSString *)host + packageName:(NSString *)packageName + serviceName:(NSString *)serviceName { + if ((self = [super init])) { + _host = [host copy]; + _packageName = [packageName copy]; + _serviceName = [serviceName copy]; + _callOptions = nil; + } + return self; +} + +#pragma clang diagnostic pop + - (GRPCProtoCall *)RPCToMethod:(NSString *)method requestsWriter:(GRXWriter *)requestsWriter responseClass:(Class)responseClass @@ -65,6 +89,41 @@ responseClass:responseClass responsesWriteable:responsesWriteable]; } + +- (GRPCUnaryProtoCall *)RPCToMethod:(NSString *)method + message:(id)message + responseHandler:(id<GRPCProtoResponseHandler>)handler + callOptions:(GRPCCallOptions *)callOptions + responseClass:(Class)responseClass { + GRPCProtoMethod *methodName = + [[GRPCProtoMethod alloc] initWithPackage:_packageName service:_serviceName method:method]; + GRPCRequestOptions *requestOptions = + [[GRPCRequestOptions alloc] initWithHost:_host + path:methodName.HTTPPath + safety:GRPCCallSafetyDefault]; + return [[GRPCUnaryProtoCall alloc] initWithRequestOptions:requestOptions + message:message + responseHandler:handler + callOptions:callOptions ?: _callOptions + responseClass:responseClass]; +} + +- (GRPCStreamingProtoCall *)RPCToMethod:(NSString *)method + responseHandler:(id<GRPCProtoResponseHandler>)handler + callOptions:(GRPCCallOptions *)callOptions + responseClass:(Class)responseClass { + GRPCProtoMethod *methodName = + [[GRPCProtoMethod alloc] initWithPackage:_packageName service:_serviceName method:method]; + GRPCRequestOptions *requestOptions = + [[GRPCRequestOptions alloc] initWithHost:_host + path:methodName.HTTPPath + safety:GRPCCallSafetyDefault]; + return [[GRPCStreamingProtoCall alloc] initWithRequestOptions:requestOptions + responseHandler:handler + callOptions:callOptions ?: _callOptions + responseClass:responseClass]; +} + @end @implementation GRPCProtoService |