diff --git a/Package.swift b/Package.swift index 4b194d25b..d0db22a72 100644 --- a/Package.swift +++ b/Package.swift @@ -94,6 +94,7 @@ extension Target.Dependency { static let reflectionService: Self = .target(name: "GRPCReflectionService") static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") static let grpcProtobuf: Self = .target(name: "GRPCProtobuf") + static let grpcProtobufCodeGen: Self = .target(name: "GRPCProtobufCodeGen") // Target dependencies; internal static let grpcSampleData: Self = .target(name: "GRPCSampleData") @@ -235,6 +236,7 @@ extension Target { dependencies: [ .protobuf, .protobufPluginLibrary, + .grpcCodeGen ], exclude: [ "README.md", @@ -341,12 +343,22 @@ extension Target { static let grpcProtobufTests: Target = .testTarget( name: "GRPCProtobufTests", dependencies: [ - .grpcCore, .grpcProtobuf, + .grpcCore, .protobuf ] ) + static let grpcProtobufCodeGenTests: Target = .testTarget( + name: "GRPCProtobufCodeGenTests", + dependencies: [ + .grpcCodeGen, + .grpcProtobufCodeGen, + .protobuf, + .protobufPluginLibrary + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -601,10 +613,19 @@ extension Target { name: "GRPCProtobuf", dependencies: [ .grpcCore, - .protobuf + .protobuf, ], path: "Sources/GRPCProtobuf" ) + static let grpcProtobufCodeGen: Target = .target( + name: "GRPCProtobufCodeGen", + dependencies: [ + .protobuf, + .protobufPluginLibrary, + .grpcCodeGen + ], + path: "Sources/GRPCProtobufCodeGen" + ) } // MARK: - Products @@ -693,6 +714,7 @@ let package = Package( .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf, + .grpcProtobufCodeGen, // v2 tests .grpcCoreTests, @@ -702,7 +724,8 @@ let package = Package( .grpcHTTP2CoreTests, .grpcHTTP2TransportNIOPosixTests, .grpcHTTP2TransportNIOTransportServicesTests, - .grpcProtobufTests + .grpcProtobufTests, + .grpcProtobufCodeGenTests ] ) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 9711b39d2..0c0abb1b9 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -81,7 +81,7 @@ public struct CodeGenerationRequest { } /// Represents an import: a module or a specific item from a module. - public struct Dependency { + public struct Dependency: Equatable { /// If the dependency is an item, the property's value is the item representation. /// If the dependency is a module, this property is nil. public var item: Item? = nil @@ -111,7 +111,7 @@ public struct CodeGenerationRequest { } /// Represents an item imported from a module. - public struct Item { + public struct Item: Equatable { /// The keyword that specifies the item's kind (e.g. `func`, `struct`). public var kind: Kind @@ -124,7 +124,7 @@ public struct CodeGenerationRequest { } /// Represents the imported item's kind. - public struct Kind { + public struct Kind: Equatable { /// Describes the keyword associated with the imported item. internal enum Value: String { case `typealias` @@ -186,8 +186,8 @@ public struct CodeGenerationRequest { } /// Describes any requirement for the `@preconcurrency` attribute. - public struct PreconcurrencyRequirement { - internal enum Value { + public struct PreconcurrencyRequirement: Equatable { + internal enum Value: Equatable { case required case notRequired case requiredOnOS([String]) diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift new file mode 100644 index 000000000..1ff93f6a0 --- /dev/null +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -0,0 +1,123 @@ +/* + * 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. + */ + +import Foundation +import SwiftProtobuf +import SwiftProtobufPluginLibrary + +import struct GRPCCodeGen.CodeGenerationRequest + +/// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. +internal struct ProtobufCodeGenParser { + internal init() {} + internal func parse(input: FileDescriptor) throws -> CodeGenerationRequest { + var header = input.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: \(input.name) + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + """ + var dependencies = input.dependencies.map { + CodeGenerationRequest.Dependency(module: $0.name) + } + dependencies.append(CodeGenerationRequest.Dependency(module: "GRPCProtobuf")) + let lookupSerializer: (String) -> String = { messageType in + "ProtobufSerializer<\(messageType)>()" + } + let lookupDeserializer: (String) -> String = { messageType in + "ProtobufDeserializer<\(messageType)>()" + } + let services = input.services.map { + CodeGenerationRequest.ServiceDescriptor(descriptor: $0, package: input.package) + } + + return CodeGenerationRequest( + fileName: input.name, + leadingTrivia: header + leadingTrivia, + dependencies: dependencies, + services: services, + lookupSerializer: lookupSerializer, + lookupDeserializer: lookupDeserializer + ) + } +} + +extension CodeGenerationRequest.ServiceDescriptor { + fileprivate init(descriptor: ServiceDescriptor, package: String) { + let methods = descriptor.methods.map { + CodeGenerationRequest.ServiceDescriptor.MethodDescriptor(descriptor: $0) + } + let name = CodeGenerationRequest.Name( + base: descriptor.name, + generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), + generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name) + ) + let namespace = CodeGenerationRequest.Name( + base: package, + generatedUpperCase: NamingUtils.toUpperCamelCase(package), + generatedLowerCase: NamingUtils.toLowerCamelCase(package) + ) + let documentation = descriptor.protoSourceComments() + self.init(documentation: documentation, name: name, namespace: namespace, methods: methods) + } +} + +extension CodeGenerationRequest.ServiceDescriptor.MethodDescriptor { + fileprivate init(descriptor: MethodDescriptor) { + let name = CodeGenerationRequest.Name( + base: descriptor.name, + generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), + generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name) + ) + let documentation = descriptor.protoSourceComments() + self.init( + documentation: documentation, + name: name, + isInputStreaming: descriptor.clientStreaming, + isOutputStreaming: descriptor.serverStreaming, + inputType: descriptor.inputType.name, + outputType: descriptor.outputType.name + ) + } +} + +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 + } +} diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift new file mode 100644 index 000000000..664501d82 --- /dev/null +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -0,0 +1,193 @@ +/* + * 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. + */ + +import GRPCCodeGen +import SwiftProtobuf +import SwiftProtobufPluginLibrary +import XCTest + +@testable import GRPCProtobufCodeGen + +final class ProtobufCodeGenParserTests: XCTestCase { + func testParser() throws { + let parsedCodeGenRequest = try ProtobufCodeGenParser().parse( + input: self.helloWorldFileDescriptor + ) + XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") + XCTAssertEqual( + parsedCodeGenRequest.leadingTrivia, + """ + // Copyright 2015 gRPC authors. + // + // 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. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: helloworld.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + """ + ) + + XCTAssertEqual(parsedCodeGenRequest.services.count, 1) + + let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( + documentation: "/// Sends a greeting.\n", + name: CodeGenerationRequest.Name( + base: "SayHello", + generatedUpperCase: "SayHello", + generatedLowerCase: "sayHello" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "HelloRequest", + outputType: "HelloReply" + ) + guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } + XCTAssertEqual(method, expectedMethod) + + let expectedService = CodeGenerationRequest.ServiceDescriptor( + documentation: "/// The greeting service definition.\n", + name: CodeGenerationRequest.Name( + base: "Greeter", + generatedUpperCase: "Greeter", + generatedLowerCase: "greeter" + ), + namespace: CodeGenerationRequest.Name( + base: "helloworld", + generatedUpperCase: "Helloworld", + generatedLowerCase: "helloworld" + ), + methods: [expectedMethod] + ) + guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } + XCTAssertEqual(service, expectedService) + XCTAssertEqual(service.methods.count, 1) + + XCTAssertEqual( + parsedCodeGenRequest.lookupSerializer("HelloRequest"), + "ProtobufSerializer()" + ) + XCTAssertEqual( + parsedCodeGenRequest.lookupDeserializer("HelloRequest"), + "ProtobufDeserializer()" + ) + XCTAssertEqual(parsedCodeGenRequest.dependencies.count, 1) + XCTAssertEqual( + parsedCodeGenRequest.dependencies[0], + CodeGenerationRequest.Dependency(module: "GRPCProtobuf") + ) + } + + var helloWorldFileDescriptor: FileDescriptor { + let requestType = Google_Protobuf_DescriptorProto.with { + $0.name = "HelloRequest" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "name" + $0.number = 1 + $0.label = .optional + $0.type = .string + $0.jsonName = "name" + } + ] + } + let responseType = Google_Protobuf_DescriptorProto.with { + $0.name = "HelloReply" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "message" + $0.number = 1 + $0.label = .optional + $0.type = .string + $0.jsonName = "message" + } + ] + } + + let service = Google_Protobuf_ServiceDescriptorProto.with { + $0.name = "Greeter" + $0.method = [ + Google_Protobuf_MethodDescriptorProto.with { + $0.name = "SayHello" + $0.inputType = ".helloworld.HelloRequest" + $0.outputType = ".helloworld.HelloReply" + $0.clientStreaming = false + $0.serverStreaming = false + } + ] + } + let protoDescriptor = Google_Protobuf_FileDescriptorProto.with { + $0.name = "helloworld.proto" + $0.package = "helloworld" + $0.messageType = [requestType, responseType] + $0.service = [service] + $0.sourceCodeInfo = Google_Protobuf_SourceCodeInfo.with { + $0.location = [ + Google_Protobuf_SourceCodeInfo.Location.with { + $0.path = [12] + $0.span = [14, 0, 18] + $0.leadingDetachedComments = [ + """ + Copyright 2015 gRPC authors. + + 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. + + """ + ] + }, + Google_Protobuf_SourceCodeInfo.Location.with { + $0.path = [6, 0] + $0.span = [19, 0, 22, 1] + $0.leadingComments = " The greeting service definition.\n" + }, + Google_Protobuf_SourceCodeInfo.Location.with { + $0.path = [6, 0, 2, 0] + $0.span = [21, 2, 53] + $0.leadingComments = " Sends a greeting.\n" + }, + ] + } + $0.syntax = "proto3" + } + let descriptorSet = DescriptorSet(protos: [protoDescriptor]) + return descriptorSet.fileDescriptor(named: "helloworld.proto")! + } +}