Skip to content

Commit eb6b1da

Browse files
committed
Add a 'simple' service protocol
Motivation: The generated service protocol requires users to deal with request/response objects. Many users won't care about metadata so can benefit from an easier-to-use API. Modifications: - Add a "simple" server protocol which doesn't make user of request/response objects Result: Easier to implement as service
1 parent 311486e commit eb6b1da

9 files changed

+509
-4
lines changed

Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,14 @@ struct TextBasedRenderer: RendererProtocol {
613613
writer.nextLineAppendsToLastLine()
614614
writer.writeLine("<")
615615
writer.nextLineAppendsToLastLine()
616-
renderExistingTypeDescription(wrapped)
616+
for (wrap, isLast) in wrapped.enumeratedWithLastMarker() {
617+
renderExistingTypeDescription(wrap)
618+
writer.nextLineAppendsToLastLine()
619+
if !isLast {
620+
writer.writeLine(", ")
621+
writer.nextLineAppendsToLastLine()
622+
}
623+
}
617624
writer.nextLineAppendsToLastLine()
618625
writer.writeLine(">")
619626
case .optional(let existingTypeDescription):

Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,289 @@ extension ExtensionDescription {
473473
)
474474
}
475475
}
476+
477+
extension FunctionSignatureDescription {
478+
/// ```
479+
/// func <Name>(
480+
/// request: <Input>,
481+
/// context: GRPCCore.ServerContext,
482+
/// ) async throws -> <Output>
483+
/// ```
484+
///
485+
/// ```
486+
/// func <Name>(
487+
/// request: GRPCCore.RPCAsyncSequence<Input, any Error>,
488+
/// response: GRPCCore.RPCAsyncWriter<Output>
489+
/// context: GRPCCore.ServerContext,
490+
/// ) async throws
491+
/// ```
492+
static func simpleServerMethod(
493+
accessLevel: AccessModifier? = nil,
494+
name: String,
495+
input: String,
496+
output: String,
497+
streamingInput: Bool,
498+
streamingOutput: Bool
499+
) -> Self {
500+
var parameters: [ParameterDescription] = [
501+
ParameterDescription(
502+
label: "request",
503+
type: streamingInput ? .rpcAsyncSequence(forType: input) : .member(input)
504+
)
505+
]
506+
507+
if streamingOutput {
508+
parameters.append(ParameterDescription(label: "response", type: .rpcWriter(forType: output)))
509+
}
510+
511+
parameters.append(ParameterDescription(label: "context", type: .serverContext))
512+
513+
return FunctionSignatureDescription(
514+
accessModifier: accessLevel,
515+
kind: .function(name: name),
516+
parameters: parameters,
517+
keywords: [.async, .throws],
518+
returnType: streamingOutput ? nil : .identifier(.pattern(output))
519+
)
520+
}
521+
}
522+
523+
extension ProtocolDescription {
524+
/// ```
525+
/// protocol SimpleServiceProtocol: <ServiceProtocol> {
526+
/// ...
527+
/// }
528+
/// ```
529+
static func simpleServiceProtocol(
530+
accessModifier: AccessModifier? = nil,
531+
name: String,
532+
serviceProtocol: String,
533+
methods: [MethodDescriptor]
534+
) -> Self {
535+
func docs(for method: MethodDescriptor) -> String {
536+
let summary = """
537+
/// Handle the "\(method.name.normalizedBase)" method.
538+
"""
539+
540+
let requestText =
541+
method.isInputStreaming
542+
? "A stream of `\(method.inputType)` messages."
543+
: "A `\(method.inputType)` message."
544+
545+
var parameters = """
546+
/// - Parameters:
547+
/// - request: \(requestText)
548+
"""
549+
550+
if method.isOutputStreaming {
551+
parameters += "\n"
552+
parameters += """
553+
/// - response: A response stream of `\(method.outputType)` messages.
554+
"""
555+
}
556+
557+
parameters += "\n"
558+
parameters += """
559+
/// - context: Context providing information about the RPC.
560+
/// - Throws: Any error which occurred during the processing of the request. Thrown errors
561+
/// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
562+
/// to an internal error.
563+
"""
564+
565+
if !method.isOutputStreaming {
566+
parameters += "\n"
567+
parameters += """
568+
/// - Returns: A `\(method.outputType)` to respond with.
569+
"""
570+
}
571+
572+
return Docs.interposeDocs(method.documentation, between: summary, and: parameters)
573+
}
574+
575+
return ProtocolDescription(
576+
accessModifier: accessModifier,
577+
name: name,
578+
conformances: [serviceProtocol],
579+
members: methods.map { method in
580+
.commentable(
581+
.preFormatted(docs(for: method)),
582+
.function(
583+
signature: .simpleServerMethod(
584+
name: method.name.generatedLowerCase,
585+
input: method.inputType,
586+
output: method.outputType,
587+
streamingInput: method.isInputStreaming,
588+
streamingOutput: method.isOutputStreaming
589+
)
590+
)
591+
)
592+
}
593+
)
594+
}
595+
}
596+
597+
extension FunctionCallDescription {
598+
/// ```
599+
/// try await self.<Name>(
600+
/// request: request.message,
601+
/// response: writer,
602+
/// context: context
603+
/// )
604+
/// ```
605+
static func serviceMethodCallingSimpleMethod(
606+
name: String,
607+
input: String,
608+
output: String,
609+
streamingInput: Bool,
610+
streamingOutput: Bool
611+
) -> Self {
612+
var arguments: [FunctionArgumentDescription] = [
613+
FunctionArgumentDescription(
614+
label: "request",
615+
expression: .identifierPattern("request").dot(streamingInput ? "messages" : "message")
616+
)
617+
]
618+
619+
if streamingOutput {
620+
arguments.append(
621+
FunctionArgumentDescription(
622+
label: "response",
623+
expression: .identifierPattern("writer")
624+
)
625+
)
626+
}
627+
628+
arguments.append(
629+
FunctionArgumentDescription(
630+
label: "context",
631+
expression: .identifierPattern("context")
632+
)
633+
)
634+
635+
return FunctionCallDescription(
636+
calledExpression: .try(.await(.identifierPattern("self").dot(name))),
637+
arguments: arguments
638+
)
639+
}
640+
}
641+
642+
extension FunctionDescription {
643+
/// ```
644+
/// func <Name>(
645+
/// request: GRPCCore.ServerRequest<Input>,
646+
/// context: GRPCCore.ServerContext
647+
/// ) async throws -> GRPCCore.ServerResponse<Output> {
648+
/// return GRPCCore.ServerResponse<Output>(
649+
/// message: try await self.<Name>(
650+
/// request: request.message,
651+
/// context: context
652+
/// )
653+
/// metadata: [:]
654+
/// )
655+
/// }
656+
/// ```
657+
static func serviceProtocolDefaultImplementation(
658+
accessModifier: AccessModifier? = nil,
659+
name: String,
660+
input: String,
661+
output: String,
662+
streamingInput: Bool,
663+
streamingOutput: Bool
664+
) -> Self {
665+
func makeUnaryOutputArguments() -> [FunctionArgumentDescription] {
666+
return [
667+
FunctionArgumentDescription(
668+
label: "message",
669+
expression: .functionCall(
670+
.serviceMethodCallingSimpleMethod(
671+
name: name,
672+
input: input,
673+
output: output,
674+
streamingInput: streamingInput,
675+
streamingOutput: streamingOutput
676+
)
677+
)
678+
),
679+
FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))),
680+
]
681+
}
682+
683+
func makeStreamingOutputArguments() -> [FunctionArgumentDescription] {
684+
return [
685+
FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))),
686+
FunctionArgumentDescription(
687+
label: "producer",
688+
expression: .closureInvocation(
689+
argumentNames: ["writer"],
690+
body: [
691+
.expression(
692+
.functionCall(
693+
.serviceMethodCallingSimpleMethod(
694+
name: name,
695+
input: input,
696+
output: output,
697+
streamingInput: streamingInput,
698+
streamingOutput: streamingOutput
699+
)
700+
)
701+
),
702+
.expression(.return(.literal(.dictionary([])))),
703+
]
704+
)
705+
),
706+
]
707+
}
708+
709+
return FunctionDescription(
710+
signature: .serverMethod(
711+
accessLevel: accessModifier,
712+
name: name,
713+
input: input,
714+
output: output,
715+
streamingInput: streamingInput,
716+
streamingOutput: streamingOutput
717+
),
718+
body: [
719+
.expression(
720+
.functionCall(
721+
calledExpression: .return(
722+
.identifierType(
723+
.serverResponse(forType: output, streaming: streamingOutput)
724+
)
725+
),
726+
arguments: streamingOutput ? makeStreamingOutputArguments() : makeUnaryOutputArguments()
727+
)
728+
)
729+
]
730+
)
731+
}
732+
}
733+
734+
extension ExtensionDescription {
735+
/// ```
736+
/// extension ServiceProtocol {
737+
/// ...
738+
/// }
739+
/// ```
740+
static func serviceProtocolDefaultImplementation(
741+
accessModifier: AccessModifier? = nil,
742+
on extensionName: String,
743+
methods: [MethodDescriptor]
744+
) -> Self {
745+
ExtensionDescription(
746+
onType: extensionName,
747+
declarations: methods.map { method in
748+
.function(
749+
.serviceProtocolDefaultImplementation(
750+
accessModifier: accessModifier,
751+
name: method.name.generatedLowerCase,
752+
input: method.inputType,
753+
output: method.outputType,
754+
streamingInput: method.isInputStreaming,
755+
streamingOutput: method.isOutputStreaming
756+
)
757+
)
758+
}
759+
)
760+
}
761+
}

Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ extension ExistingTypeDescription {
7070
.generic(wrapper: .grpcCore("RPCWriter"), wrapped: .member(type))
7171
}
7272

73+
package static func rpcAsyncSequence(forType type: String) -> Self {
74+
.generic(
75+
wrapper: .grpcCore("RPCAsyncSequence"),
76+
wrapped: .member(type),
77+
.any(.member(["Swift", "Error"]))
78+
)
79+
}
80+
7381
package static let callOptions: Self = .grpcCore("CallOptions")
7482
package static let metadata: Self = .grpcCore("Metadata")
7583
package static let grpcClient: Self = .grpcCore("GRPCClient")

Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,10 +453,10 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable {
453453
/// For example, `Foo?`.
454454
case optional(ExistingTypeDescription)
455455

456-
/// A wrapper type generic over a wrapped type.
456+
/// A wrapper type generic over a list of wrapped types.
457457
///
458458
/// For example, `Wrapper<Wrapped>`.
459-
case generic(wrapper: ExistingTypeDescription, wrapped: ExistingTypeDescription)
459+
case generic(wrapper: ExistingTypeDescription, wrapped: [ExistingTypeDescription])
460460

461461
/// A type reference represented by the components.
462462
///
@@ -483,6 +483,16 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable {
483483
///
484484
/// For example: `(String) async throws -> Int`.
485485
case closure(ClosureSignatureDescription)
486+
487+
/// A wrapper type generic over a list of wrapped types.
488+
///
489+
/// For example, `Wrapper<Wrapped>`.
490+
static func generic(
491+
wrapper: ExistingTypeDescription,
492+
wrapped: ExistingTypeDescription...
493+
) -> Self {
494+
return .generic(wrapper: wrapper, wrapped: Array(wrapped))
495+
}
486496
}
487497

488498
/// A description of a typealias declaration.

Sources/GRPCCodeGen/Internal/Translator/Docs.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ package enum Docs {
5656
"""
5757

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

6262
return header + "\n" + body

0 commit comments

Comments
 (0)