Swiftで書いたMessagePackライブラリの紹介
MessagePackライブラリのswift-msgpackをgithubに公開しました。
ここでは、swift-msgpack
について紹介します。このライブラリはCodableを利用したエンコーダ&デコーダのため、標準ライブラリのJSONEncoder/JSONDecooderと同様に出力をカスタムすることが出来ます。
arrayにするかmapにするか
swiftのstructかclassをMessagePackにエンコードすると、arrayにするかmapにするか任意性が存在します。swiftコンパイラが自動生成するCodableのコードはmapにしますが、自分でコードを書くことでarrayにすることもできます。まずmapの場合のコードを次に示します。
struct Pair: Codable, Equatable {
var X: Int
var Y: Int
}
let input = Pair(X: 1, Y: 2)
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(Pair.self, from: data)
print(data.hexDescription)// => "82a15801a15902"
assert(out == input)
arrayにエンコードし、arrayからデコードできるように書き換えてみます。
struct Pair: Equatable {
var X: Int
var Y: Int
}
extension Pair: Codable {
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
X = try container.decode(Int.self)
Y = try container.decode(Int.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(X)
try container.encode(Y)
}
}
mapのキーの名前を変更する
これはJSONEncoder/JSONDecoderの場合と同じですが、次のようにします。
struct Pair: Codable, Equatable {
var X: Int
var Y: Int
private enum CodingKeys: String, CodingKey {
case X = "x"
case Y = "y"
}
}
Timestamp
MessagePackには拡張型としてTimestampが定義されています。拡張型は拡張タイプという番号で識別されます。MessagePackとして定義されるものは-1
から-128
が割り当て可能で、アプリケーション独自に使える拡張タイプは0
から127
です。Timestampの拡張タイプは-1
です。
MsgPackTimestamp
として定義しています。Date
への変換はライブラリ側としては用意していませんが、必要に応じてDateのextensionとして定義すれば良いと考えています。簡単な例は次の通りです。
let format = ISO8601DateFormatter()
format.timeZone = .init(identifier: "UTC")
format.formatOptions.insert(.withFractionalSeconds)
let date = format.date(from: "2022-06-22T08:56:32.999Z")!
let timeInterval = date.timeIntervalSince1970
let seconds = Int64(timeInterval)
let nanoseconds = Int32(timeInterval.truncatingRemainder(dividingBy: 1) * 1_000_000_000)
let input = MsgPackTimestamp(seconds: seconds, nanoseconds: nanoseconds)
print(input)// => MsgPackTimestamp(seconds: 1655888192, nanoseconds: 999000072)
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(MsgPackTimestamp.self, from: data)
print(data.hexDescription) // => "d7ffee2e202062b2d940"
assert(out == input)
拡張型の定義
MsgPackEncodable
プロトコルを実装するとエンコードできるようになり、MsgPackDecodable
を実装するとデコードできるようになります。MsgPackCodable
はMsgPackEncodable & MsgPackDecodable
のエイリアスになっています。また、上で説明したように拡張タイプ番号を決める必要があります。
public protocol MsgPackDecodable: Decodable {
var type: Int8 { get }
init(msgPack data: Data) throws
}
public protocol MsgPackEncodable: Encodable {
func encodeMsgPack() throws -> Data
var type: Int8 { get }
}
簡単な例を挙げておきます。
struct Position: MsgPackCodable, Equatable {
init(msgPack data: Data) throws {
let a = [UInt8](data)
x = a[0]
y = a[1]
}
private var x: UInt8
private var y: UInt8
init(x: UInt8, y: UInt8) {
self.x = x
self.y = y
}
func encodeMsgPack() throws -> Data {
.init([x, y])
}
var type: Int8 { 1 }
}
let input: Position = .init(x: 34, y: 78)
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(Position.self, from: data)
print(data.hexDescription) // => "d501224e"
assert(out == input)
サブクラスの場合
クラスをCodableにして、さらにスーパークラスがある場合はsuperDecoder/superEncoder
を呼び出すと、うまく変換できます。この辺りはCodableを使ったEncoder/Decoder一般に言えることですが、このようなケースにも対応しています。
class Animal: Codable {
var legCount: Int
init(legCount: Int) {
self.legCount = legCount
}
func equals(rhs: Animal) -> Bool {
return legCount == rhs.legCount
}
}
extension Animal: Equatable {
static func == (lhs: Animal, rhs: Animal) -> Bool {
return lhs.equals(rhs: rhs)
}
}
class Pet: Animal {
var name: String
private enum CodingKeys: String, CodingKey {
case name
}
required init(legCount: Int, name: String) {
self.name = name
super.init(legCount: legCount)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
let superDecoder = try container.superDecoder()
try super.init(from: superDecoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
let superEncoder = container.superEncoder()
try super.encode(to: superEncoder)
}
override func equals(rhs: Animal) -> Bool {
guard let rhs = rhs as? Pet else { return false }
return super.equals(rhs: rhs) && name == rhs.name
}
}
let input = Pet(legCount: 4, name: "siro")
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(Pet.self, from: data)
print(data.hexDescription) // => "82a46e616d65a47369726fa5737570657281a86c6567436f756e7404"
assert(out == input)
Directory
DirectoryのキーがCodableであれば、mapにエンコーディングされます。
let input: [Double: String] = [3.14159265359: "pi", 1.41421356237: "sqrt(2)"]
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode([Double: String].self, from: data)
print(data.hexDescription) // "82cb400921fb54442eeaa27069cb3ff6a09e667f055aa773717274283229"
assert(input == out)
structでnilを含む場合
swift的にはnilの場合はデコードの時に使わないので、エンコードでも何も出力しないという選択肢があります。Codableを使ったエンコーダ&デコーダの標準的な動作としてもそのようになっていますが、出力が必要な言語と通信している場合は詰んでしまうのであえて出力するようにしています。この辺りはオプションにできると良いかもしれません。
struct NilArray: Codable, Equatable {
var a: [String]?
}
let input = NilArray(a: nil)
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(NilArray.self, from: data)
print(data.hexDescription) // == "81a161c0"
assert(input == out)
Discussion