🎞️
SwiftProtobufPluginの使い方
概要
本記事では、SwiftでProtobufを扱う際に、CLIツールを使わずにSwift Package Managerのプラグイン機能を利用してコード生成を行う手順を紹介します。swift-protobuf
を利用すると、依存関係とプラグインの設定だけで簡単にProtobufファイルをSwiftコードに変換できるので、その方法を備忘録としてまとめました。
結論
Package.swift
のdependencies
とplugins
セクションに、swift-protobuf
を追加するだけでProtobufコードの自動生成が可能になります。
以下のように依存を追加します.
Package.swift
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "YourPackage",
platforms: [.macOS(.v15)],
dependencies: [
+ .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.0.0")
],
targets: [
.executableTarget(
name: "YourPackage",
dependencies: [
+ .product(name: "SwiftProtobuf", package: "swift-protobuf"),
],
plugins: [
+ .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf"),
]
)
]
)
設定ファイル
生成ターゲットのディレクトリ直下(例:Source
ディレクトリ配下、あるいはSource/App
ディレクトリ配下など)に、以下のような設定ファイルを用意します。ここで指定したprotoファイルが、自動的にSwiftコードへ変換されます。
Source/swift-protobuf-config.json
{
"invocations": [
{
"protoFiles": [
"journal.proto"
],
"visibility": "public"
}
]
}
設定ファイルの型
下記のような型定義に沿ったJSONを書けば、他のオプションに関しても設定できます。
interface SwiftProtobufConfig {
/** protocバイナリへのパス */
protocPath?: string
/** 実施一覧 */
invocations: {
/** 生成元protoファイル一覧 */
protoFiles: string[]
/** 生成コードのアクセス修飾子レベル(デフォルトはinternal) */
visibility: "internal" | "public" | "package"
/** 生成ファイルの命名パターン(デフォルトはFullPath) */
fileNaming?: "FullPath" | "PathToUnderscores" | "DropPath"
/**
* trueに設定すると、内部のみで使用するモジュールに対して@_implementationOnly importを付与する。
* 外部に公開しない型の取り扱いを容易にし、バイナリサイズの肥大化を抑えられる場合がある。
*/
implementationOnlyImports?: boolean
/**
* import文の前にアクセス修飾子(public/internalなど)を付与するかどうか
*/
useAccessLevelOnImports?: boolean
}[]
}
使用例
.proto
ファイル例
Source/journal.proto
syntax = "proto3";
package sample;
import "google/protobuf/timestamp.proto";
message AccountEvent {
oneof content {
AccountCreated created = 1;
AccountRenamed renamed = 2;
AccountDeleted deleted = 3;
}
}
message AccountCreated {
string name = 1;
string aid = 2;
uint32 seq_nr = 3;
google.protobuf.Timestamp occurred_at = 4;
bool is_created = 5;
}
message AccountRenamed {
string name = 1;
string aid = 2;
uint32 seq_nr = 3;
google.protobuf.Timestamp occurred_at = 4;
bool is_created = 5;
}
message AccountDeleted {
string aid = 2;
uint32 seq_nr = 3;
google.protobuf.Timestamp occurred_at = 4;
bool is_created = 5;
}
Swiftコード例
自動生成されるProtobuf用の型(Sample_AccountEvent
など)を活用すると、次のようにエンコード・デコードを行うことができます。
Source/main.swift
import Foundation
import SwiftProtobuf
enum AccountEvent: Sendable, Hashable, Codable {
case created(Created)
case renamed(Renamed)
case deleted(Deleted)
struct Created: Sendable, Hashable, Codable {
var aid: String
var name: String
var seqNr: Int
var occurredAt: Date
var isCreated: Bool
}
struct Renamed: Sendable, Hashable, Codable {
var aid: String
var name: String
var seqNr: Int
var occurredAt: Date
var isCreated: Bool
}
struct Deleted: Sendable, Hashable, Codable {
var aid: String
var seqNr: Int
var occurredAt: Date
var isCreated: Bool
}
func protobufData() throws -> Data {
var result = Sample_AccountEvent()
switch self {
case .created(let event):
var created = Sample_AccountCreated()
created.aid = event.aid
created.name = event.name
created.seqNr = UInt32(event.seqNr)
created.occurredAt = .init(date: event.occurredAt)
created.isCreated = event.isCreated
result.created = created
case .renamed(let event):
var renamed = Sample_AccountRenamed()
renamed.aid = event.aid
renamed.name = event.name
renamed.seqNr = UInt32(event.seqNr)
renamed.occurredAt = .init(date: event.occurredAt)
renamed.isCreated = event.isCreated
result.renamed = renamed
case .deleted(let event):
var deleted = Sample_AccountDeleted()
deleted.aid = event.aid
deleted.seqNr = UInt32(event.seqNr)
deleted.occurredAt = .init(date: event.occurredAt)
deleted.isCreated = event.isCreated
result.deleted = deleted
}
return try result.serializedData()
}
init(protobuf data: Data) throws {
let protobuf = try Sample_AccountEvent(serializedBytes: data)
guard let content = protobuf.content else {
throw ProtobufDecodingError.valueNotFound(protobuf)
}
switch content {
case .created(let created):
self = .created(.init(
aid: created.aid,
name: created.name,
seqNr: Int(created.seqNr),
occurredAt: created.occurredAt.date,
isCreated: created.isCreated
))
case .renamed(let renamed):
self = .renamed(.init(
aid: renamed.aid,
name: renamed.name,
seqNr: Int(renamed.seqNr),
occurredAt: renamed.occurredAt.date,
isCreated: renamed.isCreated
))
case .deleted(let deleted):
self = .deleted(.init(
aid: deleted.aid,
seqNr: Int(deleted.seqNr),
occurredAt: deleted.occurredAt.date,
isCreated: deleted.isCreated
))
}
}
}
enum ProtobufDecodingError: Error {
case valueNotFound(any Sendable)
}
// 実際にエンコード・デコードしてみる
let event: AccountEvent = .created(
.init(
aid: "hello",
name: "Hello",
seqNr: 1,
occurredAt: .init(),
isCreated: true
)
)
print(event)
let data = try event.protobufData()
print("💚 Proto Buffers")
print(data)
print(data.base64EncodedString())
print("💚 Proto Buffers")
print(try AccountEvent(protobuf: data))
まとめ
-
swift-protobuf
をPackage.swift
のdependencies
とplugins
に追加し、swift-protobuf-config.json
を用意するだけで、Protobufファイルのコンパイルを自動化できます。 - わざわざCLIを叩かなくても、Swift Package Managerでビルドするたびにコード生成が実行されるので、手軽にProtobufを導入できます。
以上が、Swift Package Pluginを利用したProtobufコード生成方法の概要です。設定ファイルで細かいオプションを管理できるので、プロジェクト規模や運用方針に合わせて活用してみてください。
Discussion