diff --git a/Sources/GRPCProtobuf/Documentation.docc/Articles/Generating-stubs.md b/Sources/GRPCProtobuf/Documentation.docc/Articles/Generating-stubs.md index 991a55b..a3ebecd 100644 --- a/Sources/GRPCProtobuf/Documentation.docc/Articles/Generating-stubs.md +++ b/Sources/GRPCProtobuf/Documentation.docc/Articles/Generating-stubs.md @@ -133,26 +133,35 @@ protoc \ #### Generator options -| Name | Possible Values | Default | Description | -|---------------------------|---------------------------------------------|------------|----------------------------------------------------------| -| `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs | -| `Server` | `True`, `False` | `True` | Generate server stubs | -| `Client` | `True`, `False` | `True` | Generate client stubs | -| `FileNaming` | `FullPath`, `PathToUnderscores`, `DropPath` | `FullPath` | How generated source files should be named. (See below.) | -| `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. (See below.) | -| `UseAccessLevelOnImports` | `True`, `False` | `False` | Whether imports should have explicit access levels. | - -The `FileNaming` option has three possible values, for an input of `foo/bar/baz.proto` the following +| Name | Possible Values | Default | Description | +|---------------------------|---------------------------------------------|-----------------|----------------------------------------------------------| +| `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs | +| `Server` | `True`, `False` | `True` | Generate server stubs | +| `Client` | `True`, `False` | `True` | Generate client stubs | +| `FileNaming` | `FullPath`, `PathToUnderscores`, `DropPath` | `FullPath` | How generated source files should be named. † | +| `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. ‡ | +| `UseAccessLevelOnImports` | `True`, `False` | `False` | Whether imports should have explicit access levels. | +| `GRPCModuleName` | | `GRPCCore` | The name of the `GRPCCore` module. | +| `GRPCProtobufModuleName` | | `GRPCProtobuf` | The name of the `GRPCProtobuf` module. | +| `SwiftProtobufModuleName` | | `SwiftProtobuf` | The name of the `SwiftProtobuf` module. | +| `Availability` | String, in the form `OS Version` | | Platform availability to use in generated code. § | + +† The `FileNaming` option has three possible values, for an input of `foo/bar/baz.proto` the following output file will be generated: - `FullPath`: `foo/bar/baz.grpc.swift`. - `PathToUnderscores`: `foo_bar_baz.grpc.swift` - `DropPath`: `baz.grpc.swift` -The code generator assumes all inputs are generated into the same module, `ProtoPathModuleMappings` +‡ The code generator assumes all inputs are generated into the same module, `ProtoPathModuleMappings` allows you to specify a mapping from `.proto` files to the Swift module they are generated in. This allows the code generator to add appropriate imports to your generated stubs. This is described in more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md). +§ If unspecified the following availability is used: macOS 15, iOS 18, tvOS 18, +watchOS 11, visionOS 2. The `Availability` option may be specified multiple +times, where each value is a space delimited pair of platform and version, e.g. +`Availability=macOS 15.0`. + #### Building the protoc plugin > The version of `protoc-gen-grpc-swift` you use mustn't be newer than the version of diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index a7682dc..55307c7 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -29,7 +29,8 @@ package struct ProtobufCodeGenerator { package func generateCode( fileDescriptor: FileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings, - extraModuleImports: [String] + extraModuleImports: [String], + availabilityOverrides: [(os: String, version: String)] = [] ) throws -> String { let parser = ProtobufCodeGenParser( protoFileModuleMappings: protoFileModuleMappings, @@ -46,6 +47,17 @@ package struct ProtobufCodeGenerator { indentation: self.config.indentation ) codeGeneratorConfig.grpcCoreModuleName = self.config.moduleNames.grpcCore + + if availabilityOverrides.isEmpty { + codeGeneratorConfig.availability = .default + } else { + codeGeneratorConfig.availability = .custom( + availabilityOverrides.map { (os, version) in + .init(os: os, version: version) + } + ) + } + let codeGenerator = GRPCCodeGen.CodeGenerator(config: codeGeneratorConfig) let codeGenerationRequest = try parser.parse(descriptor: fileDescriptor) diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift index 013ec6a..b6299b2 100644 --- a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift +++ b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift @@ -73,7 +73,8 @@ final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator { let contents = try fileGenerator.generateCode( fileDescriptor: descriptor, protoFileModuleMappings: options.protoToModuleMappings, - extraModuleImports: options.extraModuleImports + extraModuleImports: options.extraModuleImports, + availabilityOverrides: options.availabilityOverrides ) try outputs.add(fileName: fileName, contents: contents) diff --git a/Sources/protoc-gen-grpc-swift/Options.swift b/Sources/protoc-gen-grpc-swift/Options.swift index b6db70d..5907ab1 100644 --- a/Sources/protoc-gen-grpc-swift/Options.swift +++ b/Sources/protoc-gen-grpc-swift/Options.swift @@ -52,6 +52,7 @@ struct GeneratorOptions { private(set) var protoToModuleMappings = ProtoFileToModuleMappings() private(set) var fileNaming = FileNaming.fullPath private(set) var extraModuleImports: [String] = [] + private(set) var availabilityOverrides: [(os: String, version: String)] = [] private(set) var config: ProtobufCodeGenerator.Config = .defaults @@ -130,6 +131,18 @@ struct GeneratorOptions { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } + case "Availability": + if !pair.value.isEmpty { + let parts = pair.value.split(separator: " ", maxSplits: 1) + if parts.count == 2 { + self.availabilityOverrides.append((os: String(parts[0]), version: String(parts[1]))) + } else { + throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) + } + } else { + throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) + } + case "ReflectionData": throw GenerationError.unsupportedParameter( name: pair.key, diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 393f057..86e5ec4 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -25,8 +25,38 @@ struct ProtobufCodeGeneratorTests { static let descriptorSetName = "test-service" static let fileDescriptorName = "test-service" - @Test("Generate", arguments: [CodeGenerator.Config.AccessLevel.internal]) - func generate(accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel) throws { + enum Availability { + case `default` + case fooOS + + var override: [(String, String)] { + switch self { + case .default: + return [] + case .fooOS: + return [("fooOS", "42.0")] + } + } + + var expected: String { + switch self { + case .default: + return "macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0" + case .fooOS: + return "fooOS 42.0" + } + } + } + + @Test( + "Generate", + arguments: [CodeGenerator.Config.AccessLevel.internal], + [Availability.default, Availability.fooOS] + ) + func generate( + accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel, + availability: Availability + ) throws { var config = ProtobufCodeGenerator.Config.defaults config.accessLevel = accessLevel config.indentation = 2 @@ -44,10 +74,13 @@ struct ProtobufCodeGeneratorTests { fatalError() } + let expectedAvailability = availability.expected + let generated = try generator.generateCode( fileDescriptor: Self.fileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings(), - extraModuleImports: [] + extraModuleImports: [], + availabilityOverrides: availability.override ) let expected = """ @@ -70,7 +103,7 @@ struct ProtobufCodeGeneratorTests { // MARK: - test.TestService /// Namespace containing generated types for the "test.TestService" service. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) \(access) enum Test_TestService { /// Service descriptor for the "test.TestService" service. \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService") @@ -134,7 +167,7 @@ struct ProtobufCodeGeneratorTests { } } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension GRPCCore.ServiceDescriptor { /// Service descriptor for the "test.TestService" service. \(access) static let test_TestService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService") @@ -142,7 +175,7 @@ struct ProtobufCodeGeneratorTests { // MARK: test.TestService (server) - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension Test_TestService { /// Streaming variant of the service protocol for the "test.TestService" service. /// @@ -404,7 +437,7 @@ struct ProtobufCodeGeneratorTests { } // Default implementation of 'registerMethods(with:)'. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension Test_TestService.StreamingServiceProtocol { \(access) func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( @@ -455,7 +488,7 @@ struct ProtobufCodeGeneratorTests { } // Default implementation of streaming methods from 'StreamingServiceProtocol'. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension Test_TestService.ServiceProtocol { \(access) func unary( request: GRPCCore.StreamingServerRequest, @@ -492,7 +525,7 @@ struct ProtobufCodeGeneratorTests { } // Default implementation of methods from 'ServiceProtocol'. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension Test_TestService.SimpleServiceProtocol { \(access) func unary( request: GRPCCore.ServerRequest, @@ -557,7 +590,7 @@ struct ProtobufCodeGeneratorTests { // MARK: test.TestService (client) - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension Test_TestService { /// Generated client protocol for the "test.TestService" service. /// @@ -816,7 +849,7 @@ struct ProtobufCodeGeneratorTests { } // Helpers providing default arguments to 'ClientProtocol' methods. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension Test_TestService.ClientProtocol { /// Call the "Unary" method. /// @@ -932,7 +965,7 @@ struct ProtobufCodeGeneratorTests { } // Helpers providing sugared APIs for 'ClientProtocol' methods. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @available(\(expectedAvailability), *) extension Test_TestService.ClientProtocol { /// Call the "Unary" method. ///