diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift new file mode 100644 index 000000000..a71cafd83 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -0,0 +1,304 @@ +/* + * 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. + */ + +extension TypealiasDescription { + /// `typealias Input = ` + package static func methodInput( + accessModifier: AccessModifier? = nil, + name: String + ) -> Self { + return TypealiasDescription( + accessModifier: accessModifier, + name: "Input", + existingType: .member(name) + ) + } + + /// `typealias Output = ` + package static func methodOutput( + accessModifier: AccessModifier? = nil, + name: String + ) -> Self { + return TypealiasDescription( + accessModifier: accessModifier, + name: "Output", + existingType: .member(name) + ) + } +} + +extension VariableDescription { + /// ``` + /// static let descriptor = GRPCCore.MethodDescriptor( + /// service: .descriptor.fullyQualifiedService, + /// method: "" + /// ``` + package static func methodDescriptor( + accessModifier: AccessModifier? = nil, + serviceNamespace: String, + literalMethodName: String + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern("descriptor")), + right: .functionCall( + FunctionCallDescription( + calledExpression: .identifierType(.methodDescriptor), + arguments: [ + FunctionArgumentDescription( + label: "service", + expression: .identifierType( + .member([serviceNamespace, "descriptor"]) + ).dot("fullyQualifiedService") + ), + FunctionArgumentDescription( + label: "method", + expression: .literal(literalMethodName) + ), + ] + ) + ) + ) + } + + /// ``` + /// static let descriptor = GRPCCore.ServiceDescriptor. + /// ``` + package static func serviceDescriptor( + accessModifier: AccessModifier? = nil, + namespacedProperty: String + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifierPattern("descriptor"), + right: .identifier(.type(.serviceDescriptor)).dot(namespacedProperty) + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension GRPCCore.ServiceDescriptor { + /// static let = Self( + /// package: "", + /// service: "" + /// ) + /// } + /// ``` + package static func serviceDescriptor( + accessModifier: AccessModifier? = nil, + propertyName: String, + literalNamespace: String, + literalService: String + ) -> ExtensionDescription { + return ExtensionDescription( + onType: "GRPCCore.ServiceDescriptor", + declarations: [ + .variable( + accessModifier: accessModifier, + isStatic: true, + 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) + ), + ] + ) + ) + ] + ) + } +} + +extension VariableDescription { + /// ``` + /// static let descriptors: [GRPCCore.MethodDescriptor] = [.descriptor, ...] + /// ``` + package static func methodDescriptorsArray( + accessModifier: AccessModifier? = nil, + methodNamespaceNames names: [String] + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern("descriptors")), + type: .array(.methodDescriptor), + right: .literal(.array(names.map { name in .identifierPattern(name).dot("descriptor") })) + ) + } +} + +extension EnumDescription { + /// ``` + /// enum { + /// typealias Input = + /// typealias Output = + /// static let descriptor = GRPCCore.MethodDescriptor( + /// service: .descriptor.fullyQualifiedService, + /// method: "" + /// ) + /// } + /// ``` + package static func methodNamespace( + accessModifier: AccessModifier? = nil, + name: String, + literalMethod: String, + serviceNamespace: String, + inputType: String, + outputType: String + ) -> Self { + return EnumDescription( + accessModifier: accessModifier, + name: name, + members: [ + .typealias(.methodInput(accessModifier: accessModifier, name: inputType)), + .typealias(.methodOutput(accessModifier: accessModifier, name: outputType)), + .variable( + .methodDescriptor( + accessModifier: accessModifier, + serviceNamespace: serviceNamespace, + literalMethodName: literalMethod + ) + ), + ] + ) + } + + /// ``` + /// enum Method { + /// enum { + /// typealias Input = + /// typealias Output = + /// static let descriptor = GRPCCore.MethodDescriptor( + /// service: .descriptor.fullyQualifiedService, + /// method: "" + /// ) + /// } + /// ... + /// static let descriptors: [GRPCCore.MethodDescriptor] = [ + /// .descriptor, + /// ... + /// ] + /// } + /// ``` + package static func methodsNamespace( + accessModifier: AccessModifier? = nil, + serviceNamespace: String, + methods: [MethodDescriptor] + ) -> EnumDescription { + var description = EnumDescription(accessModifier: accessModifier, name: "Method") + + // Add a namespace for each method. + let methodNamespaces: [Declaration] = methods.map { method in + return .enum( + .methodNamespace( + accessModifier: accessModifier, + name: method.name.base, + literalMethod: method.name.base, + serviceNamespace: serviceNamespace, + inputType: method.inputType, + outputType: method.outputType + ) + ) + } + description.members.append(contentsOf: methodNamespaces) + + // Add an array of method descriptors + let methodDescriptorsArray: VariableDescription = .methodDescriptorsArray( + accessModifier: accessModifier, + methodNamespaceNames: methods.map { $0.name.base } + ) + description.members.append(.variable(methodDescriptorsArray)) + + return description + } + + /// ``` + /// enum { + /// static let descriptor = GRPCCore.ServiceDescriptor. + /// enum Method { + /// ... + /// } + /// @available(...) + /// typealias StreamingServiceProtocol = ... + /// @available(...) + /// typealias ServiceProtocol = ... + /// ... + /// } + /// ``` + package static func serviceNamespace( + accessModifier: AccessModifier? = nil, + name: String, + serviceDescriptorProperty: String, + client: Bool, + server: Bool, + methods: [MethodDescriptor] + ) -> EnumDescription { + var description = EnumDescription(accessModifier: accessModifier, name: name) + + // static let descriptor = GRPCCore.ServiceDescriptor. + let descriptor = VariableDescription.serviceDescriptor( + accessModifier: accessModifier, + namespacedProperty: serviceDescriptorProperty + ) + description.members.append(.variable(descriptor)) + + // enum Method { ... } + let methodsNamespace: EnumDescription = .methodsNamespace( + accessModifier: accessModifier, + serviceNamespace: name, + 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 + .guarded( + .grpc, + .typealias( + accessModifier: accessModifier, + name: alias, + existingType: .member(name + "_" + alias) + ) + ) + } + description.members.append(contentsOf: typealiases) + + return description + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift new file mode 100644 index 000000000..981566543 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift @@ -0,0 +1,88 @@ +/* + * 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. + */ + +extension AvailabilityDescription { + package static let grpc = AvailabilityDescription( + osVersions: [ + OSVersion(os: .macOS, version: "15.0"), + OSVersion(os: .iOS, version: "18.0"), + OSVersion(os: .watchOS, version: "11.0"), + OSVersion(os: .tvOS, version: "18.0"), + OSVersion(os: .visionOS, version: "2.0"), + ] + ) +} + +extension ExistingTypeDescription { + fileprivate static func grpcCore(_ typeName: String) -> Self { + return .member(["GRPCCore", typeName]) + } + + fileprivate static func requestResponse( + for type: String?, + isRequest: Bool, + isStreaming: Bool, + isClient: Bool + ) -> Self { + let prefix = isStreaming ? "Streaming" : "" + let peer = isClient ? "Client" : "Server" + let kind = isRequest ? "Request" : "Response" + let baseType: Self = .grpcCore(prefix + peer + kind) + + if let type = type { + return .generic(wrapper: baseType, wrapped: .member(type)) + } else { + return baseType + } + } + + package static func serverRequest(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: true, isStreaming: streaming, isClient: false) + } + + package static func serverResponse(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: false, isStreaming: streaming, isClient: false) + } + + package static func clientRequest(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: true, isStreaming: streaming, isClient: true) + } + + package static func clientResponse(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: false, isStreaming: streaming, isClient: true) + } + + package static let serverContext: Self = .grpcCore("ServerContext") + package static let rpcRouter: Self = .grpcCore("RPCRouter") + package static let serviceDescriptor: Self = .grpcCore("ServiceDescriptor") + package static let methodDescriptor: Self = .grpcCore("MethodDescriptor") + + package static func serializer(forType type: String) -> Self { + .generic(wrapper: .grpcCore("MessageSerializer"), wrapped: .member(type)) + } + + package static func deserializer(forType type: String) -> Self { + .generic(wrapper: .grpcCore("MessageDeserializer"), wrapped: .member(type)) + } + + package static func rpcWriter(forType type: String) -> Self { + .generic(wrapper: .grpcCore("RPCWriter"), wrapped: .member(type)) + } + + package static let callOptions: Self = .grpcCore("CallOptions") + package static let metadata: Self = .grpcCore("Metadata") + package static let grpcClient: Self = .grpcCore("GRPCClient") +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index bc61819d3..a9f9c33d7 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -100,7 +100,7 @@ struct ImportDescription: Equatable, Codable, Sendable { /// A description of an access modifier. /// /// For example: `public`. -internal enum AccessModifier: String, Sendable, Equatable, Codable { +internal enum AccessModifier: String, Sendable, Equatable, Codable, CaseIterable { /// A declaration accessible outside of the module. case `public` diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 2319e900f..fb5474859 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -87,7 +87,7 @@ struct IDLToStructuredSwiftTranslator: Translator { } extension AccessModifier { - fileprivate init(_ accessLevel: SourceGenerator.Config.AccessLevel) { + init(_ accessLevel: SourceGenerator.Config.AccessLevel) { switch accessLevel.level { case .internal: self = .internal case .package: self = .package diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift new file mode 100644 index 000000000..76997a643 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -0,0 +1,295 @@ +/* + * 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 Testing + +@testable import GRPCCodeGen + +extension StructuedSwiftTests { + @Suite("Metadata") + struct Metadata { + @Test("@available(...)") + func grpcAvailability() async throws { + let availability: AvailabilityDescription = .grpc + let structDecl = StructDescription(name: "Ignored") + let expected = """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + struct Ignored {} + """ + + #expect(render(.guarded(availability, .struct(structDecl))) == expected) + } + + @Test("typealias Input = ", arguments: AccessModifier.allCases) + func methodInputTypealias(access: AccessModifier) { + let decl: TypealiasDescription = .methodInput(accessModifier: access, name: "Foo") + let expected = "\(access) typealias Input = Foo" + #expect(render(.typealias(decl)) == expected) + } + + @Test("typealias Output = ", arguments: AccessModifier.allCases) + func methodOutputTypealias(access: AccessModifier) { + let decl: TypealiasDescription = .methodOutput(accessModifier: access, name: "Foo") + let expected = "\(access) typealias Output = Foo" + #expect(render(.typealias(decl)) == expected) + } + + @Test( + "static let descriptor = GRPCCore.MethodDescriptor(...)", + arguments: AccessModifier.allCases + ) + func staticMethodDescriptorProperty(access: AccessModifier) { + let decl: VariableDescription = .methodDescriptor( + accessModifier: access, + serviceNamespace: "FooService", + literalMethodName: "Bar" + ) + + let expected = """ + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: FooService.descriptor.fullyQualifiedService, + method: "Bar" + ) + """ + #expect(render(.variable(decl)) == expected) + } + + @Test( + "static let descriptor = GRPCCore.ServiceDescriptor.", + arguments: AccessModifier.allCases + ) + func staticServiceDescriptorProperty(access: AccessModifier) { + let decl: VariableDescription = .serviceDescriptor( + accessModifier: access, + namespacedProperty: "foo" + ) + + let expected = "\(access) static let descriptor = GRPCCore.ServiceDescriptor.foo" + #expect(render(.variable(decl)) == expected) + } + + @Test("extension GRPCCore.ServiceDescriptor { ... }", arguments: AccessModifier.allCases) + func staticServiceDescriptorPropertyExtension(access: AccessModifier) { + let decl: ExtensionDescription = .serviceDescriptor( + accessModifier: access, + propertyName: "foo", + literalNamespace: "echo", + literalService: "EchoService" + ) + + let expected = """ + extension GRPCCore.ServiceDescriptor { + \(access) static let foo = Self( + package: "echo", + service: "EchoService" + ) + } + """ + #expect(render(.extension(decl)) == expected) + } + + @Test( + "static let descriptors: [GRPCCore.MethodDescriptor] = [...]", + arguments: AccessModifier.allCases + ) + func staticMethodDescriptorsArray(access: AccessModifier) { + let decl: VariableDescription = .methodDescriptorsArray( + accessModifier: access, + methodNamespaceNames: ["Foo", "Bar", "Baz"] + ) + + let expected = """ + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ + Foo.descriptor, + Bar.descriptor, + Baz.descriptor + ] + """ + #expect(render(.variable(decl)) == expected) + } + + @Test("enum { ... }", arguments: AccessModifier.allCases) + func methodNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .methodNamespace( + accessModifier: access, + name: "Foo", + literalMethod: "Foo", + serviceNamespace: "Bar_Baz", + inputType: "FooInput", + outputType: "FooOutput" + ) + + let expected = """ + \(access) enum Foo { + \(access) typealias Input = FooInput + \(access) typealias Output = FooOutput + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: Bar_Baz.descriptor.fullyQualifiedService, + method: "Foo" + ) + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum Method { ... }", arguments: AccessModifier.allCases) + func methodsNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .methodsNamespace( + accessModifier: access, + serviceNamespace: "Bar_Baz", + methods: [ + .init( + documentation: "", + name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ) + ] + ) + + let expected = """ + \(access) enum Method { + \(access) enum Foo { + \(access) typealias Input = FooInput + \(access) typealias Output = FooOutput + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: Bar_Baz.descriptor.fullyQualifiedService, + method: "Foo" + ) + } + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ + Foo.descriptor + ] + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum Method { ... } (no methods)", arguments: AccessModifier.allCases) + func methodsNamespaceEnumNoMethods(access: AccessModifier) { + let decl: EnumDescription = .methodsNamespace( + accessModifier: access, + serviceNamespace: "Bar_Baz", + methods: [] + ) + + let expected = """ + \(access) enum Method { + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum { ... }", arguments: AccessModifier.allCases) + func serviceNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .serviceNamespace( + accessModifier: access, + name: "Foo", + serviceDescriptorProperty: "foo", + client: false, + server: false, + methods: [ + .init( + documentation: "", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ] + ) + + let expected = """ + \(access) enum Foo { + \(access) static let descriptor = GRPCCore.ServiceDescriptor.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, + method: "Bar" + ) + } + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ + Bar.descriptor + ] + } + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test( + "enum { ... } (no methods)", + arguments: AccessModifier.allCases, + [(true, true), (false, false), (true, false), (false, true)] + ) + func serviceNamespaceEnumNoMethods(access: AccessModifier, config: (client: Bool, server: Bool)) + { + let decl: EnumDescription = .serviceNamespace( + accessModifier: access, + name: "Foo", + serviceDescriptorProperty: "foo", + client: config.client, + server: config.server, + methods: [] + ) + + var expected = """ + \(access) enum Foo { + \(access) static let descriptor = GRPCCore.ServiceDescriptor.foo + \(access) enum Method { + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] + }\n + """ + + if config.server { + expected += """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias StreamingServiceProtocol = Foo_StreamingServiceProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias ServiceProtocol = Foo_ServiceProtocol + """ + } + + if config.client { + if config.server { + expected += "\n" + } + + expected += """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias ClientProtocol = Foo_ClientProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias Client = Foo_Client + """ + } + + if config.client || config.server { + expected += "\n}" + } else { + expected += "}" + } + + #expect(render(.enum(decl)) == expected) + } + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift new file mode 100644 index 000000000..d364e843f --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift @@ -0,0 +1,66 @@ +/* + * 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 Testing + +@testable import GRPCCodeGen + +// Used as a namespace for organising other structured swift tests. +@Suite("Structued Swift") +struct StructuedSwiftTests {} + +func render(_ declaration: Declaration) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderDeclaration(declaration) + return renderer.renderedContents() +} + +func render(_ expression: Expression) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderExpression(expression) + return renderer.renderedContents() +} + +func render(_ blocks: [CodeBlock]) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderCodeBlocks(blocks) + return renderer.renderedContents() +} + +enum RPCKind: Hashable, Sendable, CaseIterable { + case unary + case clientStreaming + case serverStreaming + case bidirectionalStreaming + + var streamsInput: Bool { + switch self { + case .clientStreaming, .bidirectionalStreaming: + return true + case .unary, .serverStreaming: + return false + } + } + + var streamsOutput: Bool { + switch self { + case .serverStreaming, .bidirectionalStreaming: + return true + case .unary, .clientStreaming: + return false + } + } +}