Browse Source

[CodeGenLib] Translator for server code (#1745)

Motivation:

In the generated server code for a service described in a Source IDL file
we want to have:
- a protocol defining all the methods as bidirectional streaming, conforming to `RegistrableRPCService`
- an extension for the streaming protocol that implements the `registerRPCs`
function that is required by the `RegistrableRPCService` conformance
- a service protocol that defines the methods of the service, as they are
descibed in the Source IDL (unary/input-streaming/output-streaming/bidirectional-streaming) that conforms to the streaming protocol.
- the extension of the service protocol that implements the bidirectional
streaming methods required by the conformance to the streaming protocol, using
calls to the service methods (in case a service method is bidirectional
streaming), it doesn't need to be reimplemented.

Modifications:

- Created the ServerCodeTranslator that conforms to SpecializedTranslator and
contains all the methods for creating the representation of the generated code
that we want using StructuredSwiftRepresentation format.
- Implemented snippet tests that test the ServerCodeTranslator.
- added the inout parameter type as a case of ExistingTypeDescription in StructuredSwiftRepresentation, as it is needed by the `registerRPCs` function.

Result:

Server code can now be generated for a CodeGenerationRequest object.
Stefana-Ioana Dranca 2 years ago
parent
commit
717e3f8788

+ 4 - 0
Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift

@@ -839,6 +839,10 @@ struct TextBasedRenderer: RendererProtocol {
     }
     writer.writeLine(": ")
     writer.nextLineAppendsToLastLine()
+    if parameterDescription.inout {
+      writer.writeLine("inout ")
+      writer.nextLineAppendsToLastLine()
+    }
     writer.writeLine(renderedExistingTypeDescription(parameterDescription.type))
     if let defaultValue = parameterDescription.defaultValue {
       writer.nextLineAppendsToLastLine()

+ 5 - 0
Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift

@@ -490,6 +490,11 @@ struct ParameterDescription: Equatable, Codable {
   /// For example, in `bar baz: String = "hi"`, `defaultValue`
   /// represents `"hi"`.
   var defaultValue: Expression? = nil
+
+  /// An inout parameter type.
+  ///
+  /// For example, `bar baz: inout String`.
+  var `inout`: Bool = false
 }
 
 /// A function kind: `func` or `init`.

+ 25 - 0
Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift

@@ -16,6 +16,7 @@
 
 struct IDLToStructuredSwiftTranslator: Translator {
   private let typealiasTranslator = TypealiasTranslator()
+  private let serverCodeTranslator = ServerCodeTranslator()
 
   func translate(
     codeGenerationRequest: CodeGenerationRequest,
@@ -31,6 +32,12 @@ struct IDLToStructuredSwiftTranslator: Translator {
       contentsOf: try self.typealiasTranslator.translate(from: codeGenerationRequest)
     )
 
+    if server {
+      codeBlocks.append(
+        contentsOf: try self.serverCodeTranslator.translate(from: codeGenerationRequest)
+      )
+    }
+
     let fileDescription = FileDescription(
       topComment: topComment,
       imports: imports,
@@ -41,3 +48,21 @@ struct IDLToStructuredSwiftTranslator: Translator {
     return StructuredSwiftRepresentation(file: file)
   }
 }
+
+extension CodeGenerationRequest.ServiceDescriptor {
+  var namespacedTypealiasPrefix: String {
+    if self.namespace.isEmpty {
+      return self.name
+    } else {
+      return "\(self.namespace).\(self.name)"
+    }
+  }
+
+  var namespacedPrefix: String {
+    if self.namespace.isEmpty {
+      return self.name
+    } else {
+      return "\(self.namespace)_\(self.name)"
+    }
+  }
+}

+ 474 - 0
Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift

@@ -0,0 +1,474 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+/// Creates a representation for the server code that will be generated based on the ``CodeGenerationRequest`` object
+/// specifications, using types from ``StructuredSwiftRepresentation``.
+///
+/// For example, in the case of a service called "Bar", in the "foo" namespace which has
+/// one method "baz", the ``ServerCodeTranslator`` will create
+/// a representation for the following generated code:
+///
+/// ```swift
+/// public protocol foo_BarServiceStreamingProtocol: GRPCCore.RegistrableRPCService {
+///   func baz(
+///     request: ServerRequest.Stream<foo.Method.baz.Input>
+///   ) async throws -> ServerResponse.Stream<foo.Method.baz.Output>
+/// }
+/// // Generated conformance to `RegistrableRPCService`.
+/// extension foo.Bar.StreamingServiceProtocol {
+///   public func registerRPCs(with router: inout RPCRouter) {
+///     router.registerHandler(
+///       for: foo.Method.baz.descriptor,
+///       deserializer: ProtobufDeserializer<foo.Methods.baz.Input>(),
+///       serializer: ProtobufSerializer<foo.Methods.baz.Output>(),
+///       handler: { request in try await self.baz(request: request) }
+///     )
+///   }
+/// }
+/// public protocol foo_BarServiceProtocol: foo.Bar.StreamingServiceProtocol {
+///   func baz(
+///     request: ServerRequest.Single<foo.Bar.Methods.baz.Input>
+///   ) async throws -> ServerResponse.Single<foo.Bar.Methods.baz.Output>
+/// }
+/// // Generated partial conformance to `foo_BarStreamingServiceProtocol`.
+/// extension foo.Bar.ServiceProtocol {
+///   public func baz(
+///     request: ServerRequest.Stream<foo.Bar.Methods.baz.Input>
+///   ) async throws -> ServerResponse.Stream<foo.Bar.Methods.baz.Output> {
+///     let response = try await self.baz(request: ServerRequest.Single(stream: request)
+///     return ServerResponse.Stream(single: response)
+///   }
+/// }
+///```
+struct ServerCodeTranslator: SpecializedTranslator {
+  func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] {
+    var codeBlocks = [CodeBlock]()
+    for service in codeGenerationRequest.services {
+      // Create the streaming protocol that declares the service methods as bidirectional streaming.
+      let streamingProtocol = CodeBlockItem.declaration(self.makeStreamingProtocol(for: service))
+      codeBlocks.append(CodeBlock(item: streamingProtocol))
+
+      // Create extension for implementing the 'registerRPCs' function which is a 'RegistrableRPCService' requirement.
+      let conformanceToRPCServiceExtension = CodeBlockItem.declaration(
+        self.makeConformanceToRPCServiceExtension(for: service, in: codeGenerationRequest)
+      )
+      codeBlocks.append(
+        CodeBlock(
+          comment: .doc("Conformance to `GRPCCore.RegistrableRPCService`."),
+          item: conformanceToRPCServiceExtension
+        )
+      )
+
+      // Create the service protocol that declares the service methods as they are described in the Source IDL (unary,
+      // client/server streaming or bidirectional streaming).
+      let serviceProtocol = CodeBlockItem.declaration(self.makeServiceProtocol(for: service))
+      codeBlocks.append(CodeBlock(item: serviceProtocol))
+
+      // Create extension for partial conformance to the streaming protocol.
+      let extensionServiceProtocol = CodeBlockItem.declaration(
+        self.makeExtensionServiceProtocol(for: service)
+      )
+      codeBlocks.append(
+        CodeBlock(
+          comment: .doc(
+            "Partial conformance to `\(self.protocolName(service: service, streaming: true))`."
+          ),
+          item: extensionServiceProtocol
+        )
+      )
+    }
+
+    return codeBlocks
+  }
+}
+
+extension ServerCodeTranslator {
+  private func makeStreamingProtocol(
+    for service: CodeGenerationRequest.ServiceDescriptor
+  ) -> Declaration {
+    let methods = service.methods.compactMap {
+      Declaration.commentable(
+        .doc($0.documentation),
+        .function(
+          FunctionDescription(
+            signature: self.makeStreamingMethodSignature(for: $0, in: service)
+          )
+        )
+      )
+    }
+
+    let streamingProtocol = Declaration.protocol(
+      .init(
+        name: self.protocolName(service: service, streaming: true),
+        conformances: ["GRPCCore.RegistrableRPCService"],
+        members: methods
+      )
+    )
+
+    return .commentable(.doc(service.documentation), streamingProtocol)
+  }
+
+  private func makeStreamingMethodSignature(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
+    in service: CodeGenerationRequest.ServiceDescriptor
+  ) -> FunctionSignatureDescription {
+    return FunctionSignatureDescription(
+      kind: .function(name: method.name),
+      parameters: [
+        .init(
+          label: "request",
+          type: .generic(
+            wrapper: .member(["ServerRequest", "Stream"]),
+            wrapped: .member(
+              self.methodInputOutputTypealias(for: method, service: service, type: .input)
+            )
+          )
+        )
+      ],
+      keywords: [.async, .throws],
+      returnType: .identifierType(
+        .generic(
+          wrapper: .member(["ServerResponse", "Stream"]),
+          wrapped: .member(
+            self.methodInputOutputTypealias(for: method, service: service, type: .output)
+          )
+        )
+      )
+    )
+  }
+
+  private func makeConformanceToRPCServiceExtension(
+    for service: CodeGenerationRequest.ServiceDescriptor,
+    in codeGenerationRequest: CodeGenerationRequest
+  ) -> Declaration {
+    let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true)
+    let registerRPCMethod = self.makeRegisterRPCsMethod(for: service, in: codeGenerationRequest)
+    return .extension(
+      accessModifier: .public,
+      onType: streamingProtocol,
+      declarations: [registerRPCMethod]
+    )
+  }
+
+  private func makeRegisterRPCsMethod(
+    for service: CodeGenerationRequest.ServiceDescriptor,
+    in codeGenerationRequest: CodeGenerationRequest
+  ) -> Declaration {
+    let registerRPCsSignature = FunctionSignatureDescription(
+      kind: .function(name: "registerRPCs"),
+      parameters: [
+        .init(
+          label: "with",
+          name: "router",
+          type: .member(["GRPCCore", "RPCRouter"]),
+          `inout`: true
+        )
+      ]
+    )
+    let registerRPCsBody = self.makeRegisterRPCsMethodBody(for: service, in: codeGenerationRequest)
+    return .function(signature: registerRPCsSignature, body: registerRPCsBody)
+  }
+
+  private func makeRegisterRPCsMethodBody(
+    for service: CodeGenerationRequest.ServiceDescriptor,
+    in codeGenerationRequest: CodeGenerationRequest
+  ) -> [CodeBlock] {
+    let registerHandlerCalls = service.methods.compactMap {
+      CodeBlock.expression(
+        Expression.functionCall(
+          calledExpression: .memberAccess(
+            MemberAccessDescription(left: .identifierPattern("router"), right: "registerHandler")
+          ),
+          arguments: self.makeArgumentsForRegisterHandler(
+            for: $0,
+            in: service,
+            from: codeGenerationRequest
+          )
+        )
+      )
+    }
+
+    return registerHandlerCalls
+  }
+
+  private func makeArgumentsForRegisterHandler(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
+    in service: CodeGenerationRequest.ServiceDescriptor,
+    from codeGenerationRequest: CodeGenerationRequest
+  ) -> [FunctionArgumentDescription] {
+    var arguments = [FunctionArgumentDescription]()
+    arguments.append(
+      .init(
+        label: "for",
+        expression: .identifierPattern(
+          self.methodDescriptorPath(for: method, service: service)
+        )
+      )
+    )
+
+    arguments.append(
+      .init(
+        label: "deserializer",
+        expression: .identifierPattern(
+          codeGenerationRequest.lookupDeserializer(
+            self.methodInputOutputTypealias(for: method, service: service, type: .input)
+          )
+        )
+      )
+    )
+
+    arguments.append(
+      .init(
+        label: "serializer",
+        expression:
+          .identifierPattern(
+            codeGenerationRequest.lookupSerializer(
+              self.methodInputOutputTypealias(for: method, service: service, type: .output)
+            )
+          )
+      )
+    )
+
+    let getFunctionCall = Expression.functionCall(
+      calledExpression: .memberAccess(
+        MemberAccessDescription(left: .identifierPattern("self"), right: method.name)
+      ),
+      arguments: [
+        FunctionArgumentDescription(label: "request", expression: .identifierPattern("request"))
+      ]
+    )
+
+    let handlerClosureBody = Expression.unaryKeyword(
+      kind: .try,
+      expression: .unaryKeyword(kind: .await, expression: getFunctionCall)
+    )
+
+    arguments.append(
+      .init(
+        label: "handler",
+        expression: .closureInvocation(
+          .init(argumentNames: ["request"], body: [.expression(handlerClosureBody)])
+        )
+      )
+    )
+
+    return arguments
+  }
+
+  private func makeServiceProtocol(
+    for service: CodeGenerationRequest.ServiceDescriptor
+  ) -> Declaration {
+    let methods = service.methods.compactMap {
+      self.makeServiceProtocolMethod(for: $0, in: service)
+    }
+    let protocolName = self.protocolName(service: service, streaming: false)
+    let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true)
+
+    return .commentable(
+      .doc(service.documentation),
+      .protocol(
+        ProtocolDescription(name: protocolName, conformances: [streamingProtocol], members: methods)
+      )
+    )
+  }
+
+  private func makeServiceProtocolMethod(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
+    in service: CodeGenerationRequest.ServiceDescriptor
+  ) -> Declaration {
+    let inputStreaming = method.isInputStreaming ? "Stream" : "Single"
+    let outputStreaming = method.isOutputStreaming ? "Stream" : "Single"
+
+    let inputTypealiasComponents = self.methodInputOutputTypealias(
+      for: method,
+      service: service,
+      type: .input
+    )
+    let outputTypealiasComponents = self.methodInputOutputTypealias(
+      for: method,
+      service: service,
+      type: .output
+    )
+
+    let functionSignature = FunctionSignatureDescription(
+      kind: .function(name: method.name),
+      parameters: [
+        .init(
+          label: "request",
+          type:
+            .generic(
+              wrapper: .member(["ServerRequest", inputStreaming]),
+              wrapped: .member(inputTypealiasComponents)
+            )
+        )
+      ],
+      keywords: [.async, .throws],
+      returnType: .identifierType(
+        .generic(
+          wrapper: .member(["ServerResponse", outputStreaming]),
+          wrapped: .member(outputTypealiasComponents)
+        )
+      )
+    )
+
+    return .commentable(
+      .doc(method.documentation),
+      .function(FunctionDescription(signature: functionSignature))
+    )
+  }
+
+  private func makeExtensionServiceProtocol(
+    for service: CodeGenerationRequest.ServiceDescriptor
+  ) -> Declaration {
+    let methods = service.methods.compactMap {
+      self.makeServiceProtocolExtensionMethod(for: $0, in: service)
+    }
+
+    let protocolName = self.protocolNameTypealias(service: service, streaming: false)
+    return .extension(accessModifier: .public, onType: protocolName, declarations: methods)
+  }
+
+  private func makeServiceProtocolExtensionMethod(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
+    in service: CodeGenerationRequest.ServiceDescriptor
+  ) -> Declaration? {
+    // The method has the same definition in StreamingServiceProtocol and ServiceProtocol.
+    if method.isInputStreaming && method.isOutputStreaming {
+      return nil
+    }
+
+    let response = CodeBlock(item: .declaration(self.makeResponse(for: method)))
+    let returnStatement = CodeBlock(item: .expression(self.makeReturnStatement(for: method)))
+
+    return .function(
+      signature: self.makeStreamingMethodSignature(for: method, in: service),
+      body: [response, returnStatement]
+    )
+  }
+
+  private func makeResponse(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor
+  ) -> Declaration {
+    let serverRequest: Expression
+    if !method.isInputStreaming {
+      // Transform the streaming request into a unary request.
+      serverRequest = Expression.functionCall(
+        calledExpression: .memberAccess(
+          MemberAccessDescription(
+            left: .identifierPattern("ServerRequest"),
+            right: "Single"
+          )
+        ),
+        arguments: [
+          FunctionArgumentDescription(label: "stream", expression: .identifierPattern("request"))
+        ]
+      )
+    } else {
+      serverRequest = Expression.identifierPattern("request")
+    }
+    // Call to the corresponding ServiceProtocol method.
+    let serviceProtocolMethod = Expression.functionCall(
+      calledExpression: .memberAccess(
+        MemberAccessDescription(left: .identifierPattern("self"), right: method.name)
+      ),
+      arguments: [FunctionArgumentDescription(label: "request", expression: serverRequest)]
+    )
+
+    let responseValue = Expression.unaryKeyword(
+      kind: .try,
+      expression: .unaryKeyword(kind: .await, expression: serviceProtocolMethod)
+    )
+
+    return .variable(kind: .let, left: "response", right: responseValue)
+  }
+
+  private func makeReturnStatement(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor
+  ) -> Expression {
+    let returnValue: Expression
+    // Transforming the unary response into a streaming one.
+    if !method.isOutputStreaming {
+      returnValue = .functionCall(
+        calledExpression: .memberAccess(
+          MemberAccessDescription(
+            left: .identifierType(.member(["ServerResponse"])),
+            right: "Stream"
+          )
+        ),
+        arguments: [
+          (FunctionArgumentDescription(label: "single", expression: .identifierPattern("response")))
+        ]
+      )
+    } else {
+      returnValue = .identifierPattern("response")
+    }
+
+    return .unaryKeyword(kind: .return, expression: returnValue)
+  }
+
+  fileprivate enum InputOutputType {
+    case input
+    case output
+  }
+
+  /// Generates the fully qualified name of the typealias for the input or output type of a method.
+  private func methodInputOutputTypealias(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
+    service: CodeGenerationRequest.ServiceDescriptor,
+    type: InputOutputType
+  ) -> String {
+    var components: String = "\(service.namespacedTypealiasPrefix).Methods.\(method.name)"
+
+    switch type {
+    case .input:
+      components.append(".Input")
+    case .output:
+      components.append(".Output")
+    }
+
+    return components
+  }
+
+  /// Generates the fully qualified name of a method descriptor.
+  private func methodDescriptorPath(
+    for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
+    service: CodeGenerationRequest.ServiceDescriptor
+  ) -> String {
+    return "\(service.namespacedTypealiasPrefix).Methods.\(method.name).descriptor"
+  }
+
+  /// Generates the fully qualified name of the type alias for a service protocol.
+  internal func protocolNameTypealias(
+    service: CodeGenerationRequest.ServiceDescriptor,
+    streaming: Bool
+  ) -> String {
+    if streaming {
+      return "\(service.namespacedTypealiasPrefix).StreamingServiceProtocol"
+    }
+    return "\(service.namespacedTypealiasPrefix).ServiceProtocol"
+  }
+
+  /// Generates the name of a service protocol.
+  internal func protocolName(
+    service: CodeGenerationRequest.ServiceDescriptor,
+    streaming: Bool
+  ) -> String {
+    if streaming {
+      return "\(service.namespacedPrefix)StreamingServiceProtocol"
+    }
+    return "\(service.namespacedPrefix)ServiceProtocol"
+  }
+}

+ 431 - 0
Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift

@@ -0,0 +1,431 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import XCTest
+
+@testable import GRPCCodeGen
+
+final class ServerCodeTranslatorSnippetBasedTests: XCTestCase {
+  typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor
+  typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor
+
+  func testServerCodeTranslatorUnaryMethod() throws {
+    let method = MethodDescriptor(
+      documentation: "Documentation for unaryMethod",
+      name: "unaryMethod",
+      isInputStreaming: false,
+      isOutputStreaming: false,
+      inputType: "NamespaceA_ServiceARequest",
+      outputType: "NamespaceA_ServiceAResponse"
+    )
+    let service = ServiceDescriptor(
+      documentation: "Documentation for ServiceA",
+      name: "ServiceA",
+      namespace: "namespaceA",
+      methods: [method]
+    )
+    let expectedSwift =
+      """
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Documentation for unaryMethod
+          func unaryMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.unaryMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.unaryMethod.Output>
+      }
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension namespaceA.ServiceA.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {
+              router.registerHandler(
+                  for: namespaceA.ServiceA.Methods.unaryMethod.descriptor,
+                  deserializer: ProtobufDeserializer<namespaceA.ServiceA.Methods.unaryMethod.Input>(),
+                  serializer: ProtobufSerializer<namespaceA.ServiceA.Methods.unaryMethod.Output>(),
+                  handler: { request in
+                      try await self.unaryMethod(request: request)
+                  }
+              )
+          }
+      }
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {
+          /// Documentation for unaryMethod
+          func unaryMethod(request: ServerRequest.Single<namespaceA.ServiceA.Methods.unaryMethod.Input>) async throws -> ServerResponse.Single<namespaceA.ServiceA.Methods.unaryMethod.Output>
+      }
+      /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`.
+      public extension namespaceA.ServiceA.ServiceProtocol {
+          func unaryMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.unaryMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.unaryMethod.Output> {
+              let response = try await self.unaryMethod(request: ServerRequest.Single(stream: request))
+              return ServerResponse.Stream(single: response)
+          }
+      }
+      """
+
+    try self.assertServerCodeTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testServerCodeTranslatorInputStreamingMethod() throws {
+    let method = MethodDescriptor(
+      documentation: "Documentation for inputStreamingMethod",
+      name: "inputStreamingMethod",
+      isInputStreaming: true,
+      isOutputStreaming: false,
+      inputType: "NamespaceA_ServiceARequest",
+      outputType: "NamespaceA_ServiceAResponse"
+    )
+    let service = ServiceDescriptor(
+      documentation: "Documentation for ServiceA",
+      name: "ServiceA",
+      namespace: "namespaceA",
+      methods: [method]
+    )
+    let expectedSwift =
+      """
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Documentation for inputStreamingMethod
+          func inputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Output>
+      }
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension namespaceA.ServiceA.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {
+              router.registerHandler(
+                  for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor,
+                  deserializer: ProtobufDeserializer<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>(),
+                  serializer: ProtobufSerializer<namespaceA.ServiceA.Methods.inputStreamingMethod.Output>(),
+                  handler: { request in
+                      try await self.inputStreamingMethod(request: request)
+                  }
+              )
+          }
+      }
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {
+          /// Documentation for inputStreamingMethod
+          func inputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>) async throws -> ServerResponse.Single<namespaceA.ServiceA.Methods.inputStreamingMethod.Output>
+      }
+      /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`.
+      public extension namespaceA.ServiceA.ServiceProtocol {
+          func inputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Output> {
+              let response = try await self.inputStreamingMethod(request: request)
+              return ServerResponse.Stream(single: response)
+          }
+      }
+      """
+
+    try self.assertServerCodeTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testServerCodeTranslatorOutputStreamingMethod() throws {
+    let method = MethodDescriptor(
+      documentation: "Documentation for outputStreamingMethod",
+      name: "outputStreamingMethod",
+      isInputStreaming: false,
+      isOutputStreaming: true,
+      inputType: "NamespaceA_ServiceARequest",
+      outputType: "NamespaceA_ServiceAResponse"
+    )
+    let service = ServiceDescriptor(
+      documentation: "Documentation for ServiceA",
+      name: "ServiceA",
+      namespace: "namespaceA",
+      methods: [method]
+    )
+    let expectedSwift =
+      """
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Documentation for outputStreamingMethod
+          func outputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Output>
+      }
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension namespaceA.ServiceA.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {
+              router.registerHandler(
+                  for: namespaceA.ServiceA.Methods.outputStreamingMethod.descriptor,
+                  deserializer: ProtobufDeserializer<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>(),
+                  serializer: ProtobufSerializer<namespaceA.ServiceA.Methods.outputStreamingMethod.Output>(),
+                  handler: { request in
+                      try await self.outputStreamingMethod(request: request)
+                  }
+              )
+          }
+      }
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {
+          /// Documentation for outputStreamingMethod
+          func outputStreamingMethod(request: ServerRequest.Single<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Output>
+      }
+      /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`.
+      public extension namespaceA.ServiceA.ServiceProtocol {
+          func outputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Output> {
+              let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request))
+              return response
+          }
+      }
+      """
+
+    try self.assertServerCodeTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testServerCodeTranslatorBidirectionalStreamingMethod() throws {
+    let method = MethodDescriptor(
+      documentation: "Documentation for bidirectionalStreamingMethod",
+      name: "bidirectionalStreamingMethod",
+      isInputStreaming: true,
+      isOutputStreaming: true,
+      inputType: "NamespaceA_ServiceARequest",
+      outputType: "NamespaceA_ServiceAResponse"
+    )
+    let service = ServiceDescriptor(
+      documentation: "Documentation for ServiceA",
+      name: "ServiceA",
+      namespace: "namespaceA",
+      methods: [method]
+    )
+    let expectedSwift =
+      """
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Documentation for bidirectionalStreamingMethod
+          func bidirectionalStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.Output>
+      }
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension namespaceA.ServiceA.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {
+              router.registerHandler(
+                  for: namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.descriptor,
+                  deserializer: ProtobufDeserializer<namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.Input>(),
+                  serializer: ProtobufSerializer<namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.Output>(),
+                  handler: { request in
+                      try await self.bidirectionalStreamingMethod(request: request)
+                  }
+              )
+          }
+      }
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {
+          /// Documentation for bidirectionalStreamingMethod
+          func bidirectionalStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.Output>
+      }
+      /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`.
+      public extension namespaceA.ServiceA.ServiceProtocol {
+      }
+      """
+
+    try self.assertServerCodeTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testServerCodeTranslatorMultipleMethods() throws {
+    let inputStreamingMethod = MethodDescriptor(
+      documentation: "Documentation for inputStreamingMethod",
+      name: "inputStreamingMethod",
+      isInputStreaming: true,
+      isOutputStreaming: false,
+      inputType: "NamespaceA_ServiceARequest",
+      outputType: "NamespaceA_ServiceAResponse"
+    )
+    let outputStreamingMethod = MethodDescriptor(
+      documentation: "Documentation for outputStreamingMethod",
+      name: "outputStreamingMethod",
+      isInputStreaming: false,
+      isOutputStreaming: true,
+      inputType: "NamespaceA_ServiceARequest",
+      outputType: "NamespaceA_ServiceAResponse"
+    )
+    let service = ServiceDescriptor(
+      documentation: "Documentation for ServiceA",
+      name: "ServiceA",
+      namespace: "namespaceA",
+      methods: [inputStreamingMethod, outputStreamingMethod]
+    )
+    let expectedSwift =
+      """
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Documentation for inputStreamingMethod
+          func inputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Output>
+          /// Documentation for outputStreamingMethod
+          func outputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Output>
+      }
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension namespaceA.ServiceA.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {
+              router.registerHandler(
+                  for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor,
+                  deserializer: ProtobufDeserializer<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>(),
+                  serializer: ProtobufSerializer<namespaceA.ServiceA.Methods.inputStreamingMethod.Output>(),
+                  handler: { request in
+                      try await self.inputStreamingMethod(request: request)
+                  }
+              )
+              router.registerHandler(
+                  for: namespaceA.ServiceA.Methods.outputStreamingMethod.descriptor,
+                  deserializer: ProtobufDeserializer<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>(),
+                  serializer: ProtobufSerializer<namespaceA.ServiceA.Methods.outputStreamingMethod.Output>(),
+                  handler: { request in
+                      try await self.outputStreamingMethod(request: request)
+                  }
+              )
+          }
+      }
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {
+          /// Documentation for inputStreamingMethod
+          func inputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>) async throws -> ServerResponse.Single<namespaceA.ServiceA.Methods.inputStreamingMethod.Output>
+          /// Documentation for outputStreamingMethod
+          func outputStreamingMethod(request: ServerRequest.Single<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Output>
+      }
+      /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`.
+      public extension namespaceA.ServiceA.ServiceProtocol {
+          func inputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.inputStreamingMethod.Output> {
+              let response = try await self.inputStreamingMethod(request: request)
+              return ServerResponse.Stream(single: response)
+          }
+          func outputStreamingMethod(request: ServerRequest.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Input>) async throws -> ServerResponse.Stream<namespaceA.ServiceA.Methods.outputStreamingMethod.Output> {
+              let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request))
+              return response
+          }
+      }
+      """
+
+    try assertServerCodeTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testServerCodeTranslatorNoNamespaceService() throws {
+    let method = MethodDescriptor(
+      documentation: "Documentation for MethodA",
+      name: "methodA",
+      isInputStreaming: false,
+      isOutputStreaming: false,
+      inputType: "NamespaceA_ServiceARequest",
+      outputType: "NamespaceA_ServiceAResponse"
+    )
+    let service = ServiceDescriptor(
+      documentation: "Documentation for ServiceA",
+      name: "ServiceA",
+      namespace: "",
+      methods: [method]
+    )
+    let expectedSwift =
+      """
+      /// Documentation for ServiceA
+      protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Documentation for MethodA
+          func methodA(request: ServerRequest.Stream<ServiceA.Methods.methodA.Input>) async throws -> ServerResponse.Stream<ServiceA.Methods.methodA.Output>
+      }
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension ServiceA.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {
+              router.registerHandler(
+                  for: ServiceA.Methods.methodA.descriptor,
+                  deserializer: ProtobufDeserializer<ServiceA.Methods.methodA.Input>(),
+                  serializer: ProtobufSerializer<ServiceA.Methods.methodA.Output>(),
+                  handler: { request in
+                      try await self.methodA(request: request)
+                  }
+              )
+          }
+      }
+      /// Documentation for ServiceA
+      protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol {
+          /// Documentation for MethodA
+          func methodA(request: ServerRequest.Single<ServiceA.Methods.methodA.Input>) async throws -> ServerResponse.Single<ServiceA.Methods.methodA.Output>
+      }
+      /// Partial conformance to `ServiceAStreamingServiceProtocol`.
+      public extension ServiceA.ServiceProtocol {
+          func methodA(request: ServerRequest.Stream<ServiceA.Methods.methodA.Input>) async throws -> ServerResponse.Stream<ServiceA.Methods.methodA.Output> {
+              let response = try await self.methodA(request: ServerRequest.Single(stream: request))
+              return ServerResponse.Stream(single: response)
+          }
+      }
+      """
+
+    try self.assertServerCodeTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testServerCodeTranslatorMoreServicesOrder() throws {
+    let serviceA = ServiceDescriptor(
+      documentation: "Documentation for ServiceA",
+      name: "ServiceA",
+      namespace: "namespaceA",
+      methods: []
+    )
+    let serviceB = ServiceDescriptor(
+      documentation: "Documentation for ServiceB",
+      name: "ServiceB",
+      namespace: "namespaceA",
+      methods: []
+    )
+    let expectedSwift =
+      """
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {}
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension namespaceA.ServiceA.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {}
+      }
+      /// Documentation for ServiceA
+      protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {}
+      /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`.
+      public extension namespaceA.ServiceA.ServiceProtocol {
+      }
+      /// Documentation for ServiceB
+      protocol namespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {}
+      /// Conformance to `GRPCCore.RegistrableRPCService`.
+      public extension namespaceA.ServiceB.StreamingServiceProtocol {
+          func registerRPCs(with router: inout GRPCCore.RPCRouter) {}
+      }
+      /// Documentation for ServiceB
+      protocol namespaceA_ServiceBServiceProtocol: namespaceA.ServiceB.StreamingServiceProtocol {}
+      /// Partial conformance to `namespaceA_ServiceBStreamingServiceProtocol`.
+      public extension namespaceA.ServiceB.ServiceProtocol {
+      }
+      """
+
+    try self.assertServerCodeTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  private func assertServerCodeTranslation(
+    codeGenerationRequest: CodeGenerationRequest,
+    expectedSwift: String
+  ) throws {
+    let translator = ServerCodeTranslator()
+    let codeBlocks = try translator.translate(from: codeGenerationRequest)
+    let renderer = TextBasedRenderer.default
+    renderer.renderCodeBlocks(codeBlocks)
+    let contents = renderer.renderedContents()
+    try XCTAssertEqualWithDiff(contents, expectedSwift)
+  }
+}

+ 32 - 0
Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift

@@ -31,6 +31,8 @@
 
 import XCTest
 
+import GRPCCodeGen
+
 private func diff(expected: String, actual: String) throws -> String {
   let process = Process()
   process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
@@ -69,4 +71,34 @@ internal func XCTAssertEqualWithDiff(
   )
 }
 
+internal func makeCodeGenerationRequest(
+  services: [CodeGenerationRequest.ServiceDescriptor]
+) -> CodeGenerationRequest {
+  return CodeGenerationRequest(
+    fileName: "test.grpc",
+    leadingTrivia: "Some really exciting license header 2023.",
+    dependencies: [],
+    services: services,
+    lookupSerializer: {
+      "ProtobufSerializer<\($0)>()"
+    },
+    lookupDeserializer: {
+      "ProtobufDeserializer<\($0)>()"
+    }
+  )
+}
+
+internal func XCTAssertThrowsError<T, E: Error>(
+  ofType: E.Type,
+  _ expression: @autoclosure () throws -> T,
+  _ errorHandler: (E) -> Void
+) {
+  XCTAssertThrowsError(try expression()) { error in
+    guard let error = error as? E else {
+      return XCTFail("Error had unexpected type '\(type(of: error))'")
+    }
+    errorHandler(error)
+  }
+}
+
 #endif  // os(macOS) || os(Linux)

+ 18 - 48
Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift → Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift

@@ -20,7 +20,7 @@ import XCTest
 
 @testable import GRPCCodeGen
 
-final class SnippetBasedTranslatorTests: XCTestCase {
+final class TypealiasTranslatorSnippetBasedTests: XCTestCase {
   typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor
   typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor
 
@@ -63,7 +63,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
       expectedSwift: expectedSwift
     )
   }
@@ -105,7 +105,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
       expectedSwift: expectedSwift
     )
   }
@@ -166,7 +166,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
       expectedSwift: expectedSwift
     )
   }
@@ -191,7 +191,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
       expectedSwift: expectedSwift
     )
   }
@@ -230,7 +230,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]),
       expectedSwift: expectedSwift
     )
   }
@@ -267,7 +267,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]),
       expectedSwift: expectedSwift
     )
   }
@@ -308,7 +308,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]),
       expectedSwift: expectedSwift
     )
   }
@@ -345,7 +345,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       """
 
     try self.assertTypealiasTranslation(
-      codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceA, serviceB]),
+      codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]),
       expectedSwift: expectedSwift
     )
   }
@@ -358,9 +358,9 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       methods: []
     )
 
-    let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceA])
+    let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA])
     let translator = TypealiasTranslator()
-    self.assertThrowsError(
+    XCTAssertThrowsError(
       ofType: CodeGenError.self,
       try translator.translate(from: codeGenerationRequest)
     ) {
@@ -386,9 +386,9 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       methods: []
     )
 
-    let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceA])
+    let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA])
     let translator = TypealiasTranslator()
-    self.assertThrowsError(
+    XCTAssertThrowsError(
       ofType: CodeGenError.self,
       try translator.translate(from: codeGenerationRequest)
     ) {
@@ -422,9 +422,9 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       methods: [methodA, methodA]
     )
 
-    let codeGenerationRequest = self.makeCodeGenerationRequest(services: [service])
+    let codeGenerationRequest = makeCodeGenerationRequest(services: [service])
     let translator = TypealiasTranslator()
-    self.assertThrowsError(
+    XCTAssertThrowsError(
       ofType: CodeGenError.self,
       try translator.translate(from: codeGenerationRequest)
     ) {
@@ -455,9 +455,9 @@ final class SnippetBasedTranslatorTests: XCTestCase {
       namespace: "SameName",
       methods: []
     )
-    let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceB])
+    let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB])
     let translator = TypealiasTranslator()
-    self.assertThrowsError(
+    XCTAssertThrowsError(
       ofType: CodeGenError.self,
       try translator.translate(from: codeGenerationRequest)
     ) {
@@ -476,7 +476,7 @@ final class SnippetBasedTranslatorTests: XCTestCase {
   }
 }
 
-extension SnippetBasedTranslatorTests {
+extension TypealiasTranslatorSnippetBasedTests {
   private func assertTypealiasTranslation(
     codeGenerationRequest: CodeGenerationRequest,
     expectedSwift: String
@@ -488,36 +488,6 @@ extension SnippetBasedTranslatorTests {
     let contents = renderer.renderedContents()
     try XCTAssertEqualWithDiff(contents, expectedSwift)
   }
-
-  private func assertThrowsError<T, E: Error>(
-    ofType: E.Type,
-    _ expression: @autoclosure () throws -> T,
-    _ errorHandler: (E) -> Void
-  ) {
-    XCTAssertThrowsError(try expression()) { error in
-      guard let error = error as? E else {
-        return XCTFail("Error had unexpected type '\(type(of: error))'")
-      }
-      errorHandler(error)
-    }
-  }
-}
-
-extension SnippetBasedTranslatorTests {
-  private func makeCodeGenerationRequest(services: [ServiceDescriptor]) -> CodeGenerationRequest {
-    return CodeGenerationRequest(
-      fileName: "test.grpc",
-      leadingTrivia: "Some really exciting license header 2023.",
-      dependencies: [],
-      services: services,
-      lookupSerializer: {
-        "ProtobufSerializer<\($0)>()"
-      },
-      lookupDeserializer: {
-        "ProtobufDeserializer<\($0)>()"
-      }
-    )
-  }
 }
 
 #endif  // os(macOS) || os(Linux)