Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Docs/serialization.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<serialization_register>>), 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Message: Codable>: Serializer<Message> {
// TODO: would be nice to be able to abstract over the coders (using TopLevelDecoder-like types) then rename this to `AnyCodableSerializer`
internal class JSONCodableSerializer<Message: Codable>: Serializer<Message> {
internal let allocate: ByteBufferAllocator
internal var encoder: JSONEncoder
internal var decoder: JSONDecoder
Expand All @@ -45,7 +44,7 @@ public class JSONCodableSerializer<Message: Codable>: Serializer<Message> {

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)
Expand All @@ -63,20 +62,29 @@ public class JSONCodableSerializer<Message: Codable>: Serializer<Message> {
}
}

/// 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<Message: Codable>: Serializer<Message> {
// TODO: would be nice to be able to abstract over the coders (using TopLevelDecoder-like types) then rename this to `AnyCodableSerializer`
internal class PropertyListCodableSerializer<Message: Codable>: Serializer<Message> {
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
}

Expand All @@ -91,10 +99,11 @@ public class PropertyListCodableSerializer<Message: Codable>: Serializer<Message

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)
var format = self.format
return try self.decoder.decode(Message.self, from: data, format: &format)
}

public override func setSerializationContext(_ context: Serialization.Context) {
Expand Down
50 changes: 38 additions & 12 deletions Sources/DistributedActors/Serialization/Serialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,22 @@ extension Serialization {
serializer.setSerializationContext(self.context)
return serializer

case Serialization.SerializerID.foundationPropertyList:
let serializer = PropertyListCodableSerializer<Message>(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<Message>(
allocator: self.allocator,
format: format
)
serializer.setSerializationContext(self.context)
return serializer

Expand Down Expand Up @@ -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)")
Expand Down Expand Up @@ -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)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
76 changes: 54 additions & 22 deletions Tests/DistributedActorsTests/SerializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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<Test> = 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)
}
}

Expand Down Expand Up @@ -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]
}