alpha
Login
or
Join now
woody.fm
/
swift-file-tree
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
A type safe swift ResultBuilder DSL for structured directories
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
Add optional file writing support
author
Woodrow Melling
date
1 month ago
(Jun 11, 2026, 5:12 PM -0600)
commit
9a3f2dc4
9a3f2dc43a2a7a2df0690080c3a571fbfd975033
parent
c2111ea4
c2111ea4a650b8bc293c027e5c3c187e3de1f578
change-id
soltwlqv
soltwlqvyzlxusqtqsmrtrzsxrkunswm
+216
-3
5 changed files
Expand all
Collapse all
Unified
Split
Sources
FileTree
Components
Directory.swift
File.swift
Conversions.swift
Tests
FileTreeTests
RewriteTests.swift
TupleWriteTests.swift
+9
-1
Sources/FileTree/Components/Directory.swift
View file
Reviewed
···
91
91
@TaskLocal
92
92
public var writingToEmptyDirectory = false
93
93
94
94
+
@TaskLocal
95
95
+
public var deletingNilOptionalWrites = false
96
96
+
94
97
95
98
extension Directory {
96
99
public struct Many: FileTreeReader {
···
314
317
to url: URL,
315
318
context: FileTreeWriteContext
316
319
) throws -> FileTreeResult<Component.Content?> {
320
320
+
let directoryURL = url.appending(component: self.path.description)
321
321
+
317
322
guard let content else {
323
323
+
if deletingNilOptionalWrites,
324
324
+
FileManager.default.fileExists(atPath: directoryURL.path()) {
325
325
+
try FileManager.default.removeItem(at: directoryURL)
326
326
+
}
318
327
return .value(nil)
319
328
}
320
329
321
321
-
let directoryURL = url.appending(component: self.path.description)
322
330
if !FileManager.default.fileExists(atPath: directoryURL.path()) {
323
331
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: false)
324
332
}
+27
-1
Sources/FileTree/Components/File.swift
View file
Reviewed
···
160
160
161
161
162
162
extension File {
163
163
-
public struct Optional: FileTreeReader {
163
163
+
public struct Optional: FileTreeReader, FileTreeWriter {
164
164
public typealias Content = Data?
165
165
166
166
public init(_ fileName: StaticString, _ fileType: FileExtension) {
···
180
180
do {
181
181
return .value(
182
182
try Data(contentsOf: fileUrl),
183
183
+
graph: .node(.file(SourceGraph.Path(rawValue: "\(fileName.description).\(fileType.rawValue)")))
184
184
+
)
185
185
+
} catch {
186
186
+
throw Error(fileName: self.fileName.description, fileType: self.fileType, error: error)
187
187
+
}
188
188
+
}
189
189
+
190
190
+
public func write(_ data: Data?, to url: URL) throws -> FileTreeResult<Data?> {
191
191
+
let fileUrl = url.appendingPathComponent(fileName.description, withType: fileType)
192
192
+
193
193
+
guard let data else {
194
194
+
do {
195
195
+
if deletingNilOptionalWrites,
196
196
+
FileManager.default.fileExists(atPath: fileUrl.path()) {
197
197
+
try FileManager.default.removeItem(at: fileUrl)
198
198
+
}
199
199
+
return .value(nil)
200
200
+
} catch {
201
201
+
throw Error(fileName: self.fileName.description, fileType: self.fileType, error: error)
202
202
+
}
203
203
+
}
204
204
+
205
205
+
do {
206
206
+
try data.write(to: fileUrl)
207
207
+
return .value(
208
208
+
data,
183
209
graph: .node(.file(SourceGraph.Path(rawValue: "\(fileName.description).\(fileType.rawValue)")))
184
210
)
185
211
} catch {
+46
Sources/FileTree/Conversions.swift
View file
Reviewed
···
585
585
public typealias Content = Downstream.Output?
586
586
}
587
587
588
588
+
extension _OptionalConvertedFileTreeReader: FileTreeWriter where Upstream: FileTreeWriter {
589
589
+
public func write(
590
590
+
_ content: Downstream.Output?,
591
591
+
to url: URL
592
592
+
) throws -> FileTreeResult<Downstream.Output?> {
593
593
+
try write(content, to: url, context: .empty)
594
594
+
}
595
595
+
596
596
+
public func write(
597
597
+
_ content: Downstream.Output?,
598
598
+
to url: URL,
599
599
+
context: FileTreeWriteContext
600
600
+
) throws -> FileTreeResult<Downstream.Output?> {
601
601
+
let conversionContext = context.contextForUpstream(upstream, at: url)
602
602
+
603
603
+
guard let content else {
604
604
+
let upstreamResult = try upstream.write(nil, to: url, context: conversionContext)
605
605
+
switch upstreamResult.output {
606
606
+
case .value where !upstreamResult.diagnostics.hasErrors:
607
607
+
return .value(nil, graph: upstreamResult.graph, diagnostics: upstreamResult.diagnostics)
608
608
+
case .value, .invalid:
609
609
+
return .invalid(graph: upstreamResult.graph, diagnostics: upstreamResult.diagnostics)
610
610
+
}
611
611
+
}
612
612
+
613
613
+
let conversionResult = downstream.unapply(content, in: conversionContext)
614
614
+
var diagnostics = conversionResult.diagnostics
615
615
+
616
616
+
switch conversionResult.output {
617
617
+
case let .value(upstreamContent) where !diagnostics.hasErrors:
618
618
+
let upstreamResult = try upstream.write(.some(upstreamContent), to: url, context: conversionContext)
619
619
+
diagnostics.append(contentsOf: upstreamResult.diagnostics)
620
620
+
621
621
+
switch upstreamResult.output {
622
622
+
case .value where !diagnostics.hasErrors:
623
623
+
return .value(.some(content), graph: upstreamResult.graph, diagnostics: diagnostics)
624
624
+
case .value, .invalid:
625
625
+
return .invalid(graph: upstreamResult.graph, diagnostics: diagnostics)
626
626
+
}
627
627
+
628
628
+
case .value, .invalid:
629
629
+
return .invalid(diagnostics: diagnostics)
630
630
+
}
631
631
+
}
632
632
+
}
633
633
+
588
634
extension File.Optional {
589
635
public func convert<C>(_ conversion: C) -> _OptionalConvertedFileTreeReader<Self, C>
590
636
where Content == C.Input?, C: Conversion, C.Output: Sendable {
+59
Tests/FileTreeTests/RewriteTests.swift
View file
Reviewed
···
120
120
)
121
121
}
122
122
123
123
+
@Test func optionalConvertedFileWritePrintsPresentValue() throws {
124
124
+
let directory = try RewriteTemporaryDirectory()
125
125
+
126
126
+
let tree = File.Optional("event", "txt")
127
127
+
.convert(RewriteNameConversion())
128
128
+
129
129
+
let result = try tree.write("New", to: directory.url)
130
130
+
131
131
+
#expect(try result.output.requiredValue == "New")
132
132
+
#expect(try directory.text(at: "event.txt") == "name: New\n")
133
133
+
}
134
134
+
135
135
+
@Test func optionalConvertedFileWriteSkipsNilValue() throws {
136
136
+
let directory = try RewriteTemporaryDirectory()
137
137
+
try Data("name: Old\n# keep\n".utf8)
138
138
+
.write(to: directory.url.appendingPathComponent("event", withType: "txt"))
139
139
+
140
140
+
let tree = File.Optional("event", "txt")
141
141
+
.convert(RewriteNameConversion())
142
142
+
143
143
+
let result = try tree.write(nil, to: directory.url)
144
144
+
145
145
+
#expect(try result.output.requiredValue == nil)
146
146
+
#expect(try directory.text(at: "event.txt") == "name: Old\n# keep\n")
147
147
+
}
148
148
+
149
149
+
@Test func optionalConvertedFileWriteDeletesNilValueWhenEnabled() throws {
150
150
+
let directory = try RewriteTemporaryDirectory()
151
151
+
try Data("name: Old\n# keep\n".utf8)
152
152
+
.write(to: directory.url.appendingPathComponent("event", withType: "txt"))
153
153
+
154
154
+
let tree = File.Optional("event", "txt")
155
155
+
.convert(RewriteNameConversion())
156
156
+
157
157
+
let result = try $deletingNilOptionalWrites.withValue(true) {
158
158
+
try tree.write(nil, to: directory.url)
159
159
+
}
160
160
+
161
161
+
#expect(try result.output.requiredValue == nil)
162
162
+
#expect(!FileManager.default.fileExists(atPath: directory.url.appendingPathComponent("event", withType: "txt").path()))
163
163
+
}
164
164
+
165
165
+
@Test func optionalConvertedFileRewritePassesExistingDataToConvertedWriter() throws {
166
166
+
let directory = try RewriteTemporaryDirectory()
167
167
+
try Data("name: Old\n# keep\n".utf8)
168
168
+
.write(to: directory.url.appendingPathComponent("event", withType: "txt"))
169
169
+
170
170
+
let tree = File.Optional("event", "txt")
171
171
+
.convert(RewriteNameConversion())
172
172
+
173
173
+
let result = try tree.rewrite("New", at: directory.url)
174
174
+
175
175
+
#expect(try result.output.requiredValue == "New")
176
176
+
#expect(
177
177
+
try directory.text(at: "event.txt")
178
178
+
== "name: New\n# keep\n"
179
179
+
)
180
180
+
}
181
181
+
123
182
@Test func writeWithoutSourceContextUsesCanonicalOutput() throws {
124
183
let directory = try RewriteTemporaryDirectory()
125
184
let tree = File("event", "txt")
+75
-1
Tests/FileTreeTests/TupleWriteTests.swift
View file
Reviewed
···
173
173
#expect(result.graph.containsNode(.file("winter-market/venue.txt")))
174
174
}
175
175
176
176
+
@Test func optionalFileWriteWritesPresentData() throws {
177
177
+
let directory = try TupleWriteTemporaryDirectory()
178
178
+
179
179
+
let result = try File.Optional("event", "txt")
180
180
+
.write(Data("Wicked Woods".utf8), to: directory.url)
181
181
+
182
182
+
let event = try String(
183
183
+
decoding: Data(contentsOf: directory.url.appendingPathComponent("event", withType: "txt")),
184
184
+
as: UTF8.self
185
185
+
)
186
186
+
187
187
+
let output = try result.output.requiredValue
188
188
+
#expect(output == Data("Wicked Woods".utf8))
189
189
+
#expect(event == "Wicked Woods")
190
190
+
#expect(result.graph.containsNode(.file("event.txt")))
191
191
+
}
192
192
+
193
193
+
@Test func optionalFileWriteSkipsNilFile() throws {
194
194
+
let directory = try TupleWriteTemporaryDirectory()
195
195
+
try Data("Existing".utf8)
196
196
+
.write(to: directory.url.appendingPathComponent("event", withType: "txt"))
197
197
+
198
198
+
let result = try File.Optional("event", "txt")
199
199
+
.write(nil, to: directory.url)
200
200
+
201
201
+
let output = try result.output.requiredValue
202
202
+
let event = try String(
203
203
+
decoding: Data(contentsOf: directory.url.appendingPathComponent("event", withType: "txt")),
204
204
+
as: UTF8.self
205
205
+
)
206
206
+
#expect(output == nil)
207
207
+
#expect(event == "Existing")
208
208
+
#expect(result.graph.nodes.isEmpty)
209
209
+
}
210
210
+
211
211
+
@Test func optionalFileWriteDeletesNilFileWhenEnabled() throws {
212
212
+
let directory = try TupleWriteTemporaryDirectory()
213
213
+
try Data("Existing".utf8)
214
214
+
.write(to: directory.url.appendingPathComponent("event", withType: "txt"))
215
215
+
216
216
+
let result = try $deletingNilOptionalWrites.withValue(true) {
217
217
+
try File.Optional("event", "txt")
218
218
+
.write(nil, to: directory.url)
219
219
+
}
220
220
+
221
221
+
let output = try result.output.requiredValue
222
222
+
#expect(output == nil)
223
223
+
#expect(!FileManager.default.fileExists(atPath: directory.url.appendingPathComponent("event", withType: "txt").path()))
224
224
+
#expect(result.graph.nodes.isEmpty)
225
225
+
}
226
226
+
176
227
@Test func optionalDirectoryWriteSkipsNilDirectory() throws {
177
228
let directory = try TupleWriteTemporaryDirectory()
229
229
+
let stagesURL = directory.url.appending(component: "stages")
230
230
+
try FileManager.default.createDirectory(at: stagesURL, withIntermediateDirectories: true)
231
231
+
try Data("Existing".utf8)
232
232
+
.write(to: stagesURL.appendingPathComponent("meadow", withType: "txt"))
178
233
179
234
let result = try Directory.Optional("stages") {
180
235
File.Many(withExtension: "txt")
···
183
238
184
239
let output = try result.output.requiredValue
185
240
#expect(output == nil)
186
186
-
#expect(!FileManager.default.fileExists(atPath: directory.url.appending(component: "stages").path()))
241
241
+
#expect(FileManager.default.fileExists(atPath: stagesURL.appendingPathComponent("meadow", withType: "txt").path()))
242
242
+
}
243
243
+
244
244
+
@Test func optionalDirectoryWriteDeletesNilDirectoryWhenEnabled() throws {
245
245
+
let directory = try TupleWriteTemporaryDirectory()
246
246
+
let stagesURL = directory.url.appending(component: "stages")
247
247
+
try FileManager.default.createDirectory(at: stagesURL, withIntermediateDirectories: true)
248
248
+
try Data("Existing".utf8)
249
249
+
.write(to: stagesURL.appendingPathComponent("meadow", withType: "txt"))
250
250
+
251
251
+
let result = try $deletingNilOptionalWrites.withValue(true) {
252
252
+
try Directory.Optional("stages") {
253
253
+
File.Many(withExtension: "txt")
254
254
+
}
255
255
+
.write(nil, to: directory.url)
256
256
+
}
257
257
+
258
258
+
let output = try result.output.requiredValue
259
259
+
#expect(output == nil)
260
260
+
#expect(!FileManager.default.fileExists(atPath: stagesURL.path()))
187
261
}
188
262
189
263
@Test func optionalDirectoryWritePrefixesGraphAndWritesPresentContent() throws {