FileTree#
Describe a directory tree once, then use that description to read, write, and rewrite files as typed Swift values.
FileTree is a small parse/print layer for folder-backed data. It lets a
package define the files and directories it owns, convert raw Data into
domain models, and carry source locations and diagnostics through the read and
write path.
import FileTree
import Foundation
let eventTree = FileTree {
File("event", "txt")
.convert(Conversions.DataToString())
Directory("stages") {
File.Many(withExtension: "txt")
.map(FileContentConversion(Conversions.DataToString()))
}
}
let event = try eventTree.read(from: sourceURL).output.requiredValue
The type of event is inferred from the tree:
(String, [FileContent<String>])
Installation#
Add the package to your Swift package dependencies:
.package(url: "https://tangled.org/woody.fm/swift-file-tree", branch: "main")
Then add FileTree to the targets that need it:
.target(
name: "YourFeature",
dependencies: [
"FileTree",
]
)
Defining Trees#
Use FileTree, Directory, and File to describe the shape of the on-disk
source:
let source = FileTree {
File.Optional("calendar", "txt")
.convert(Conversions.DataToString())
Directory("events") {
Directory.Many {
File("event", "txt")
.convert(Conversions.DataToString())
Directory.Optional("stages") {
File.Many(withExtension: "txt")
.map(FileContentConversion(Conversions.DataToString()))
}
}
}
}
That tree reads a folder like this:
calendar.txt
events/
wicked-woods/
event.txt
stages/
meadow.txt
forest.txt
Multiple children compose into tuples, optional files and directories produce
optional values, and Many components produce arrays of FileContent or
DirectoryContent values.
Conversions#
File and directory components start with raw file-system values, usually
Data. Attach conversions to move between raw data and domain-specific Swift
types:
struct Event: Equatable {
var name: String
var stages: [FileContent<String>]?
}
struct EventConversion: Conversion {
func apply(
_ input: (String, [FileContent<String>]?)
) throws -> Event {
Event(name: input.0, stages: input.1)
}
func unapply(
_ output: Event
) throws -> (String, [FileContent<String>]?) {
(output.name, output.stages)
}
}
let eventModelTree = FileTree {
File("event", "txt")
.convert(Conversions.DataToString())
Directory.Optional("stages") {
File.Many(withExtension: "txt")
.map(FileContentConversion(Conversions.DataToString()))
}
}
.convert(EventConversion())
Conversions are bidirectional: apply is used while reading, and unapply is
used while writing or rewriting.
Writing#
Any tree made entirely of writable components can write the same shape it reads:
try eventTree.write(
(
"Wicked Woods",
[
try FileContent(fileName: "meadow", fileType: "txt", data: "Meadow Stage"),
try FileContent(fileName: "forest", fileType: "txt", data: "Forest Stage"),
]
),
to: sourceURL
)
FileContent validates file names before writing, which keeps generated source
trees from silently producing invalid paths.
Rewriting#
Use rewrite(_:at:) when an edit should preserve context from the existing
source tree. Rewrites first read the source, keep the resulting SourceGraph,
and pass that graph into conversion print operations.
var event = try eventModelTree.read(from: sourceURL).output.requiredValue
event.name = "Wicked Woods Music Festival"
try eventModelTree.rewrite(event, at: sourceURL)
This is the path to use for source-backed editing flows where diagnostics and existing file locations matter.
Diagnostics#
Reads and writes return FileTreeResult, not just a bare value. A result
contains:
- The decoded or printed output.
- A
SourceGraphdescribing the physical and logical source nodes touched. - A diagnostic report that can point back to file paths or logical fields.
let result = try eventTree.read(from: sourceURL)
if result.diagnostics.hasErrors {
for diagnostic in result.diagnostics.diagnostics {
print(diagnostic)
}
}
let value = try result.output.requiredValue
This makes FileTree useful for importers and editors that need to show users
where invalid source data came from.
Requirements#
This package currently targets Swift 6.0 or later, iOS 17 or later, and macOS 15 or later.