diff --git a/Docs/serialization.adoc b/Docs/serialization.adoc index 5330e8d29..5d12add98 100644 --- a/Docs/serialization.adoc +++ b/Docs/serialization.adoc @@ -52,6 +52,10 @@ NOTE: While messages locally are passed simply by storing the passed message in ==== Selecting the default Codable Coders +NOTE: By default, Foundation's JSON and PropertyList (binary or xml) coders are supported. ++ +You can configure which coder should be used by default by setting `settings.serialization.defaultSerializerID`. + The ActorSystem's serialization infrastructure allows for specifying what coder should be used for certain messages. If a type is safe to be serialized (see <>), it will be serialized using the serializer associated with its type. If no serializer is specified for its specific type, the default Codable coder will be used. diff --git a/Sources/DistributedActors/Serialization/Serialization+Codable.swift b/Sources/DistributedActors/Serialization/Serialization+Codable.swift index e55c2db6a..a28a22810 100644 --- a/Sources/DistributedActors/Serialization/Serialization+Codable.swift +++ b/Sources/DistributedActors/Serialization/Serialization+Codable.swift @@ -34,13 +34,14 @@ extension Decodable { } extension Decodable { - static func _decode(from buffer: inout NIO.ByteBuffer, using decoder: PropertyListDecoder) throws -> Self { + static func _decode(from buffer: inout NIO.ByteBuffer, using decoder: PropertyListDecoder, format _format: PropertyListSerialization.PropertyListFormat) throws -> Self { let readableBytes = buffer.readableBytes return try buffer.withUnsafeMutableReadableBytes { // we are getting the pointer from a ByteBuffer, so it should be valid and force unwrap should be fine let data = Data(bytesNoCopy: $0.baseAddress!, count: readableBytes, deallocator: .none) - return try decoder.decode(Self.self, from: data) + var format = _format + return try decoder.decode(Self.self, from: data, format: &format) } } } diff --git a/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift b/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift index 616e8c5c7..a4a4fe152 100644 --- a/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift +++ b/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift @@ -31,14 +31,16 @@ extension Serialization { switch self.value { case SerializerID.doNotSerialize.value: return "serializerID:doNotSerialize(\(self.value))" + case SerializerID.protobufRepresentable.value: + return "serializerID:protobufRepresentable(\(self.value))" case SerializerID.specializedWithTypeHint.value: return "serializerID:specialized(\(self.value))" case SerializerID.foundationJSON.value: return "serializerID:jsonCodable(\(self.value))" - case SerializerID.foundationPropertyList.value: - return "serializerID:plistCodable(\(self.value))" - case SerializerID.protobufRepresentable.value: - return "serializerID:protobufRepresentable(\(self.value))" + case SerializerID.foundationPropertyListBinary.value: + return "serializerID:foundationPropertyListBinary(\(self.value))" + case SerializerID.foundationPropertyListXML.value: + return "serializerID:foundationPropertyListXML(\(self.value))" default: return "serializerID:\(self.value)" } @@ -66,10 +68,12 @@ extension Serialization.SerializerID { public static let doNotSerialize: SerializerID = 0 public static let specializedWithTypeHint: SerializerID = 1 - public static let foundationJSON: SerializerID = 2 - public static let foundationPropertyList: SerializerID = 3 - public static let protobufRepresentable: SerializerID = 4 - // ... reserved = 5 + public static let protobufRepresentable: SerializerID = 2 + + public static let foundationJSON: SerializerID = 3 + public static let foundationPropertyListBinary: SerializerID = 4 + public static let foundationPropertyListXML: SerializerID = 5 + // ... reserved = 6 // ... -- || -- // ... reserved = 16 diff --git a/Sources/DistributedActors/Serialization/Serialization+Serializers+Codable.swift b/Sources/DistributedActors/Serialization/Serialization+Serializers+Codable.swift index c9d52f84d..f86b25f11 100644 --- a/Sources/DistributedActors/Serialization/Serialization+Serializers+Codable.swift +++ b/Sources/DistributedActors/Serialization/Serialization+Serializers+Codable.swift @@ -17,13 +17,12 @@ import NIOFoundationCompat import Foundation // for Codable -/// Allows for serialization of messages using any compatible `Encoder` and `Decoder` pair. -/// -/// Such serializer may be registered with `Serialization` and assigned either as default (see `Serialization.Settings +/// Allows for serialization of messages using the Foundation's `JSONEncoder` and `JSONDecoder`. /// /// - Note: Take care to ensure that both "ends" (sending and receiving members of a cluster) /// use the same encoding/decoding mechanism for a specific message. -public class JSONCodableSerializer: Serializer { +// TODO: would be nice to be able to abstract over the coders (using TopLevelDecoder-like types) then rename this to `AnyCodableSerializer` +internal class JSONCodableSerializer: Serializer { internal let allocate: ByteBufferAllocator internal var encoder: JSONEncoder internal var decoder: JSONDecoder @@ -45,7 +44,7 @@ public class JSONCodableSerializer: Serializer { public override func deserialize(from bytes: ByteBuffer) throws -> Message { guard let data = bytes.getData(at: 0, length: bytes.readableBytes) else { - fatalError("Could not read data! Was: \(bytes), trying to deserialize: \(Message.self)") + throw SerializationError.unableToDeserialize(hint: "Could not read data! Was: \(bytes), trying to deserialize: \(Message.self)") } return try self.decoder.decode(Message.self, from: data) @@ -63,20 +62,29 @@ public class JSONCodableSerializer: Serializer { } } -/// Allows for serialization of messages using any compatible `Encoder` and `Decoder` pair. -/// -/// Such serializer may be registered with `Serialization` and assigned either as default (see `Serialization.Settings +/// Allows for serialization of messages using the Foundation's `PropertyListEncoder` and `PropertyListDecoder`, using the specified format. /// /// - Note: Take care to ensure that both "ends" (sending and receiving members of a cluster) /// use the same encoding/decoding mechanism for a specific message. -public class PropertyListCodableSerializer: Serializer { +// TODO: would be nice to be able to abstract over the coders (using TopLevelDecoder-like types) then rename this to `AnyCodableSerializer` +internal class PropertyListCodableSerializer: Serializer { internal let allocate: ByteBufferAllocator - internal var encoder: PropertyListEncoder - internal var decoder: PropertyListDecoder + internal let encoder: PropertyListEncoder + internal let decoder: PropertyListDecoder + internal let format: PropertyListSerialization.PropertyListFormat + + public init(allocator: ByteBufferAllocator, format: PropertyListSerialization.PropertyListFormat) { + self.allocate = allocator + self.format = format + self.encoder = PropertyListEncoder() + self.encoder.outputFormat = format + self.decoder = PropertyListDecoder() + } public init(allocator: ByteBufferAllocator, encoder: PropertyListEncoder = .init(), decoder: PropertyListDecoder = .init()) { self.allocate = allocator self.encoder = encoder + self.format = encoder.outputFormat self.decoder = decoder } @@ -91,10 +99,11 @@ public class PropertyListCodableSerializer: Serializer Message { guard let data = bytes.getData(at: 0, length: bytes.readableBytes) else { - fatalError("Could not read data! Was: \(bytes), trying to deserialize: \(Message.self)") + throw SerializationError.unableToDeserialize(hint: "Could not read data! Was: \(bytes), trying to deserialize: \(Message.self)") } - return try self.decoder.decode(Message.self, from: data) + var format = self.format + return try self.decoder.decode(Message.self, from: data, format: &format) } public override func setSerializationContext(_ context: Serialization.Context) { diff --git a/Sources/DistributedActors/Serialization/Serialization.swift b/Sources/DistributedActors/Serialization/Serialization.swift index 5bd3832ba..efeb663ea 100644 --- a/Sources/DistributedActors/Serialization/Serialization.swift +++ b/Sources/DistributedActors/Serialization/Serialization.swift @@ -306,8 +306,22 @@ extension Serialization { serializer.setSerializationContext(self.context) return serializer - case Serialization.SerializerID.foundationPropertyList: - let serializer = PropertyListCodableSerializer(allocator: self.allocator) + case Serialization.SerializerID.foundationPropertyListBinary, + Serialization.SerializerID.foundationPropertyListXML: + let format: PropertyListSerialization.PropertyListFormat + switch manifest.serializerID { + case Serialization.SerializerID.foundationPropertyListBinary: + format = .binary + case Serialization.SerializerID.foundationPropertyListXML: + format = .xml + default: + fatalError("Unable to make PropertyList serializer for serializerID: \(manifest.serializerID); type: \(String(reflecting: Message.self))") + } + + let serializer = PropertyListCodableSerializer( + allocator: self.allocator, + format: format + ) serializer.setSerializationContext(self.context) return serializer @@ -376,20 +390,27 @@ extension Serialization { """ ) + case .protobufRepresentable: + let encoder = TopLevelProtobufBlobEncoder(allocator: self.allocator) + encoder.userInfo[.actorSerializationContext] = self.context + result = try encodableMessage._encode(using: encoder) + case .foundationJSON: let encoder = JSONEncoder() encoder.userInfo[.actorSerializationContext] = self.context result = try encodableMessage._encode(using: encoder, allocator: self.allocator) - case .foundationPropertyList: + case .foundationPropertyListBinary: let encoder = PropertyListEncoder() + encoder.outputFormat = .binary encoder.userInfo[.actorSerializationContext] = self.context result = try encodableMessage._encode(using: encoder, allocator: self.allocator) - case .protobufRepresentable: - let encoder = TopLevelProtobufBlobEncoder(allocator: self.allocator) + case .foundationPropertyListXML: + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml encoder.userInfo[.actorSerializationContext] = self.context - result = try encodableMessage._encode(using: encoder) + result = try encodableMessage._encode(using: encoder, allocator: self.allocator) case let otherSerializerID: throw SerializationError.unableToMakeSerializer(hint: "SerializerID: \(otherSerializerID), messageType: \(messageType), manifest: \(manifest)") @@ -484,20 +505,25 @@ extension Serialization { Known specializedSerializerMakers: \(self.settings.specializedSerializerMakers) """ ) + + case .protobufRepresentable: + let decoder = TopLevelProtobufBlobDecoder() + decoder.userInfo[.actorSerializationContext] = self.context + result = try decodableMessageType._decode(from: &bytes, using: decoder) + case .foundationJSON: let decoder = JSONDecoder() decoder.userInfo[.actorSerializationContext] = self.context result = try decodableMessageType._decode(from: &bytes, using: decoder) - case .foundationPropertyList: + case .foundationPropertyListBinary: let decoder = PropertyListDecoder() decoder.userInfo[.actorSerializationContext] = self.context - result = try decodableMessageType._decode(from: &bytes, using: decoder) - - case .protobufRepresentable: - let decoder = TopLevelProtobufBlobDecoder() + result = try decodableMessageType._decode(from: &bytes, using: decoder, format: .binary) + case .foundationPropertyListXML: + let decoder = PropertyListDecoder() decoder.userInfo[.actorSerializationContext] = self.context - result = try decodableMessageType._decode(from: &bytes, using: decoder) + result = try decodableMessageType._decode(from: &bytes, using: decoder, format: .xml) case let otherSerializerID: throw SerializationError.unableToMakeSerializer(hint: "SerializerID: \(otherSerializerID), messageType: \(manifestMessageType), manifest: \(manifest)") diff --git a/Sources/DistributedActors/Serialization/TopLevelProtobufCoders.swift b/Sources/DistributedActors/Serialization/TopLevelProtobufCoders.swift index 261b89a5b..07a7d0c30 100644 --- a/Sources/DistributedActors/Serialization/TopLevelProtobufCoders.swift +++ b/Sources/DistributedActors/Serialization/TopLevelProtobufCoders.swift @@ -227,7 +227,7 @@ struct TopLevelProtobufBlobSingleValueDecodingContainer: SingleValueDecodingCont if type is Data.Type { guard let data = bytes.getData(at: 0, length: bytes.readableBytes) else { - fatalError("Could not read data! Was: \(bytes), trying to deserialize: \(T.self)") + throw SerializationError.unableToDeserialize(hint: "Could not read data! Was: \(bytes), trying to deserialize: \(T.self)") } return data as! T } else if type is NIO.ByteBuffer.Type { diff --git a/Tests/DistributedActorsTests/SerializationTests.swift b/Tests/DistributedActorsTests/SerializationTests.swift index 5fa5f8e70..fab8ee09a 100644 --- a/Tests/DistributedActorsTests/SerializationTests.swift +++ b/Tests/DistributedActorsTests/SerializationTests.swift @@ -28,6 +28,9 @@ class SerializationTests: ActorSystemTestBase { settings.serialization.register(HasIntRef.self) settings.serialization.register(HasInterestingMessageRef.self) settings.serialization.register(CodableTestingError.self) + + settings.serialization.register(PListBinCodableTest.self, serializerID: .foundationPropertyListBinary) + settings.serialization.register(PListXMLCodableTest.self, serializerID: .foundationPropertyListXML) } } @@ -349,34 +352,53 @@ class SerializationTests: ActorSystemTestBase { codableTestingError.shouldEqual(codableError) } - func test_plist() throws { - struct Test: Codable, Equatable { - let name: String - let items: [String] + // ==== ---------------------------------------------------------------------------------------------------------------- + // MARK: PList coding + + func test_plist_binary() throws { + let test = PListBinCodableTest(name: "foo", items: ["bar", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz", "baz"]) + + var (manifest, bytes) = try! shouldNotThrow { + try system.serialization.serialize(test) } - let s2 = ActorSystem("SerializeMessages") { settings in - settings.serialization.serializeLocalMessages = true - settings.serialization.register(Test.self, serializerID: .foundationPropertyList) + let back = try! system.serialization.deserialize(as: PListBinCodableTest.self, from: &bytes, using: manifest) + + back.shouldEqual(test) + } + + func test_plist_xml() throws { + let test = PListXMLCodableTest(name: "foo", items: ["bar", "baz"]) + + var (manifest, bytes) = try shouldNotThrow { + try system.serialization.serialize(test) } - do { - let p = self.testKit.spawnTestProbe("p1", expecting: Test.self) - let echo: ActorRef = try s2.spawn( - "echo", - .receiveMessage { msg in - p.ref.tell(Test(name: "echo:\(msg.name)", items: msg.items.map { "echo:\($0)" })) - return .same - } - ) + let back = try system.serialization.deserialize(as: PListXMLCodableTest.self, from: &bytes, using: manifest) - echo.tell(Test(name: "foo", items: ["bar", "baz"])) // is a built-in serializable message - try p.expectMessage(Test(name: "echo:foo", items: ["echo:bar", "echo:baz"])) - } catch { - s2.shutdown().wait() - throw error + back.shouldEqual(test) + } + + func test_plist_throws_whenWrongFormat() throws { + let test = PListXMLCodableTest(name: "foo", items: ["bar", "baz"]) + + var (manifest, bytes) = try shouldNotThrow { + try system.serialization.serialize(test) } - s2.shutdown().wait() + + let system2 = ActorSystem("OtherSystem") { settings in + settings.serialization.register(PListXMLCodableTest.self, serializerID: .foundationPropertyListBinary) // on purpose "wrong" format + } + defer { + system2.shutdown().wait() + } + + _ = shouldThrow { + _ = try system2.serialization.deserialize(as: PListXMLCodableTest.self, from: &bytes, using: manifest) + } + + let back = try system.serialization.deserialize(as: PListXMLCodableTest.self, from: &bytes, using: manifest) + back.shouldEqual(test) } } @@ -467,3 +489,13 @@ private enum CodableTestingError: String, Error, Equatable, Codable { case errorA case errorB } + +private struct PListBinCodableTest: Codable, Equatable { + let name: String + let items: [String] +} + +private struct PListXMLCodableTest: Codable, Equatable { + let name: String + let items: [String] +}