Browse Source

Generate static service metadata (#1322)

Motivation:

Sometimes it's useful to know information about a service, including its
name, methods it offers and so on. An example where this is useful is
service discovery (#1183). However, we currently only provide the
service name and this isn't available statically. For service discovery
this is problematic as it requires users to create a client in order to
get the service name so that a server can be dialled.

For the async/await code, since it is not yet final, we can add
requirements to generated protocols to provide the service metadata
statically.

Modifications:

- Add service and method descriptors to `GRPC`
- Generate service descriptors for client and server separately (it's feasible
  that a user may generate client code into one module and server code
  into separate modules)
- Update the generated code for async/await and NIO based APIs to use
  the descriptors directly rather than generating literals in places
  where they are required.
- Add test for the generated echo service metadata
- Regenerate other services

Result:

Adopters can get static information about services.
George Barnett 4 years ago
parent
commit
cd2e87ecb5

+ 11 - 7
Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift

@@ -29,7 +29,7 @@ extension Generator {
     self.printAvailabilityForAsyncAwait()
     self.println("\(self.access) protocol \(self.asyncClientProtocolName): GRPCClient {")
     self.withIndentation {
-      self.println("var serviceName: String { get }")
+      self.println("static var serviceDescriptor: GRPCServiceDescriptor { get }")
       self.println("var interceptors: \(self.clientInterceptorProtocolName)? { get }")
 
       for method in service.methods {
@@ -71,10 +71,14 @@ extension Generator {
   internal func printAsyncClientProtocolExtension() {
     self.printAvailabilityForAsyncAwait()
     self.withIndentation("extension \(self.asyncClientProtocolName)", braces: .curly) {
-      // Service name. TODO: use static metadata.
-      self.withIndentation("\(self.access) var serviceName: String", braces: .curly) {
-        self.println("return \"\(self.servicePath)\"")
+      // Service descriptor.
+      self.withIndentation(
+        "\(self.access) static var serviceDescriptor: GRPCServiceDescriptor",
+        braces: .curly
+      ) {
+        self.println("return \(self.serviceClientMetadata).serviceDescriptor")
       }
+
       self.println()
 
       // Interceptor factory.
@@ -106,7 +110,7 @@ extension Generator {
             access: self.access
           ) {
             self.withIndentation("return self.make\(callTypeWithoutPrefix)", braces: .round) {
-              self.println("path: \(self.methodPath),")
+              self.println("path: \(self.methodPathUsingClientMetadata),")
               self.println("request: request,")
               self.println("callOptions: callOptions ?? self.defaultCallOptions,")
               self.println(
@@ -123,7 +127,7 @@ extension Generator {
             access: self.access
           ) {
             self.withIndentation("return self.make\(callTypeWithoutPrefix)", braces: .round) {
-              self.println("path: \(self.methodPath),")
+              self.println("path: \(self.methodPathUsingClientMetadata),")
               self.println("callOptions: callOptions ?? self.defaultCallOptions,")
               self.println(
                 "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []"
@@ -185,7 +189,7 @@ extension Generator {
               "return\(!streamsResponses ? " try await" : "") self.perform\(callTypeWithoutPrefix)",
               braces: .round
             ) {
-              self.println("path: \(self.methodPath),")
+              self.println("path: \(self.methodPathUsingClientMetadata),")
               self.println("\(requestParamName): \(requestParamName),")
               self.println("callOptions: callOptions ?? self.defaultCallOptions,")
               self.println(

+ 16 - 8
Sources/protoc-gen-grpc-swift/Generator-Client.swift

@@ -25,8 +25,6 @@ extension Generator {
       self.println()
       self.printClientProtocolExtension()
       self.println()
-      self.printServiceClientInterceptorFactoryProtocol()
-      self.println()
       self.printServiceClientImplementation()
     }
 
@@ -44,6 +42,14 @@ extension Generator {
       self.printEndCompilerGuardForAsyncAwait()
     }
 
+    // Both implementations share definitions for interceptors and metadata.
+    if self.options.generateClient || self.options.generateAsyncClient {
+      self.println()
+      self.printServiceClientInterceptorFactoryProtocol()
+      self.println()
+      self.printClientMetadata()
+    }
+
     if self.options.generateTestClient {
       self.println()
       self.printTestClient()
@@ -254,7 +260,7 @@ extension Generator {
     ) {
       self.println("return self.makeUnaryCall(")
       self.withIndentation {
-        self.println("path: \(self.methodPath),")
+        self.println("path: \(self.methodPathUsingClientMetadata),")
         self.println("request: request,")
         self.println("callOptions: callOptions ?? self.defaultCallOptions,")
         self.println(
@@ -281,7 +287,7 @@ extension Generator {
     ) {
       self.println("return self.makeServerStreamingCall(")
       self.withIndentation {
-        self.println("path: \(self.methodPath),")
+        self.println("path: \(self.methodPathUsingClientMetadata),")
         self.println("request: request,")
         self.println("callOptions: callOptions ?? self.defaultCallOptions,")
         self.println(
@@ -312,7 +318,7 @@ extension Generator {
     ) {
       self.println("return self.makeClientStreamingCall(")
       self.withIndentation {
-        self.println("path: \(self.methodPath),")
+        self.println("path: \(self.methodPathUsingClientMetadata),")
         self.println("callOptions: callOptions ?? self.defaultCallOptions,")
         self.println(
           "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []"
@@ -339,7 +345,7 @@ extension Generator {
     ) {
       self.println("return self.makeBidirectionalStreamingCall(")
       self.withIndentation {
-        self.println("path: \(self.methodPath),")
+        self.println("path: \(self.methodPathUsingClientMetadata),")
         self.println("callOptions: callOptions ?? self.defaultCallOptions,")
         self.println(
           "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? [],"
@@ -462,7 +468,7 @@ extension Generator {
     ) {
       self
         .println(
-          "return self.fakeChannel.\(factory)(path: \(self.methodPath), requestHandler: requestHandler)"
+          "return self.fakeChannel.\(factory)(path: \(self.methodPathUsingClientMetadata), requestHandler: requestHandler)"
         )
     }
   }
@@ -472,7 +478,9 @@ extension Generator {
       .println("/// Returns true if there are response streams enqueued for '\(self.method.name)'")
     self.println("\(self.access) var has\(self.method.name)ResponsesRemaining: Bool {")
     self.withIndentation {
-      self.println("return self.fakeChannel.hasFakeResponseEnqueued(forPath: \(self.methodPath))")
+      self.println(
+        "return self.fakeChannel.hasFakeResponseEnqueued(forPath: \(self.methodPathUsingClientMetadata))"
+      )
     }
     self.println("}")
   }

+ 82 - 0
Sources/protoc-gen-grpc-swift/Generator-Metadata.swift

@@ -0,0 +1,82 @@
+/*
+ * Copyright 2021, 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 SwiftProtobuf
+import SwiftProtobufPluginLibrary
+
+extension Generator {
+  internal func printServerMetadata() {
+    self.printMetadata(server: true)
+  }
+
+  internal func printClientMetadata() {
+    self.printMetadata(server: false)
+  }
+
+  private func printMetadata(server: Bool) {
+    let enumName = server ? self.serviceServerMetadata : self.serviceClientMetadata
+
+    self.withIndentation("\(self.access) enum \(enumName)", braces: .curly) {
+      self.println("\(self.access) static let serviceDescriptor = GRPCServiceDescriptor(")
+      self.withIndentation {
+        self.println("name: \(quoted(self.service.name)),")
+        self.println("fullName: \(quoted(self.servicePath)),")
+        self.println("methods: [")
+        for method in self.service.methods {
+          self.method = method
+          self.withIndentation {
+            self.println("\(enumName).Methods.\(self.methodFunctionName),")
+          }
+        }
+        self.println("]")
+      }
+      self.println(")")
+      self.println()
+
+      self.withIndentation("\(self.access) enum Methods", braces: .curly) {
+        for (offset, method) in self.service.methods.enumerated() {
+          self.method = method
+          self.println(
+            "\(self.access) static let \(self.methodFunctionName) = GRPCMethodDescriptor("
+          )
+          self.withIndentation {
+            self.println("name: \(quoted(self.method.name)),")
+            self.println("path: \(quoted(self.methodPath)),")
+            self.println("type: \(streamingType(self.method).asGRPCCallTypeCase)")
+          }
+          self.println(")")
+
+          if (offset + 1) < self.service.methods.count {
+            self.println()
+          }
+        }
+      }
+    }
+  }
+}
+
+extension Generator {
+  internal var serviceServerMetadata: String {
+    return nameForPackageService(self.file, self.service) + "ServerMetadata"
+  }
+
+  internal var serviceClientMetadata: String {
+    return nameForPackageService(self.file, self.service) + "ClientMetadata"
+  }
+
+  internal var methodPathUsingClientMetadata: String {
+    return "\(self.serviceClientMetadata).Methods.\(self.methodFunctionName).path"
+  }
+}

+ 9 - 1
Sources/protoc-gen-grpc-swift/Generator-Names.swift

@@ -147,6 +147,14 @@ extension Generator {
   }
 
   internal var methodPath: String {
-    return "\"/" + self.servicePath + "/" + method.name + "\""
+    return "/" + self.fullMethodName
   }
+
+  internal var fullMethodName: String {
+    return self.servicePath + "/" + self.method.name
+  }
+}
+
+internal func quoted(_ str: String) -> String {
+  return "\"" + str + "\""
 }

+ 13 - 1
Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift

@@ -32,6 +32,7 @@ extension Generator {
       "\(self.access) protocol \(self.asyncProviderName): CallHandlerProvider",
       braces: .curly
     ) {
+      self.println("static var serviceDescriptor: GRPCServiceDescriptor { get }")
       self.println("var interceptors: \(self.serverInterceptorProtocolName)? { get }")
 
       for method in service.methods {
@@ -100,8 +101,19 @@ extension Generator {
     // Default extension to provide the service name and routing for methods.
     self.printAvailabilityForAsyncAwait()
     self.withIndentation("extension \(self.asyncProviderName)", braces: .curly) {
+      self.withIndentation(
+        "\(self.access) static var serviceDescriptor: GRPCServiceDescriptor",
+        braces: .curly
+      ) {
+        self.println("return \(self.serviceServerMetadata).serviceDescriptor")
+      }
+
+      self.println()
+
+      // This fulfils a requirement from 'CallHandlerProvider'
       self.withIndentation("\(self.access) var serviceName: Substring", braces: .curly) {
-        self.println("return \"\(self.servicePath)\"")
+        /// This API returns a Substring (hence the '[...]')
+        self.println("return \(self.serviceServerMetadata).serviceDescriptor.fullName[...]")
       }
 
       self.println()

+ 9 - 8
Sources/protoc-gen-grpc-swift/Generator-Server.swift

@@ -23,9 +23,6 @@ extension Generator {
       self.printServerProtocol()
       self.println()
       self.printServerProtocolExtension()
-      self.println()
-      self.printServerInterceptorFactoryProtocol()
-      self.println()
     }
 
     if self.options.generateAsyncServer {
@@ -36,13 +33,14 @@ extension Generator {
       self.printServerProtocolExtensionAsyncAwait()
       self.println()
       self.printEndCompilerGuardForAsyncAwait()
-      self.println()
     }
 
-    // If we generate only the async server we need to print the interceptor factory protocol (as
-    // it is used by both).
-    if self.options.generateAsyncServer, !self.options.generateServer {
+    // Both implementations share definitions for interceptors and metadata.
+    if self.options.generateServer || self.options.generateAsyncServer {
+      self.println()
       self.printServerInterceptorFactoryProtocol()
+      self.println()
+      self.printServerMetadata()
     }
   }
 
@@ -91,7 +89,10 @@ extension Generator {
   private func printServerProtocolExtension() {
     self.println("extension \(self.providerName) {")
     self.withIndentation {
-      self.println("\(self.access) var serviceName: Substring { return \"\(self.servicePath)\" }")
+      self.withIndentation("\(self.access) var serviceName: Substring", braces: .curly) {
+        /// This API returns a Substring (hence the '[...]')
+        self.println("return \(self.serviceServerMetadata).serviceDescriptor.fullName[...]")
+      }
       self.println()
       self.println(
         "/// Determines, calls and returns the appropriate request handler, depending on the request's method."

+ 15 - 0
Sources/protoc-gen-grpc-swift/StreamingType.swift

@@ -22,6 +22,21 @@ internal enum StreamingType {
   case bidirectionalStreaming
 }
 
+extension StreamingType {
+  internal var asGRPCCallTypeCase: String {
+    switch self {
+    case .unary:
+      return "GRPCCallType.unary"
+    case .clientStreaming:
+      return "GRPCCallType.clientStreaming"
+    case .serverStreaming:
+      return "GRPCCallType.serverStreaming"
+    case .bidirectionalStreaming:
+      return "GRPCCallType.bidirectionalStreaming"
+    }
+  }
+}
+
 internal func streamingType(_ method: MethodDescriptor) -> StreamingType {
   if method.proto.clientStreaming {
     if method.proto.serverStreaming {