Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a 'simple' service protocol #2134

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,14 @@ struct TextBasedRenderer: RendererProtocol {
writer.nextLineAppendsToLastLine()
writer.writeLine("<")
writer.nextLineAppendsToLastLine()
renderExistingTypeDescription(wrapped)
for (wrap, isLast) in wrapped.enumeratedWithLastMarker() {
renderExistingTypeDescription(wrap)
writer.nextLineAppendsToLastLine()
if !isLast {
writer.writeLine(", ")
writer.nextLineAppendsToLastLine()
}
}
writer.nextLineAppendsToLastLine()
writer.writeLine(">")
case .optional(let existingTypeDescription):
Expand Down
286 changes: 286 additions & 0 deletions Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -473,3 +473,289 @@ extension ExtensionDescription {
)
}
}

extension FunctionSignatureDescription {
/// ```
/// func <Name>(
/// request: <Input>,
/// context: GRPCCore.ServerContext,
/// ) async throws -> <Output>
/// ```
///
/// ```
/// func <Name>(
/// request: GRPCCore.RPCAsyncSequence<Input, any Error>,
/// response: GRPCCore.RPCAsyncWriter<Output>
/// context: GRPCCore.ServerContext,
/// ) async throws
/// ```
static func simpleServerMethod(
accessLevel: AccessModifier? = nil,
name: String,
input: String,
output: String,
streamingInput: Bool,
streamingOutput: Bool
) -> Self {
var parameters: [ParameterDescription] = [
ParameterDescription(
label: "request",
type: streamingInput ? .rpcAsyncSequence(forType: input) : .member(input)
)
]

if streamingOutput {
parameters.append(ParameterDescription(label: "response", type: .rpcWriter(forType: output)))
}

parameters.append(ParameterDescription(label: "context", type: .serverContext))

return FunctionSignatureDescription(
accessModifier: accessLevel,
kind: .function(name: name),
parameters: parameters,
keywords: [.async, .throws],
returnType: streamingOutput ? nil : .identifier(.pattern(output))
)
}
}

extension ProtocolDescription {
/// ```
/// protocol SimpleServiceProtocol: <ServiceProtocol> {
/// ...
/// }
/// ```
static func simpleServiceProtocol(
accessModifier: AccessModifier? = nil,
name: String,
serviceProtocol: String,
methods: [MethodDescriptor]
) -> Self {
func docs(for method: MethodDescriptor) -> String {
let summary = """
/// Handle the "\(method.name.normalizedBase)" method.
"""

let requestText =
method.isInputStreaming
? "A stream of `\(method.inputType)` messages."
: "A `\(method.inputType)` message."

var parameters = """
/// - Parameters:
/// - request: \(requestText)
"""

if method.isOutputStreaming {
parameters += "\n"
parameters += """
/// - response: A response stream of `\(method.outputType)` messages.
"""
}

parameters += "\n"
parameters += """
/// - context: Context providing information about the RPC.
/// - Throws: Any error which occurred during the processing of the request. Thrown errors
/// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
/// to an internal error.
"""

if !method.isOutputStreaming {
parameters += "\n"
parameters += """
/// - Returns: A `\(method.outputType)` to respond with.
"""
}

return Docs.interposeDocs(method.documentation, between: summary, and: parameters)
}

return ProtocolDescription(
accessModifier: accessModifier,
name: name,
conformances: [serviceProtocol],
members: methods.map { method in
.commentable(
.preFormatted(docs(for: method)),
.function(
signature: .simpleServerMethod(
name: method.name.generatedLowerCase,
input: method.inputType,
output: method.outputType,
streamingInput: method.isInputStreaming,
streamingOutput: method.isOutputStreaming
)
)
)
}
)
}
}

extension FunctionCallDescription {
/// ```
/// try await self.<Name>(
/// request: request.message,
/// response: writer,
/// context: context
/// )
/// ```
static func serviceMethodCallingSimpleMethod(
name: String,
input: String,
output: String,
streamingInput: Bool,
streamingOutput: Bool
) -> Self {
var arguments: [FunctionArgumentDescription] = [
FunctionArgumentDescription(
label: "request",
expression: .identifierPattern("request").dot(streamingInput ? "messages" : "message")
)
]

if streamingOutput {
arguments.append(
FunctionArgumentDescription(
label: "response",
expression: .identifierPattern("writer")
)
)
}

arguments.append(
FunctionArgumentDescription(
label: "context",
expression: .identifierPattern("context")
)
)

return FunctionCallDescription(
calledExpression: .try(.await(.identifierPattern("self").dot(name))),
arguments: arguments
)
}
}

extension FunctionDescription {
/// ```
/// func <Name>(
/// request: GRPCCore.ServerRequest<Input>,
/// context: GRPCCore.ServerContext
/// ) async throws -> GRPCCore.ServerResponse<Output> {
/// return GRPCCore.ServerResponse<Output>(
/// message: try await self.<Name>(
/// request: request.message,
/// context: context
/// )
/// metadata: [:]
/// )
/// }
/// ```
static func serviceProtocolDefaultImplementation(
accessModifier: AccessModifier? = nil,
name: String,
input: String,
output: String,
streamingInput: Bool,
streamingOutput: Bool
) -> Self {
func makeUnaryOutputArguments() -> [FunctionArgumentDescription] {
return [
FunctionArgumentDescription(
label: "message",
expression: .functionCall(
.serviceMethodCallingSimpleMethod(
name: name,
input: input,
output: output,
streamingInput: streamingInput,
streamingOutput: streamingOutput
)
)
),
FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))),
]
}

func makeStreamingOutputArguments() -> [FunctionArgumentDescription] {
return [
FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))),
FunctionArgumentDescription(
label: "producer",
expression: .closureInvocation(
argumentNames: ["writer"],
body: [
.expression(
.functionCall(
.serviceMethodCallingSimpleMethod(
name: name,
input: input,
output: output,
streamingInput: streamingInput,
streamingOutput: streamingOutput
)
)
),
.expression(.return(.literal(.dictionary([])))),
]
)
),
]
}

return FunctionDescription(
signature: .serverMethod(
accessLevel: accessModifier,
name: name,
input: input,
output: output,
streamingInput: streamingInput,
streamingOutput: streamingOutput
),
body: [
.expression(
.functionCall(
calledExpression: .return(
.identifierType(
.serverResponse(forType: output, streaming: streamingOutput)
)
),
arguments: streamingOutput ? makeStreamingOutputArguments() : makeUnaryOutputArguments()
)
)
]
)
}
}

extension ExtensionDescription {
/// ```
/// extension ServiceProtocol {
/// ...
/// }
/// ```
static func serviceProtocolDefaultImplementation(
accessModifier: AccessModifier? = nil,
on extensionName: String,
methods: [MethodDescriptor]
) -> Self {
ExtensionDescription(
onType: extensionName,
declarations: methods.map { method in
.function(
.serviceProtocolDefaultImplementation(
accessModifier: accessModifier,
name: method.name.generatedLowerCase,
input: method.inputType,
output: method.outputType,
streamingInput: method.isInputStreaming,
streamingOutput: method.isOutputStreaming
)
)
}
)
}
}
8 changes: 8 additions & 0 deletions Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ extension ExistingTypeDescription {
.generic(wrapper: .grpcCore("RPCWriter"), wrapped: .member(type))
}

package static func rpcAsyncSequence(forType type: String) -> Self {
.generic(
wrapper: .grpcCore("RPCAsyncSequence"),
wrapped: .member(type),
.any(.member(["Swift", "Error"]))
)
}

package static let callOptions: Self = .grpcCore("CallOptions")
package static let metadata: Self = .grpcCore("Metadata")
package static let grpcClient: Self = .grpcCore("GRPCClient")
Expand Down
14 changes: 12 additions & 2 deletions Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,10 +453,10 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable {
/// For example, `Foo?`.
case optional(ExistingTypeDescription)

/// A wrapper type generic over a wrapped type.
/// A wrapper type generic over a list of wrapped types.
///
/// For example, `Wrapper<Wrapped>`.
case generic(wrapper: ExistingTypeDescription, wrapped: ExistingTypeDescription)
case generic(wrapper: ExistingTypeDescription, wrapped: [ExistingTypeDescription])

/// A type reference represented by the components.
///
Expand All @@ -483,6 +483,16 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable {
///
/// For example: `(String) async throws -> Int`.
case closure(ClosureSignatureDescription)

/// A wrapper type generic over a list of wrapped types.
///
/// For example, `Wrapper<Wrapped>`.
static func generic(
wrapper: ExistingTypeDescription,
wrapped: ExistingTypeDescription...
) -> Self {
return .generic(wrapper: wrapper, wrapped: Array(wrapped))
}
}

/// A description of a typealias declaration.
Expand Down
2 changes: 1 addition & 1 deletion Sources/GRPCCodeGen/Internal/Translator/Docs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ package enum Docs {
"""

let body = docs.split(separator: "\n").map { line in
"/// > " + line.dropFirst(4)
"/// > " + line.dropFirst(4).trimmingCharacters(in: .whitespaces)
}.joined(separator: "\n")

return header + "\n" + body
Expand Down
Loading
Loading