/* * Copyright 2024, 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. */ internal import SwiftProtobuf package import SwiftProtobufPluginLibrary package import struct GRPCCodeGen.CodeGenerationRequest package import struct GRPCCodeGen.CodeGenerator package import struct GRPCCodeGen.Dependency package import struct GRPCCodeGen.MethodDescriptor package import struct GRPCCodeGen.MethodName package import struct GRPCCodeGen.ServiceDescriptor package import struct GRPCCodeGen.ServiceName #if canImport(FoundationEssentials) internal import struct FoundationEssentials.IndexPath #else internal import struct Foundation.IndexPath #endif /// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. package struct ProtobufCodeGenParser { let extraModuleImports: [String] let protoToModuleMappings: ProtoFileToModuleMappings let accessLevel: CodeGenerator.Config.AccessLevel package init( protoFileModuleMappings: ProtoFileToModuleMappings, extraModuleImports: [String], accessLevel: CodeGenerator.Config.AccessLevel ) { self.extraModuleImports = extraModuleImports self.protoToModuleMappings = protoFileModuleMappings self.accessLevel = accessLevel } package func parse(descriptor: FileDescriptor) throws -> CodeGenerationRequest { let namer = SwiftProtobufNamer( currentFile: descriptor, protoFileToModuleMappings: self.protoToModuleMappings ) var header = descriptor.header // Ensuring there is a blank line after the header. if !header.isEmpty && !header.hasSuffix("\n\n") { header.append("\n") } let leadingTrivia = """ // DO NOT EDIT. // swift-format-ignore-file // // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. // Source: \(descriptor.name) // // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift """ let services = descriptor.services.map { GRPCCodeGen.ServiceDescriptor( descriptor: $0, package: descriptor.package, protobufNamer: namer, file: descriptor ) } return CodeGenerationRequest( fileName: descriptor.name, leadingTrivia: header + leadingTrivia, dependencies: self.codeDependencies(file: descriptor), services: services, makeSerializerCodeSnippet: { messageType in "GRPCProtobuf.ProtobufSerializer<\(messageType)>()" }, makeDeserializerCodeSnippet: { messageType in "GRPCProtobuf.ProtobufDeserializer<\(messageType)>()" } ) } } extension ProtobufCodeGenParser { fileprivate func codeDependencies(file: FileDescriptor) -> [Dependency] { guard file.services.count > 0 else { return [] } var codeDependencies: [Dependency] = [ Dependency(module: "GRPCProtobuf", accessLevel: .internal) ] // If there's a dependency on a bundled proto then add the SwiftProtobuf import. // // Importing SwiftProtobuf unconditionally results in warnings in the generated // code if access-levels are used on imports and no bundled protos are used. let dependsOnBundledProto = file.dependencies.contains { descriptor in SwiftProtobufInfo.isBundledProto(file: descriptor) } if dependsOnBundledProto { codeDependencies.append(Dependency(module: "SwiftProtobuf", accessLevel: self.accessLevel)) } // Adding as dependencies the modules containing generated code or types for // '.proto' files imported in the '.proto' file we are parsing. codeDependencies.append( contentsOf: (self.protoToModuleMappings.neededModules(forFile: file) ?? []).map { Dependency(module: $0, accessLevel: self.accessLevel) } ) // Adding extra imports passed in as an option to the plugin. codeDependencies.append( contentsOf: self.extraModuleImports.sorted().map { Dependency(module: $0, accessLevel: self.accessLevel) } ) return codeDependencies } } extension GRPCCodeGen.ServiceDescriptor { fileprivate init( descriptor: SwiftProtobufPluginLibrary.ServiceDescriptor, package: String, protobufNamer: SwiftProtobufNamer, file: FileDescriptor ) { let methods = descriptor.methods.map { GRPCCodeGen.MethodDescriptor( descriptor: $0, protobufNamer: protobufNamer ) } let typePrefix = protobufNamer.typePrefix(forFile: file) let name = ServiceName( identifyingName: descriptor.fullName, // The service name from the '.proto' file is expected to be in upper camel case typeName: typePrefix + descriptor.name, propertyName: protobufNamer.typePrefixProperty(file: file) + descriptor.name ) let documentation = descriptor.protoSourceComments() self.init(documentation: documentation, name: name, methods: methods) } } extension GRPCCodeGen.MethodDescriptor { fileprivate init( descriptor: SwiftProtobufPluginLibrary.MethodDescriptor, protobufNamer: SwiftProtobufNamer ) { let name = MethodName( identifyingName: descriptor.name, // The method name from the '.proto' file is expected to be in upper camel case typeName: descriptor.name, functionName: CamelCaser.toLowerCamelCase(descriptor.name) ) let documentation = descriptor.protoSourceComments() self.init( documentation: documentation, name: name, isInputStreaming: descriptor.clientStreaming, isOutputStreaming: descriptor.serverStreaming, inputType: protobufNamer.fullName(message: descriptor.inputType), outputType: protobufNamer.fullName(message: descriptor.outputType) ) } } extension FileDescriptor { fileprivate var header: String { var header = String() // Field number used to collect the syntax field which is usually the first // declaration in a.proto file. // See more here: // https://github.com/apple/swift-protobuf/blob/main/Protos/SwiftProtobuf/google/protobuf/descriptor.proto let syntaxPath = IndexPath(index: 12) if let syntaxLocation = self.sourceCodeInfoLocation(path: syntaxPath) { header = syntaxLocation.asSourceComment( commentPrefix: "///", leadingDetachedPrefix: "//" ) } return header } } extension SwiftProtobufNamer { internal func typePrefixProperty(file: FileDescriptor) -> String { let typePrefix = self.typePrefix(forFile: file) let lowercased = typePrefix.split(separator: "_").map { component in NamingUtils.toLowerCamelCase(String(component)) } let joined = lowercased.joined(separator: "_") if typePrefix.hasSuffix("_"), !joined.hasSuffix("_") { // Add the trailing "_" if it was dropped. return joined + "_" } else { return joined } } } extension String { internal func trimTrailingUnderscores() -> String { if let index = self.lastIndex(where: { $0 != "_" }) { return String(self[...index]) } else { return "" } } }