Bläddra i källkod

Use nested protocols (#2132)

Motivation:

The minimum Swift version we require supports protocols being declared
within another type. This means that all protocols and types we generate
for a service can be declared within the enum namespace for that service
and we can remove the typealiases.

Modifications:

- Declare generated protocols within their enum namespace
- Inline generated service descriptors

Result:

Simpler generated code
George Barnett 1 år sedan
förälder
incheckning
12a736686d

+ 40 - 65
Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift

@@ -43,12 +43,12 @@ extension TypealiasDescription {
 extension VariableDescription {
   /// ```
   /// static let descriptor = GRPCCore.MethodDescriptor(
-  ///   service: <serviceNamespace>.descriptor.fullyQualifiedService,
+  ///   service: GRPCCore.ServiceDescriptor(fullyQualifiedServiceName: "<literalFullyQualifiedService>"),
   ///   method: "<literalMethodName>"
   /// ```
   package static func methodDescriptor(
     accessModifier: AccessModifier? = nil,
-    serviceNamespace: String,
+    literalFullyQualifiedService: String,
     literalMethodName: String
   ) -> Self {
     return VariableDescription(
@@ -62,9 +62,11 @@ extension VariableDescription {
           arguments: [
             FunctionArgumentDescription(
               label: "service",
-              expression: .identifierType(
-                .member([serviceNamespace, "descriptor"])
-              ).dot("fullyQualifiedService")
+              expression: .functionCall(
+                .serviceDescriptor(
+                  literalFullyQualifiedService: literalFullyQualifiedService
+                )
+              )
             ),
             FunctionArgumentDescription(
               label: "method",
@@ -77,18 +79,34 @@ extension VariableDescription {
   }
 
   /// ```
-  /// static let descriptor = GRPCCore.ServiceDescriptor.<namespacedProperty>
+  /// static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: <LiteralFullyQualifiedService>)
   /// ```
   package static func serviceDescriptor(
     accessModifier: AccessModifier? = nil,
-    namespacedProperty: String
+    literalFullyQualifiedService name: String
   ) -> Self {
     return VariableDescription(
       accessModifier: accessModifier,
       isStatic: true,
       kind: .let,
       left: .identifierPattern("descriptor"),
-      right: .identifier(.type(.serviceDescriptor)).dot(namespacedProperty)
+      right: .functionCall(.serviceDescriptor(literalFullyQualifiedService: name))
+    )
+  }
+}
+
+extension FunctionCallDescription {
+  package static func serviceDescriptor(
+    literalFullyQualifiedService: String
+  ) -> Self {
+    FunctionCallDescription(
+      calledExpression: .identifier(.type(.serviceDescriptor)),
+      arguments: [
+        FunctionArgumentDescription(
+          label: "fullyQualifiedService",
+          expression: .literal(literalFullyQualifiedService)
+        )
+      ]
     )
   }
 }
@@ -97,16 +115,14 @@ extension ExtensionDescription {
   /// ```
   /// extension GRPCCore.ServiceDescriptor {
   ///   static let <PropertyName> = Self(
-  ///     package: "<LiteralNamespaceName>",
-  ///     service: "<LiteralServiceName>"
+  ///     fullyQualifiedService: <LiteralFullyQualifiedService>
   ///   )
   /// }
   /// ```
   package static func serviceDescriptor(
     accessModifier: AccessModifier? = nil,
     propertyName: String,
-    literalNamespace: String,
-    literalService: String
+    literalFullyQualifiedService: String
   ) -> ExtensionDescription {
     return ExtensionDescription(
       onType: "GRPCCore.ServiceDescriptor",
@@ -117,17 +133,7 @@ extension ExtensionDescription {
           kind: .let,
           left: .identifier(.pattern(propertyName)),
           right: .functionCall(
-            calledExpression: .identifierType(.member("Self")),
-            arguments: [
-              FunctionArgumentDescription(
-                label: "package",
-                expression: .literal(literalNamespace)
-              ),
-              FunctionArgumentDescription(
-                label: "service",
-                expression: .literal(literalService)
-              ),
-            ]
+            .serviceDescriptor(literalFullyQualifiedService: literalFullyQualifiedService)
           )
         )
       ]
@@ -169,7 +175,7 @@ extension EnumDescription {
     accessModifier: AccessModifier? = nil,
     name: String,
     literalMethod: String,
-    serviceNamespace: String,
+    literalFullyQualifiedService: String,
     inputType: String,
     outputType: String
   ) -> Self {
@@ -182,7 +188,7 @@ extension EnumDescription {
         .variable(
           .methodDescriptor(
             accessModifier: accessModifier,
-            serviceNamespace: serviceNamespace,
+            literalFullyQualifiedService: literalFullyQualifiedService,
             literalMethodName: literalMethod
           )
         ),
@@ -209,7 +215,7 @@ extension EnumDescription {
   /// ```
   package static func methodsNamespace(
     accessModifier: AccessModifier? = nil,
-    serviceNamespace: String,
+    literalFullyQualifiedService: String,
     methods: [MethodDescriptor]
   ) -> EnumDescription {
     var description = EnumDescription(accessModifier: accessModifier, name: "Method")
@@ -221,7 +227,7 @@ extension EnumDescription {
           accessModifier: accessModifier,
           name: method.name.base,
           literalMethod: method.name.base,
-          serviceNamespace: serviceNamespace,
+          literalFullyQualifiedService: literalFullyQualifiedService,
           inputType: method.inputType,
           outputType: method.outputType
         )
@@ -245,57 +251,31 @@ extension EnumDescription {
   ///   enum Method {
   ///     ...
   ///   }
-  ///   @available(...)
-  ///   typealias StreamingServiceProtocol = ...
-  ///   @available(...)
-  ///   typealias ServiceProtocol = ...
-  ///   ...
   /// }
   /// ```
   package static func serviceNamespace(
     accessModifier: AccessModifier? = nil,
     name: String,
-    serviceDescriptorProperty: String,
-    client: Bool,
-    server: Bool,
+    literalFullyQualifiedService: String,
     methods: [MethodDescriptor]
   ) -> EnumDescription {
     var description = EnumDescription(accessModifier: accessModifier, name: name)
 
-    // static let descriptor = GRPCCore.ServiceDescriptor.<namespacedServicePropertyName>
+    // static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "...")
     let descriptor = VariableDescription.serviceDescriptor(
       accessModifier: accessModifier,
-      namespacedProperty: serviceDescriptorProperty
+      literalFullyQualifiedService: literalFullyQualifiedService
     )
     description.members.append(.variable(descriptor))
 
     // enum Method { ... }
     let methodsNamespace: EnumDescription = .methodsNamespace(
       accessModifier: accessModifier,
-      serviceNamespace: name,
+      literalFullyQualifiedService: literalFullyQualifiedService,
       methods: methods
     )
     description.members.append(.enum(methodsNamespace))
 
-    // Typealiases for the various protocols.
-    var typealiasNames: [String] = []
-    if server {
-      typealiasNames.append("StreamingServiceProtocol")
-      typealiasNames.append("ServiceProtocol")
-    }
-    if client {
-      typealiasNames.append("ClientProtocol")
-      typealiasNames.append("Client")
-    }
-    let typealiases: [Declaration] = typealiasNames.map { alias in
-      .typealias(
-        accessModifier: accessModifier,
-        name: alias,
-        existingType: .member(name + "_" + alias)
-      )
-    }
-    description.members.append(contentsOf: typealiases)
-
     return description
   }
 }
@@ -312,18 +292,14 @@ extension [CodeBlock] {
   /// ```
   package static func serviceMetadata(
     accessModifier: AccessModifier? = nil,
-    service: ServiceDescriptor,
-    client: Bool,
-    server: Bool
+    service: ServiceDescriptor
   ) -> Self {
     var blocks: [CodeBlock] = []
 
     let serviceNamespace: EnumDescription = .serviceNamespace(
       accessModifier: accessModifier,
       name: service.namespacedGeneratedName,
-      serviceDescriptorProperty: service.namespacedServicePropertyName,
-      client: client,
-      server: server,
+      literalFullyQualifiedService: service.fullyQualifiedName,
       methods: service.methods
     )
     blocks.append(CodeBlock(item: .declaration(.enum(serviceNamespace))))
@@ -331,8 +307,7 @@ extension [CodeBlock] {
     let descriptorExtension: ExtensionDescription = .serviceDescriptor(
       accessModifier: accessModifier,
       propertyName: service.namespacedServicePropertyName,
-      literalNamespace: service.namespace.base,
-      literalService: service.name.base
+      literalFullyQualifiedService: service.fullyQualifiedName
     )
     blocks.append(CodeBlock(item: .declaration(.extension(descriptorExtension))))
 

+ 31 - 29
Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift

@@ -86,25 +86,41 @@ struct ClientCodeTranslator {
   ) -> [CodeBlock] {
     var blocks = [CodeBlock]()
 
-    let protocolName = "\(service.namespacedGeneratedName)_ClientProtocol"
-    let protocolTypealias = "\(service.namespacedGeneratedName).ClientProtocol"
-    let structName = "\(service.namespacedGeneratedName)_Client"
+    let `extension` = ExtensionDescription(
+      onType: service.namespacedGeneratedName,
+      declarations: [
+        // protocol ClientProtocol { ... }
+        .commentable(
+          .preFormatted(service.documentation),
+          .protocol(
+            .clientProtocol(
+              accessLevel: accessModifier,
+              name: "ClientProtocol",
+              methods: service.methods
+            )
+          )
+        ),
 
-    let clientProtocol: ProtocolDescription = .clientProtocol(
-      accessLevel: accessModifier,
-      name: protocolName,
-      methods: service.methods
-    )
-    blocks.append(
-      CodeBlock(
-        comment: .preFormatted(service.documentation),
-        item: .declaration(.protocol(clientProtocol))
-      )
+        // struct Client: ClientProtocol { ... }
+        .commentable(
+          .preFormatted(service.documentation),
+          .struct(
+            .client(
+              accessLevel: accessModifier,
+              name: "Client",
+              serviceEnum: service.namespacedGeneratedName,
+              clientProtocol: "ClientProtocol",
+              methods: service.methods
+            )
+          )
+        ),
+      ]
     )
+    blocks.append(.declaration(.extension(`extension`)))
 
     let extensionWithDefaults: ExtensionDescription = .clientMethodSignatureWithDefaults(
       accessLevel: accessModifier,
-      name: protocolTypealias,
+      name: "\(service.namespacedGeneratedName).ClientProtocol",
       methods: service.methods,
       serializer: serializer,
       deserializer: deserializer
@@ -115,27 +131,13 @@ struct ClientCodeTranslator {
 
     let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods(
       accessLevel: accessModifier,
-      on: protocolTypealias,
+      on: "\(service.namespacedGeneratedName).ClientProtocol",
       methods: service.methods
     )
     blocks.append(
       CodeBlock(item: .declaration(.extension(extensionWithExplodedAPI)))
     )
 
-    let clientStruct: StructDescription = .client(
-      accessLevel: accessModifier,
-      name: structName,
-      serviceEnum: service.namespacedGeneratedName,
-      clientProtocol: protocolTypealias,
-      methods: service.methods
-    )
-    blocks.append(
-      CodeBlock(
-        comment: .preFormatted(service.documentation),
-        item: .declaration(.struct(clientStruct))
-      )
-    )
-
     return blocks
   }
 }

+ 1 - 3
Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift

@@ -40,9 +40,7 @@ struct IDLToStructuredSwiftTranslator: Translator {
 
       let metadata = metadataTranslator.translate(
         accessModifier: accessModifier,
-        service: service,
-        client: client,
-        server: server
+        service: service
       )
       codeBlocks.append(contentsOf: metadata)
 

+ 2 - 9
Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift

@@ -19,15 +19,8 @@ struct MetadataTranslator {
 
   func translate(
     accessModifier: AccessModifier,
-    service: ServiceDescriptor,
-    client: Bool,
-    server: Bool
+    service: ServiceDescriptor
   ) -> [CodeBlock] {
-    .serviceMetadata(
-      accessModifier: accessModifier,
-      service: service,
-      client: client,
-      server: server
-    )
+    .serviceMetadata(accessModifier: accessModifier, service: service)
   }
 }

+ 32 - 57
Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift

@@ -68,36 +68,41 @@ struct ServerCodeTranslator {
   ) -> [CodeBlock] {
     var blocks = [CodeBlock]()
 
-    let serviceProtocolName = self.protocolName(service: service, streaming: false)
-    let serviceTypealiasName = self.protocolName(
-      service: service,
-      streaming: false,
-      joinedUsing: "."
-    )
-    let streamingServiceProtocolName = self.protocolName(service: service, streaming: true)
-    let streamingServiceTypealiasName = self.protocolName(
-      service: service,
-      streaming: true,
-      joinedUsing: "."
-    )
+    let `extension` = ExtensionDescription(
+      onType: service.namespacedGeneratedName,
+      declarations: [
+        // protocol StreamingServiceProtocol { ... }
+        .commentable(
+          .preFormatted(service.documentation),
+          .protocol(
+            .streamingService(
+              accessLevel: accessModifier,
+              name: "StreamingServiceProtocol",
+              methods: service.methods
+            )
+          )
+        ),
 
-    // protocol <Service>_StreamingServiceProtocol { ... }
-    let streamingServiceProtocol: ProtocolDescription = .streamingService(
-      accessLevel: accessModifier,
-      name: streamingServiceProtocolName,
-      methods: service.methods
-    )
-    blocks.append(
-      CodeBlock(
-        comment: .preFormatted(service.documentation),
-        item: .declaration(.protocol(streamingServiceProtocol))
-      )
+        // protocol ServiceProtocol { ... }
+        .commentable(
+          .preFormatted(service.documentation),
+          .protocol(
+            .service(
+              accessLevel: accessModifier,
+              name: "ServiceProtocol",
+              streamingProtocol: "\(service.namespacedGeneratedName).StreamingServiceProtocol",
+              methods: service.methods
+            )
+          )
+        ),
+      ]
     )
+    blocks.append(.declaration(.extension(`extension`)))
 
-    // extension <Service>_StreamingServiceProtocol> { ... }
+    // extension <Service>.StreamingServiceProtocol> { ... }
     let registerExtension: ExtensionDescription = .registrableRPCServiceDefaultImplementation(
       accessLevel: accessModifier,
-      on: streamingServiceTypealiasName,
+      on: "\(service.namespacedGeneratedName).StreamingServiceProtocol",
       serviceNamespace: service.namespacedGeneratedName,
       methods: service.methods,
       serializer: serializer,
@@ -110,45 +115,15 @@ struct ServerCodeTranslator {
       )
     )
 
-    // protocol <Service>_ServiceProtocol { ... }
-    let serviceProtocol: ProtocolDescription = .service(
-      accessLevel: accessModifier,
-      name: serviceProtocolName,
-      streamingProtocol: streamingServiceTypealiasName,
-      methods: service.methods
-    )
-    blocks.append(
-      CodeBlock(
-        comment: .preFormatted(service.documentation),
-        item: .declaration(.protocol(serviceProtocol))
-      )
-    )
-
     // extension <Service>_ServiceProtocol { ... }
     let streamingServiceDefaultImplExtension: ExtensionDescription =
       .streamingServiceProtocolDefaultImplementation(
         accessModifier: accessModifier,
-        on: serviceTypealiasName,
+        on: "\(service.namespacedGeneratedName).ServiceProtocol",
         methods: service.methods
       )
-    blocks.append(
-      CodeBlock(
-        comment: .doc("Partial conformance to `\(streamingServiceProtocolName)`."),
-        item: .declaration(.extension(streamingServiceDefaultImplExtension))
-      )
-    )
+    blocks.append(.declaration(.extension(streamingServiceDefaultImplExtension)))
 
     return blocks
   }
-
-  private func protocolName(
-    service: ServiceDescriptor,
-    streaming: Bool,
-    joinedUsing join: String = "_"
-  ) -> String {
-    if streaming {
-      return "\(service.namespacedGeneratedName)\(join)StreamingServiceProtocol"
-    }
-    return "\(service.namespacedGeneratedName)\(join)ServiceProtocol"
-  }
 }

+ 24 - 58
Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift

@@ -42,13 +42,13 @@ extension StructuedSwiftTests {
     func staticMethodDescriptorProperty(access: AccessModifier) {
       let decl: VariableDescription = .methodDescriptor(
         accessModifier: access,
-        serviceNamespace: "FooService",
+        literalFullyQualifiedService: "foo.Foo",
         literalMethodName: "Bar"
       )
 
       let expected = """
         \(access) static let descriptor = GRPCCore.MethodDescriptor(
-          service: FooService.descriptor.fullyQualifiedService,
+          service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "foo.Foo"),
           method: "Bar"
         )
         """
@@ -56,16 +56,18 @@ extension StructuedSwiftTests {
     }
 
     @Test(
-      "static let descriptor = GRPCCore.ServiceDescriptor.<Name>",
+      "static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService:)",
       arguments: AccessModifier.allCases
     )
     func staticServiceDescriptorProperty(access: AccessModifier) {
       let decl: VariableDescription = .serviceDescriptor(
         accessModifier: access,
-        namespacedProperty: "foo"
+        literalFullyQualifiedService: "foo.Bar"
       )
 
-      let expected = "\(access) static let descriptor = GRPCCore.ServiceDescriptor.foo"
+      let expected = """
+        \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "foo.Bar")
+        """
       #expect(render(.variable(decl)) == expected)
     }
 
@@ -74,16 +76,12 @@ extension StructuedSwiftTests {
       let decl: ExtensionDescription = .serviceDescriptor(
         accessModifier: access,
         propertyName: "foo",
-        literalNamespace: "echo",
-        literalService: "EchoService"
+        literalFullyQualifiedService: "echo.EchoService"
       )
 
       let expected = """
         extension GRPCCore.ServiceDescriptor {
-          \(access) static let foo = Self(
-            package: "echo",
-            service: "EchoService"
-          )
+          \(access) static let foo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.EchoService")
         }
         """
       #expect(render(.extension(decl)) == expected)
@@ -115,7 +113,7 @@ extension StructuedSwiftTests {
         accessModifier: access,
         name: "Foo",
         literalMethod: "Foo",
-        serviceNamespace: "Bar_Baz",
+        literalFullyQualifiedService: "bar.Bar",
         inputType: "FooInput",
         outputType: "FooOutput"
       )
@@ -125,7 +123,7 @@ extension StructuedSwiftTests {
           \(access) typealias Input = FooInput
           \(access) typealias Output = FooOutput
           \(access) static let descriptor = GRPCCore.MethodDescriptor(
-            service: Bar_Baz.descriptor.fullyQualifiedService,
+            service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"),
             method: "Foo"
           )
         }
@@ -137,7 +135,7 @@ extension StructuedSwiftTests {
     func methodsNamespaceEnum(access: AccessModifier) {
       let decl: EnumDescription = .methodsNamespace(
         accessModifier: access,
-        serviceNamespace: "Bar_Baz",
+        literalFullyQualifiedService: "bar.Bar",
         methods: [
           .init(
             documentation: "",
@@ -156,7 +154,7 @@ extension StructuedSwiftTests {
             \(access) typealias Input = FooInput
             \(access) typealias Output = FooOutput
             \(access) static let descriptor = GRPCCore.MethodDescriptor(
-              service: Bar_Baz.descriptor.fullyQualifiedService,
+              service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"),
               method: "Foo"
             )
           }
@@ -172,7 +170,7 @@ extension StructuedSwiftTests {
     func methodsNamespaceEnumNoMethods(access: AccessModifier) {
       let decl: EnumDescription = .methodsNamespace(
         accessModifier: access,
-        serviceNamespace: "Bar_Baz",
+        literalFullyQualifiedService: "bar.Bar",
         methods: []
       )
 
@@ -189,9 +187,7 @@ extension StructuedSwiftTests {
       let decl: EnumDescription = .serviceNamespace(
         accessModifier: access,
         name: "Foo",
-        serviceDescriptorProperty: "foo",
-        client: false,
-        server: false,
+        literalFullyQualifiedService: "Foo",
         methods: [
           .init(
             documentation: "",
@@ -206,13 +202,13 @@ extension StructuedSwiftTests {
 
       let expected = """
         \(access) enum Foo {
-          \(access) static let descriptor = GRPCCore.ServiceDescriptor.foo
+          \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo")
           \(access) enum Method {
             \(access) enum Bar {
               \(access) typealias Input = BarInput
               \(access) typealias Output = BarOutput
               \(access) static let descriptor = GRPCCore.MethodDescriptor(
-                service: Foo.descriptor.fullyQualifiedService,
+                service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo"),
                 method: "Bar"
               )
             }
@@ -225,53 +221,23 @@ extension StructuedSwiftTests {
       #expect(render(.enum(decl)) == expected)
     }
 
-    @Test(
-      "enum <Service> { ... } (no methods)",
-      arguments: AccessModifier.allCases,
-      [(true, true), (false, false), (true, false), (false, true)]
-    )
-    func serviceNamespaceEnumNoMethods(access: AccessModifier, config: (client: Bool, server: Bool))
-    {
+    @Test("enum <Service> { ... } (no methods)", arguments: AccessModifier.allCases)
+    func serviceNamespaceEnumNoMethods(access: AccessModifier) {
       let decl: EnumDescription = .serviceNamespace(
         accessModifier: access,
         name: "Foo",
-        serviceDescriptorProperty: "foo",
-        client: config.client,
-        server: config.server,
+        literalFullyQualifiedService: "Foo",
         methods: []
       )
 
-      var expected = """
+      let expected = """
         \(access) enum Foo {
-          \(access) static let descriptor = GRPCCore.ServiceDescriptor.foo
+          \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo")
           \(access) enum Method {
             \(access) static let descriptors: [GRPCCore.MethodDescriptor] = []
-          }\n
-        """
-
-      if config.server {
-        expected += """
-            \(access) typealias StreamingServiceProtocol = Foo_StreamingServiceProtocol
-            \(access) typealias ServiceProtocol = Foo_ServiceProtocol
-          """
-      }
-
-      if config.client {
-        if config.server {
-          expected += "\n"
+          }
         }
-
-        expected += """
-            \(access) typealias ClientProtocol = Foo_ClientProtocol
-            \(access) typealias Client = Foo_Client
-          """
-      }
-
-      if config.client || config.server {
-        expected += "\n}"
-      } else {
-        expected += "}"
-      }
+        """
 
       #expect(render(.enum(decl)) == expected)
     }

+ 41 - 38
Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift

@@ -39,16 +39,47 @@ struct ClientCodeTranslatorSnippetBasedTests {
     )
 
     let expectedSwift = """
-      /// Documentation for ServiceA
-      public protocol NamespaceA_ServiceA_ClientProtocol: Sendable {
-          /// Documentation for MethodA
-          func methodA<Result>(
-              request: GRPCCore.ClientRequest<NamespaceA_ServiceARequest>,
-              serializer: some GRPCCore.MessageSerializer<NamespaceA_ServiceARequest>,
-              deserializer: some GRPCCore.MessageDeserializer<NamespaceA_ServiceAResponse>,
-              options: GRPCCore.CallOptions,
-              onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<NamespaceA_ServiceAResponse>) async throws -> Result
-          ) async throws -> Result where Result: Sendable
+      extension NamespaceA_ServiceA {
+          /// Documentation for ServiceA
+          public protocol ClientProtocol: Sendable {
+              /// Documentation for MethodA
+              func methodA<Result>(
+                  request: GRPCCore.ClientRequest<NamespaceA_ServiceARequest>,
+                  serializer: some GRPCCore.MessageSerializer<NamespaceA_ServiceARequest>,
+                  deserializer: some GRPCCore.MessageDeserializer<NamespaceA_ServiceAResponse>,
+                  options: GRPCCore.CallOptions,
+                  onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<NamespaceA_ServiceAResponse>) async throws -> Result
+              ) async throws -> Result where Result: Sendable
+          }
+
+          /// Documentation for ServiceA
+          public struct Client: ClientProtocol {
+              private let client: GRPCCore.GRPCClient
+
+              public init(wrapping client: GRPCCore.GRPCClient) {
+                  self.client = client
+              }
+
+              /// Documentation for MethodA
+              public func methodA<Result>(
+                  request: GRPCCore.ClientRequest<NamespaceA_ServiceARequest>,
+                  serializer: some GRPCCore.MessageSerializer<NamespaceA_ServiceARequest>,
+                  deserializer: some GRPCCore.MessageDeserializer<NamespaceA_ServiceAResponse>,
+                  options: GRPCCore.CallOptions = .defaults,
+                  onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<NamespaceA_ServiceAResponse>) async throws -> Result = { response in
+                      try response.message
+                  }
+              ) async throws -> Result where Result: Sendable {
+                  try await self.client.unary(
+                      request: request,
+                      descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor,
+                      serializer: serializer,
+                      deserializer: deserializer,
+                      options: options,
+                      onResponse: handleResponse
+                  )
+              }
+          }
       }
       extension NamespaceA_ServiceA.ClientProtocol {
           public func methodA<Result>(
@@ -88,34 +119,6 @@ struct ClientCodeTranslatorSnippetBasedTests {
               )
           }
       }
-      /// Documentation for ServiceA
-      public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol {
-          private let client: GRPCCore.GRPCClient
-
-          public init(wrapping client: GRPCCore.GRPCClient) {
-              self.client = client
-          }
-
-          /// Documentation for MethodA
-          public func methodA<Result>(
-              request: GRPCCore.ClientRequest<NamespaceA_ServiceARequest>,
-              serializer: some GRPCCore.MessageSerializer<NamespaceA_ServiceARequest>,
-              deserializer: some GRPCCore.MessageDeserializer<NamespaceA_ServiceAResponse>,
-              options: GRPCCore.CallOptions = .defaults,
-              onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<NamespaceA_ServiceAResponse>) async throws -> Result = { response in
-                  try response.message
-              }
-          ) async throws -> Result where Result: Sendable {
-              try await self.client.unary(
-                  request: request,
-                  descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor,
-                  serializer: serializer,
-                  deserializer: deserializer,
-                  options: options,
-                  onResponse: handleResponse
-              )
-          }
-      }
       """
 
     let rendered = self.render(accessLevel: .public, service: service)

+ 9 - 13
Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift

@@ -215,35 +215,31 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
       // MARK: - namespaceA.ServiceA
 
       public enum NamespaceA_ServiceA {
-          public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA
+          public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA")
           public enum Method {
               public static let descriptors: [GRPCCore.MethodDescriptor] = []
           }
-          public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol
-          public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol
       }
 
       extension GRPCCore.ServiceDescriptor {
-          public static let namespaceA_ServiceA = Self(
-              package: "namespaceA",
-              service: "ServiceA"
-          )
+          public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA")
       }
 
       // MARK: namespaceA.ServiceA (server)
 
-      /// Documentation for AService
-      public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {}
+      extension NamespaceA_ServiceA {
+          /// Documentation for AService
+          public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService {}
+
+          /// Documentation for AService
+          public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {}
+      }
 
       /// Conformance to `GRPCCore.RegistrableRPCService`.
       extension NamespaceA_ServiceA.StreamingServiceProtocol {
           public func registerMethods(with router: inout GRPCCore.RPCRouter) {}
       }
 
-      /// Documentation for AService
-      public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {}
-
-      /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`.
       extension NamespaceA_ServiceA.ServiceProtocol {
       }
       """

+ 18 - 16
Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift

@@ -47,13 +47,24 @@ final class ServerCodeTranslatorSnippetBasedTests {
     )
 
     let expectedSwift = """
-      /// Documentation for ServiceA
-      public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {
-          /// Documentation for unaryMethod
-          func unary(
-              request: GRPCCore.StreamingServerRequest<NamespaceA_ServiceARequest>,
-              context: GRPCCore.ServerContext
-          ) async throws -> GRPCCore.StreamingServerResponse<NamespaceA_ServiceAResponse>
+      extension NamespaceA_ServiceA {
+          /// Documentation for ServiceA
+          public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+              /// Documentation for unaryMethod
+              func unary(
+                  request: GRPCCore.StreamingServerRequest<NamespaceA_ServiceARequest>,
+                  context: GRPCCore.ServerContext
+              ) async throws -> GRPCCore.StreamingServerResponse<NamespaceA_ServiceAResponse>
+          }
+
+          /// Documentation for ServiceA
+          public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {
+              /// Documentation for unaryMethod
+              func unary(
+                  request: GRPCCore.ServerRequest<NamespaceA_ServiceARequest>,
+                  context: GRPCCore.ServerContext
+              ) async throws -> GRPCCore.ServerResponse<NamespaceA_ServiceAResponse>
+          }
       }
       /// Conformance to `GRPCCore.RegistrableRPCService`.
       extension NamespaceA_ServiceA.StreamingServiceProtocol {
@@ -71,15 +82,6 @@ final class ServerCodeTranslatorSnippetBasedTests {
               )
           }
       }
-      /// Documentation for ServiceA
-      public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {
-          /// Documentation for unaryMethod
-          func unary(
-              request: GRPCCore.ServerRequest<NamespaceA_ServiceARequest>,
-              context: GRPCCore.ServerContext
-          ) async throws -> GRPCCore.ServerResponse<NamespaceA_ServiceAResponse>
-      }
-      /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`.
       extension NamespaceA_ServiceA.ServiceProtocol {
           public func unary(
               request: GRPCCore.StreamingServerRequest<NamespaceA_ServiceARequest>,

+ 20 - 288
Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift

@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-#if os(macOS) || os(Linux)  // swift-format doesn't like canImport(Foundation.Process)
-
-import XCTest
+import Testing
 
 @testable import GRPCCodeGen
 
-final class TypealiasTranslatorSnippetBasedTests: XCTestCase {
+@Suite
+struct TypealiasTranslatorSnippetBasedTests {
+  @Test
   func testTypealiasTranslator() throws {
     let method = MethodDescriptor(
       documentation: "Documentation for MethodA",
@@ -40,222 +40,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase {
       ),
       methods: [method]
     )
-    let expectedSwift =
-      """
+
+    let expectedSwift = """
       public enum NamespaceA_ServiceA {
-          public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA
+          public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA")
           public enum Method {
               public enum MethodA {
                   public typealias Input = NamespaceA_ServiceARequest
                   public typealias Output = NamespaceA_ServiceAResponse
                   public static let descriptor = GRPCCore.MethodDescriptor(
-                      service: NamespaceA_ServiceA.descriptor.fullyQualifiedService,
-                      method: "MethodA"
-                  )
-              }
-              public static let descriptors: [GRPCCore.MethodDescriptor] = [
-                  MethodA.descriptor
-              ]
-          }
-          public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol
-          public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol
-          public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol
-          public typealias Client = NamespaceA_ServiceA_Client
-      }
-      extension GRPCCore.ServiceDescriptor {
-          public static let namespaceA_ServiceA = Self(
-              package: "namespaceA",
-              service: "ServiceA"
-          )
-      }
-      """
-
-    try self.assertTypealiasTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
-      expectedSwift: expectedSwift,
-      client: true,
-      server: true,
-      accessLevel: .public
-    )
-  }
-
-  func testTypealiasTranslatorNoMethodsServiceClientAndServer() throws {
-    let service = ServiceDescriptor(
-      documentation: "Documentation for ServiceA",
-      name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"),
-      namespace: Name(
-        base: "namespaceA",
-        generatedUpperCase: "NamespaceA",
-        generatedLowerCase: "namespaceA"
-      ),
-      methods: []
-    )
-    let expectedSwift =
-      """
-      public enum NamespaceA_ServiceA {
-          public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA
-          public enum Method {
-              public static let descriptors: [GRPCCore.MethodDescriptor] = []
-          }
-          public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol
-          public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol
-          public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol
-          public typealias Client = NamespaceA_ServiceA_Client
-      }
-      extension GRPCCore.ServiceDescriptor {
-          public static let namespaceA_ServiceA = Self(
-              package: "namespaceA",
-              service: "ServiceA"
-          )
-      }
-      """
-
-    try self.assertTypealiasTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
-      expectedSwift: expectedSwift,
-      client: true,
-      server: true,
-      accessLevel: .public
-    )
-  }
-
-  func testTypealiasTranslatorServer() throws {
-    let service = ServiceDescriptor(
-      documentation: "Documentation for ServiceA",
-      name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"),
-      namespace: Name(
-        base: "namespaceA",
-        generatedUpperCase: "NamespaceA",
-        generatedLowerCase: "namespaceA"
-      ),
-      methods: []
-    )
-    let expectedSwift =
-      """
-      public enum NamespaceA_ServiceA {
-          public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA
-          public enum Method {
-              public static let descriptors: [GRPCCore.MethodDescriptor] = []
-          }
-          public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol
-          public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol
-      }
-      extension GRPCCore.ServiceDescriptor {
-          public static let namespaceA_ServiceA = Self(
-              package: "namespaceA",
-              service: "ServiceA"
-          )
-      }
-      """
-
-    try self.assertTypealiasTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
-      expectedSwift: expectedSwift,
-      client: false,
-      server: true,
-      accessLevel: .public
-    )
-  }
-
-  func testTypealiasTranslatorClient() throws {
-    let service = ServiceDescriptor(
-      documentation: "Documentation for ServiceA",
-      name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"),
-      namespace: Name(
-        base: "namespaceA",
-        generatedUpperCase: "NamespaceA",
-        generatedLowerCase: "namespaceA"
-      ),
-      methods: []
-    )
-    let expectedSwift =
-      """
-      public enum NamespaceA_ServiceA {
-          public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA
-          public enum Method {
-              public static let descriptors: [GRPCCore.MethodDescriptor] = []
-          }
-          public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol
-          public typealias Client = NamespaceA_ServiceA_Client
-      }
-      extension GRPCCore.ServiceDescriptor {
-          public static let namespaceA_ServiceA = Self(
-              package: "namespaceA",
-              service: "ServiceA"
-          )
-      }
-      """
-
-    try self.assertTypealiasTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
-      expectedSwift: expectedSwift,
-      client: true,
-      server: false,
-      accessLevel: .public
-    )
-  }
-
-  func testTypealiasTranslatorNoClientNoServer() throws {
-    let service = ServiceDescriptor(
-      documentation: "Documentation for ServiceA",
-      name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"),
-      namespace: Name(
-        base: "namespaceA",
-        generatedUpperCase: "NamespaceA",
-        generatedLowerCase: "namespaceA"
-      ),
-      methods: []
-    )
-    let expectedSwift =
-      """
-      public enum NamespaceA_ServiceA {
-          public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA
-          public enum Method {
-              public static let descriptors: [GRPCCore.MethodDescriptor] = []
-          }
-      }
-      extension GRPCCore.ServiceDescriptor {
-          public static let namespaceA_ServiceA = Self(
-              package: "namespaceA",
-              service: "ServiceA"
-          )
-      }
-      """
-
-    try self.assertTypealiasTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
-      expectedSwift: expectedSwift,
-      client: false,
-      server: false,
-      accessLevel: .public
-    )
-  }
-
-  func testTypealiasTranslatorEmptyNamespace() throws {
-    let method = MethodDescriptor(
-      documentation: "Documentation for MethodA",
-      name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"),
-      isInputStreaming: false,
-      isOutputStreaming: false,
-      inputType: "ServiceARequest",
-      outputType: "ServiceAResponse"
-    )
-    let service = ServiceDescriptor(
-      documentation: "Documentation for ServiceA",
-      name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"),
-      namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""),
-      methods: [method]
-    )
-    let expectedSwift =
-      """
-      public enum ServiceA {
-          public static let descriptor = GRPCCore.ServiceDescriptor.ServiceA
-          public enum Method {
-              public enum MethodA {
-                  public typealias Input = ServiceARequest
-                  public typealias Output = ServiceAResponse
-                  public static let descriptor = GRPCCore.MethodDescriptor(
-                      service: ServiceA.descriptor.fullyQualifiedService,
+                      service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA"),
                       method: "MethodA"
                   )
               }
@@ -263,91 +57,29 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase {
                   MethodA.descriptor
               ]
           }
-          public typealias StreamingServiceProtocol = ServiceA_StreamingServiceProtocol
-          public typealias ServiceProtocol = ServiceA_ServiceProtocol
-          public typealias ClientProtocol = ServiceA_ClientProtocol
-          public typealias Client = ServiceA_Client
       }
       extension GRPCCore.ServiceDescriptor {
-          public static let ServiceA = Self(
-              package: "",
-              service: "ServiceA"
-          )
+          public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA")
       }
       """
 
-    try self.assertTypealiasTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
-      expectedSwift: expectedSwift,
-      client: true,
-      server: true,
-      accessLevel: .public
-    )
-  }
-
-  func testTypealiasTranslatorNoMethodsService() throws {
-    let service = ServiceDescriptor(
-      documentation: "Documentation for ServiceA",
-      name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"),
-      namespace: Name(
-        base: "namespaceA",
-        generatedUpperCase: "NamespaceA",
-        generatedLowerCase: "namespaceA"
-      ),
-      methods: []
-    )
-    let expectedSwift =
-      """
-      package enum NamespaceA_ServiceA {
-          package static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA
-          package enum Method {
-              package static let descriptors: [GRPCCore.MethodDescriptor] = []
-          }
-          package typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol
-          package typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol
-          package typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol
-          package typealias Client = NamespaceA_ServiceA_Client
-      }
-      extension GRPCCore.ServiceDescriptor {
-          package static let namespaceA_ServiceA = Self(
-              package: "namespaceA",
-              service: "ServiceA"
-          )
-      }
-      """
-
-    try self.assertTypealiasTranslation(
-      codeGenerationRequest: makeCodeGenerationRequest(services: [service]),
-      expectedSwift: expectedSwift,
-      client: true,
-      server: true,
-      accessLevel: .package
-    )
+    #expect(self.render(accessLevel: .public, service: service) == expectedSwift)
   }
 }
 
 extension TypealiasTranslatorSnippetBasedTests {
-  private func assertTypealiasTranslation(
-    codeGenerationRequest request: CodeGenerationRequest,
-    expectedSwift: String,
-    client: Bool,
-    server: Bool,
-    accessLevel: SourceGenerator.Config.AccessLevel
-  ) throws {
+  func render(
+    accessLevel: SourceGenerator.Config.AccessLevel,
+    service: ServiceDescriptor
+  ) -> String {
     let translator = MetadataTranslator()
-    let codeBlocks = request.services.flatMap { service in
-      translator.translate(
-        accessModifier: AccessModifier(accessLevel),
-        service: service,
-        client: client,
-        server: server
-      )
-    }
+    let codeBlocks = translator.translate(
+      accessModifier: AccessModifier(accessLevel),
+      service: service
+    )
+
     let renderer = TextBasedRenderer.default
     renderer.renderCodeBlocks(codeBlocks)
-    let contents = renderer.renderedContents()
-    try XCTAssertEqualWithDiff(contents, expectedSwift)
+    return renderer.renderedContents()
   }
 }
-
-#endif  // os(macOS) || os(Linux)