Skip to content

Commit

Permalink
Add a 'UserInfo' heterotyped dictionary
Browse files Browse the repository at this point in the history
Motivation:

Interceptors may be used to provide additional information to the
service provider. This could include information about an authenticated
user, for example. However, we don't currently have such a mechanism.

Modifications:

- Add a type-safe 'UserInfo' dictionary
- Add a 'UserInfo' requirement to the 'ServerCallContext' protocol
- Store a 'Ref<UserInfo>' in the base call handler and pipeline,
  exposing the 'UserInfo' in both the server call context and server
  interceptor context.

Result:

Interceptors can share information in a type-safe way with the service
provider.
  • Loading branch information
glbrntt committed Nov 4, 2020
1 parent e63d0c9 commit a41246d
Show file tree
Hide file tree
Showing 16 changed files with 366 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public class BidirectionalStreamingCallHandler<
eventLoop: self.eventLoop,
headers: headers,
logger: self.logger,
userInfoRef: self.userInfoRef,
sendResponse: self.sendResponse(_:metadata:promise:)
)
let observer = factory(context)
Expand Down
3 changes: 2 additions & 1 deletion Sources/GRPC/CallHandlers/ClientStreamingCallHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ public final class ClientStreamingCallHandler<
let context = UnaryResponseCallContext<ResponsePayload>(
eventLoop: self.eventLoop,
headers: headers,
logger: self.logger
logger: self.logger,
userInfoRef: self.userInfoRef
)

let observer = factory(context)
Expand Down
1 change: 1 addition & 0 deletions Sources/GRPC/CallHandlers/ServerStreamingCallHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public final class ServerStreamingCallHandler<
eventLoop: self.eventLoop,
headers: headers,
logger: self.logger,
userInfoRef: self.userInfoRef,
sendResponse: self.sendResponse(_:metadata:promise:)
)
let observer = factory(context)
Expand Down
3 changes: 2 additions & 1 deletion Sources/GRPC/CallHandlers/UnaryCallHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ public final class UnaryCallHandler<
let context = UnaryResponseCallContext<ResponsePayload>(
eventLoop: self.eventLoop,
headers: headers,
logger: self.logger
logger: self.logger,
userInfoRef: self.userInfoRef
)
let observer = factory(context)

Expand Down
7 changes: 7 additions & 0 deletions Sources/GRPC/CallHandlers/_BaseCallHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,27 @@ public class _BaseCallHandler<Request, Response>: GRPCCallHandler, ChannelInboun
return self.callHandlerContext.logger
}

/// A reference to `UserInfo`.
internal var userInfoRef: Ref<UserInfo>

internal init(
callHandlerContext: CallHandlerContext,
codec: ChannelHandler,
callType: GRPCCallType,
interceptors: [ServerInterceptor<Request, Response>]
) {
let userInfoRef = Ref(UserInfo())

self.callHandlerContext = callHandlerContext
self._codec = codec
self.callType = callType
self.userInfoRef = userInfoRef
self.pipeline = ServerInterceptorPipeline(
logger: callHandlerContext.logger,
eventLoop: callHandlerContext.eventLoop,
path: callHandlerContext.path,
callType: callType,
userInfoRef: userInfoRef,
interceptors: interceptors,
onRequestPart: self.receiveRequestPartFromInterceptors(_:),
onResponsePart: self.sendResponsePartFromInterceptors(_:promise:)
Expand Down
15 changes: 15 additions & 0 deletions Sources/GRPC/Interceptor/ServerInterceptorContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ public struct ServerInterceptorContext<Request, Response> {
return self.pipeline.path
}

/// A 'UserInfo' dictionary.
///
/// - Important: While `UserInfo` has value-semantics, this property retrieves from, and sets a
/// reference wrapped `UserInfo`. The contexts passed to the service provider share the same
/// reference. As such this may be used as a mechanism to pass information between interceptors
/// and service providers.
public var userInfo: UserInfo {
get {
return self.pipeline.userInfoRef.value
}
nonmutating set {
self.pipeline.userInfoRef.value = newValue
}
}

/// Construct a `ServerInterceptorContext` for the interceptor at the given index within the
/// interceptor pipeline.
internal init(
Expand Down
5 changes: 5 additions & 0 deletions Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ internal final class ServerInterceptorPipeline<Request, Response> {
/// A logger.
internal let logger: Logger

/// A reference to a 'UserInfo'.
internal let userInfoRef: Ref<UserInfo>

/// The contexts associated with the interceptors stored in this pipeline. Contexts will be
/// removed once the RPC has completed. Contexts are ordered from inbound to outbound, that is,
/// the head is first and the tail is last.
Expand Down Expand Up @@ -80,6 +83,7 @@ internal final class ServerInterceptorPipeline<Request, Response> {
eventLoop: EventLoop,
path: String,
callType: GRPCCallType,
userInfoRef: Ref<UserInfo>,
interceptors: [ServerInterceptor<Request, Response>],
onRequestPart: @escaping (ServerRequestPart<Request>) -> Void,
onResponsePart: @escaping (ServerResponsePart<Response>, EventLoopPromise<Void>?) -> Void
Expand All @@ -88,6 +92,7 @@ internal final class ServerInterceptorPipeline<Request, Response> {
self.eventLoop = eventLoop
self.path = path
self.type = callType
self.userInfoRef = userInfoRef

// We need space for the head and tail as well as any user provided interceptors.
var contexts: [ServerInterceptorContext<Request, Response>] = []
Expand Down
22 changes: 22 additions & 0 deletions Sources/GRPC/Ref.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2020, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

internal final class Ref<Value> {
internal var value: Value
internal init(_ value: Value) {
self.value = value
}
}
39 changes: 37 additions & 2 deletions Sources/GRPC/ServerCallContexts/ServerCallContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public protocol ServerCallContext: AnyObject {
/// Request headers for this request.
var headers: HPACKHeaders { get }

/// A 'UserInfo' dictionary.
var userInfo: UserInfo { get set }

/// The logger used for this call.
var logger: Logger { get }

Expand All @@ -44,21 +47,53 @@ open class ServerCallContextBase: ServerCallContext {
public let logger: Logger
public var compressionEnabled: Bool = true

/// - Important: While `UserInfo` has value-semantics, this property retrieves from, and sets a
/// reference wrapped `UserInfo`. The contexts passed to interceptors provide the same
/// reference. As such this may be used as a mechanism to pass information between interceptors
/// and service providers.
public var userInfo: UserInfo {
get {
return self.userInfoRef.value
}
set {
self.userInfoRef.value = newValue
}
}

/// A reference to an underlying `UserInfo`. We share this with the interceptors.
private let userInfoRef: Ref<UserInfo>

/// Metadata to return at the end of the RPC. If this is required it should be updated before
/// the `responsePromise` or `statusPromise` is fulfilled.
public var trailers = HPACKHeaders()

public init(eventLoop: EventLoop, headers: HPACKHeaders, logger: Logger) {
public convenience init(
eventLoop: EventLoop,
headers: HPACKHeaders,
logger: Logger,
userInfo: UserInfo = UserInfo()
) {
self.init(eventLoop: eventLoop, headers: headers, logger: logger, userInfoRef: .init(userInfo))
}

internal init(
eventLoop: EventLoop,
headers: HPACKHeaders,
logger: Logger,
userInfoRef: Ref<UserInfo>
) {
self.eventLoop = eventLoop
self.headers = headers
self.userInfoRef = userInfoRef
self.logger = logger
}

@available(*, deprecated, renamed: "init(eventLoop:headers:logger:)")
@available(*, deprecated, renamed: "init(eventLoop:headers:logger:userInfo:)")
public init(eventLoop: EventLoop, request: HTTPRequestHead, logger: Logger) {
self.eventLoop = eventLoop
self.headers = HPACKHeaders(httpHeaders: request.headers, normalizeHTTPHeaders: false)
self.logger = logger
self.userInfoRef = .init(UserInfo())
}

/// Processes an error, transforming it into a 'GRPCStatus' and any trailers to send to the peer.
Expand Down
30 changes: 25 additions & 5 deletions Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,26 @@ open class StreamingResponseCallContext<ResponsePayload>: ServerCallContextBase

public let statusPromise: EventLoopPromise<GRPCStatus>

override public init(eventLoop: EventLoop, headers: HPACKHeaders, logger: Logger) {
public convenience init(
eventLoop: EventLoop,
headers: HPACKHeaders,
logger: Logger,
userInfo: UserInfo = UserInfo()
) {
self.init(eventLoop: eventLoop, headers: headers, logger: logger, userInfoRef: .init(userInfo))
}

override internal init(
eventLoop: EventLoop,
headers: HPACKHeaders,
logger: Logger,
userInfoRef: Ref<UserInfo>
) {
self.statusPromise = eventLoop.makePromise()
super.init(eventLoop: eventLoop, headers: headers, logger: logger)
super.init(eventLoop: eventLoop, headers: headers, logger: logger, userInfoRef: userInfoRef)
}

@available(*, deprecated, renamed: "init(eventLoop:path:headers:logger:)")
@available(*, deprecated, renamed: "init(eventLoop:path:headers:logger:userInfo:)")
override public init(eventLoop: EventLoop, request: HTTPRequestHead, logger: Logger) {
self.statusPromise = eventLoop.makePromise()
super.init(eventLoop: eventLoop, request: request, logger: logger)
Expand Down Expand Up @@ -113,10 +127,11 @@ internal final class _StreamingResponseCallContext<Request, Response>:
eventLoop: EventLoop,
headers: HPACKHeaders,
logger: Logger,
userInfoRef: Ref<UserInfo>,
sendResponse: @escaping (Response, MessageMetadata, EventLoopPromise<Void>?) -> Void
) {
self._sendResponse = sendResponse
super.init(eventLoop: eventLoop, headers: headers, logger: logger)
super.init(eventLoop: eventLoop, headers: headers, logger: logger, userInfoRef: userInfoRef)
}

override func sendResponse(
Expand Down Expand Up @@ -165,7 +180,12 @@ open class StreamingResponseCallContextImpl<ResponsePayload>: StreamingResponseC
logger: Logger
) {
self.channel = channel
super.init(eventLoop: channel.eventLoop, headers: headers, logger: logger)
super.init(
eventLoop: channel.eventLoop,
headers: headers,
logger: logger,
userInfoRef: Ref(UserInfo())
)

self.statusPromise.futureResult.whenComplete { result in
switch result {
Expand Down
27 changes: 23 additions & 4 deletions Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,26 @@ open class UnaryResponseCallContext<ResponsePayload>: ServerCallContextBase, Sta
public let responsePromise: EventLoopPromise<ResponsePayload>
public var responseStatus: GRPCStatus = .ok

override public init(eventLoop: EventLoop, headers: HPACKHeaders, logger: Logger) {
public convenience init(
eventLoop: EventLoop,
headers: HPACKHeaders,
logger: Logger,
userInfo: UserInfo = UserInfo()
) {
self.init(eventLoop: eventLoop, headers: headers, logger: logger, userInfoRef: .init(userInfo))
}

override internal init(
eventLoop: EventLoop,
headers: HPACKHeaders,
logger: Logger,
userInfoRef: Ref<UserInfo>
) {
self.responsePromise = eventLoop.makePromise()
super.init(eventLoop: eventLoop, headers: headers, logger: logger)
super.init(eventLoop: eventLoop, headers: headers, logger: logger, userInfoRef: userInfoRef)
}

@available(*, deprecated, renamed: "init(eventLoop:headers:logger:)")
@available(*, deprecated, renamed: "init(eventLoop:headers:logger:userInfo:)")
override public init(eventLoop: EventLoop, request: HTTPRequestHead, logger: Logger) {
self.responsePromise = eventLoop.makePromise()
super.init(eventLoop: eventLoop, request: request, logger: logger)
Expand Down Expand Up @@ -90,7 +104,12 @@ open class UnaryResponseCallContextImpl<ResponsePayload>: UnaryResponseCallConte
logger: Logger
) {
self.channel = channel
super.init(eventLoop: channel.eventLoop, headers: headers, logger: logger)
super.init(
eventLoop: channel.eventLoop,
headers: headers,
logger: logger,
userInfoRef: .init(UserInfo())
)

self.responsePromise.futureResult.whenComplete { [self, weak errorDelegate] result in
switch result {
Expand Down
Loading

0 comments on commit a41246d

Please sign in to comment.