最終的には、Swift のコードをパースして、mermaid の classDiagram を出力するのがゴールです。
Sponsor Link
環境&対象
- macOS14.3 Beta
- Xcode 15.2
- iOS 17.2
- Swift 5.9
作りたい Plugin
指定した Package/Project のクラス構造を mermaid 形式で出力するプラグインを作ってみます。
全体の予定
Hatch.Symbol から mermaid フォーマットへの変換
前回作成したメソッドで、Swift のコードから、class/struct/enum/protocol/actor の名称を取得することができるようになりました。
今回は、名称を取り出して mermaid フォーマットに変換するようにしてみます。
変換方針
将来的には 名称以外にも、メソッドや継承関係を取り出したくなりますが、まずは、シンプルに 定義されている class/struct/enum/protocol/actor をクラス図に表示することを目指します。
具体的には、”Class1″ という名称のクラスが見つかったときに、以下のようなクラス図を作りたいということです。
classDiagram class Class1
上記を mermaid で書くとすると以下のようになります。(使用する場所によって、適切な HTML タグで囲ったりすることが必要となります。)
classDiagram
class Class1
ただ、mermaid (というか クラス図)には、class/struct等 が基本的に同じ矩形で表現されるため、わかりにくいです。
ということで、追加情報(annotation) を使って、class であることを記述することにします。
class である Class1 に対しては、以下のように class であることを annotation を使って、表示します。
## Web 上で Mermaidを使ってうまく表示できなかったので、レンダリング後のイメージを貼ってます・・・ WordPress 難しい・・・
struct/enum/protocol/actor についても同様に annotation を使って、struct/enum/protocol/actor である旨 追記します。
実装方針
Hatch.Symbol は、protocol であって、データを表す struct としては、ProtocolType, Class, Actor, Struct, Enum に分かれています。
Hatch の実装を見てみると、名称を持つ Symbol は、InheriteingSymbol というプロトコルに conform していることがわかります。
Hatch.Symbol, Hatch.InheritingSymbol に extension を用いて追加実装することで対応してみます。
テストを作る
例によって、テストから書きます。
Struct についてのテストは以下のようにしました。
func test_Struct() async throws {
let struct1 = Hatch.Struct(name: "Struct1", children: [], inheritedTypes: [])
XCTAssertEqual(struct1.mermaidString(), """
class Struct1 {
<<struct>>
}
""")
}
Struct 型のデータから、mermaidString というメソッドで、該当する mermaid フォーマットされた文字列が取得できることとしてテストしています。
同様に、Class / Enum / Protocol / Enum 型について、適切な文字列出力がなされるかをテストします。
func test_Class() async throws {
let class1 = Hatch.Class(name: "Class1", children: [], inheritedTypes: [])
XCTAssertEqual(class1.mermaidString(), """
class Class1 {
<<class>>
}
""")
}
func test_Enum() async throws {
let enum1 = Hatch.Enum(name: "Enum1", children: [], inheritedTypes: [])
XCTAssertEqual(enum1.mermaidString(), """
class Enum1 {
<<enum>>
}
""")
}
func test_Actor() async throws {
let actor1 = Hatch.Actor(name: "Actor1", children: [], inheritedTypes: [])
XCTAssertEqual(actor1.mermaidString(), """
class Actor1 {
<<actor>>
}
""")
}
func test_Protocol() async throws {
let protocol1 = Hatch.ProtocolType(name: "Protocol1", children: [], inheritedTypes: [])
XCTAssertEqual(protocol1.mermaidString(), """
class Protocol1 {
<<protocol>>
}
""")
}
実装する
Hatch.Symbol に extension でmermaidString を定義していきます。
そして、Symbol が Class/Struct/Enum/Actor/ProtocolType のいずれかであるかを文字列で返すような swiftTypeName も定義して 使用しています。
extension Hatch.Symbol {
func mermaidString() -> String {
guard let namedSymbol = self as? Hatch.InheritingSymbol else { return "unsupported type" }
return """
class \(namedSymbol.name) {
<<\(self.swiftTypeName())>>
}
"""
}
func swiftTypeName() -> String {
switch self {
case is Hatch.Struct: return "struct"
case is Hatch.Class: return "class"
case is Hatch.Enum: return "enum"
case is Hatch.Actor: return "actor"
case is Hatch.ProtocolType: return "protocol"
default:
return "UnsupportedType"
}
}
}
¥
コード自体は、特にコメントする必要がないほど、シンプルな実装です。
このコードで、テストをパスするようになりました。
フォルダを変換
前回作成した CommandPluginExampleLib.parseProject というメソッドは、そのフォルダに含まれる Swift コードを解析して、[URL: [Symbol]] というデータを返して来ます。
実装方針
個別の Symbol は、mermaid フォーマットに変換できるようになりましたので、あとは、つなげれば OK のハズです。
ちなみに、mermaid では、クラス図であることを示す “classDiagram” というキーワードを入れることが必要となりますので、先頭に追加しておきます。
さらに、テストしやすさ/デバッグしやすさ(?)も兼ねて、与えられた [URL] に含まれていた URL の lastPathComponent 順に mermaid フォーマットに変換出力するようにしてみます。
さらに デバッグしやすくするために、URL の lastPathComponent も合わせて、出力するようにしてみます。
mermaid では、%% を行頭につけることでコメントになりますので、%% lastPathComponent というような行を追加するということです。
テスト実装
ということで、以下のようなテストにしました。
func test_mermaidString_fromSymbols() async throws {
let testBundleURL = URL(fileURLWithPath: Bundle(for: type(of: self)).bundlePath)
let result = try XCTUnwrap(CommandPluginExampleLib.parseProject(testBundleURL))
XCTAssertEqual(result.count, 5)
let resultString = CommandPluginExampleLib.mermaidString(result)
XCTAssertEqual(resultString, """
classDiagram
%% Actor.swift
class Actor1 {
>
}
%% Class.swift
class Class1 {
>
}
%% Enum.swift
class Enum1 {
>
}
%% Protocol.swift
class Protocol1 {
>
}
%% Struct.swift
class Struct1 {
>
}
""")
}
実装
特に複雑なことを実装するわけではないので、コードは”そのまま”で書けます。
ほとんど1~2行でかけるものもありますが、以下のようにメソッドを分けました。
・フォルダのURLを渡されると、含まれるファイルをパースして [URL:[Symbol] を返す parseProject メソッド
・ファイルのURLを渡されると、パースして含まれるシンボルを返す parseFile メソッド
・[URL: [Symbol]] を与えられると、含まれる シンボルを mermaid 形式の String へ変換する mermaidString メソッド
public struct CommandPluginExampleLib {
public static func parseProject(_ url: URL) throws -> [URL: [Symbol]] {
var results: [URL: [Symbol]] = [:]
let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [URLResourceKey.isDirectoryKey],
options: [.skipsHiddenFiles])
while let fileURL = enumerator?.nextObject() as? URL {
let resources = try fileURL.resourceValues(forKeys: [.isDirectoryKey])
guard let isDir = resources.isDirectory,
isDir == false else { continue }
guard fileURL.pathExtension == "swift" else { continue }
let symbols = try Self.parseFile(fileURL)
results[fileURL] = symbols
}
return results
}
public static func parseFile(_ fileURL: URL) throws -> [Symbol] {
let codeString = try String(contentsOf: fileURL, encoding: .utf8)
return SymbolParser.parse(source: codeString)
}
public static func mermaidString(_ symbolDic: [URL: [Symbol]]) -> String {
var string = "classDiagram\n"
for fileURL in symbolDic.keys.sorted(by: { $0.lastPathComponent $1.lastPathComponent }) {
let fileName = fileURL.lastPathComponent
string += "%% \(fileName)\n"
guard let symbols = symbolDic[fileURL] else { continue }
for symbol in symbols {
string += symbol.mermaidString()
}
}
return string
}
}
変換結果を表示してみる
特に意味はありませんが、結果を表示してみると以下のようになります。
まとめ
swift-syntax/Hatchを使って解析したSwift コードを mermaid フォーマットに変換する
- Hatch で用意されている Symbol/InheritingSymbol を使用すると 型の名称が簡単に取得できる
- mermaid の classDiagram の annotation を使用すると 見た目に class 等の見分けがつきやすい
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Sponsor Link