diff --git a/Package.swift b/Package.swift index cd9be155f..870d0d16d 100644 --- a/Package.swift +++ b/Package.swift @@ -93,6 +93,7 @@ extension Target.Dependency { static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") static let reflectionService: Self = .target(name: "GRPCReflectionService") static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") + static let grpcProtobuf: Self = .target(name: "GRPCProtobuf") // Target dependencies; internal static let grpcSampleData: Self = .target(name: "GRPCSampleData") @@ -330,6 +331,15 @@ extension Target { ] ) + static let grpcProtobufTests: Target = .testTarget( + name: "GRPCProtobufTests", + dependencies: [ + .grpcCore, + .grpcProtobuf, + .protobuf + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -579,6 +589,15 @@ extension Target { name: "GRPCCodeGen", path: "Sources/GRPCCodeGen" ) + + static let grpcProtobuf: Target = .target( + name: "GRPCProtobuf", + dependencies: [ + .grpcCore, + .protobuf + ], + path: "Sources/GRPCProtobuf" + ) } // MARK: - Products @@ -666,6 +685,7 @@ let package = Package( .grpcHTTP2Core, .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, + .grpcProtobuf, // v2 tests .grpcCoreTests, @@ -674,7 +694,8 @@ let package = Package( .grpcInterceptorsTests, .grpcHTTP2CoreTests, .grpcHTTP2TransportNIOPosixTests, - .grpcHTTP2TransportNIOTransportServicesTests + .grpcHTTP2TransportNIOTransportServicesTests, + .grpcProtobufTests ] ) diff --git a/Sources/GRPCProtobuf/Coding.swift b/Sources/GRPCProtobuf/Coding.swift new file mode 100644 index 000000000..174c87c1e --- /dev/null +++ b/Sources/GRPCProtobuf/Coding.swift @@ -0,0 +1,63 @@ +/* + * 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 GRPCCore +import SwiftProtobuf + +/// Serializes a Protobuf message into a sequence of bytes. +public struct ProtobufSerializer: GRPCCore.MessageSerializer { + public init() {} + + /// Serializes a ``Message`` into a sequence of bytes. + /// + /// - Parameter message: The message to serialize. + /// - Returns: An array of serialized bytes representing the message. + public func serialize(_ message: Message) throws -> [UInt8] { + do { + let data = try message.serializedData() + return Array(data) + } catch let error { + throw RPCError( + code: .invalidArgument, + message: "Can't serialize message of type \(type(of: message)).", + cause: error + ) + } + } +} + +/// Deserializes a sequence of bytes into a Protobuf message. +public struct ProtobufDeserializer: GRPCCore.MessageDeserializer { + public init() {} + + /// Deserializes a sequence of bytes into a ``Message``. + /// + /// - Parameter serializedMessageBytes: The array of bytes to deserialize. + /// - Returns: The deserialized message. + public func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message { + do { + let message = try Message(contiguousBytes: serializedMessageBytes) + return message + } catch let error { + throw RPCError( + code: .invalidArgument, + message: "Can't deserialize to message of type \(Message.self)", + cause: error + ) + } + } +} diff --git a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift new file mode 100644 index 000000000..1c657848c --- /dev/null +++ b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift @@ -0,0 +1,98 @@ +/* + * 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 GRPCCore +import GRPCProtobuf +import SwiftProtobuf +import XCTest + +final class ProtobufCodingTests: XCTestCase { + func testSerializeDeserializeRoundtrip() throws { + let message = Google_Protobuf_Timestamp.with { + $0.seconds = 4 + } + + let serializer = ProtobufSerializer() + let deserializer = ProtobufDeserializer() + + let bytes = try serializer.serialize(message) + let roundTrip = try deserializer.deserialize(bytes) + XCTAssertEqual(roundTrip, message) + } + + func testSerializerError() throws { + let message = TestMessage() + let serializer = ProtobufSerializer() + + XCTAssertThrowsError( + try serializer.serialize(message) + ) { error in + XCTAssertEqual( + error as? RPCError, + RPCError( + code: .invalidArgument, + message: + """ + Can't serialize message of type TestMessage. + """ + ) + ) + } + } + + func testDeserializerError() throws { + let bytes = Array("%%%%%££££".utf8) + let deserializer = ProtobufDeserializer() + XCTAssertThrowsError( + try deserializer.deserialize(bytes) + ) { error in + XCTAssertEqual( + error as? RPCError, + RPCError( + code: .invalidArgument, + message: + """ + Can't deserialize to message of type TestMessage + """ + ) + ) + } + } +} + +struct TestMessage: SwiftProtobuf.Message { + var text: String = "" + var unknownFields = SwiftProtobuf.UnknownStorage() + static var protoMessageName: String = "Test" + ".ServiceRequest" + init() {} + + mutating func decodeMessage(decoder: inout D) throws where D: SwiftProtobuf.Decoder { + throw RPCError(code: .internalError, message: "Decoding error") + } + + func traverse(visitor: inout V) throws where V: SwiftProtobuf.Visitor { + throw RPCError(code: .internalError, message: "Traversing error") + } + + public var isInitialized: Bool { + if self.text.isEmpty { return false } + return true + } + + func isEqualTo(message: SwiftProtobuf.Message) -> Bool { + return false + } +}