A type safe swift ResultBuilder DSL for structured directories
0

Configure Feed

Select the types of activity you want to include in your feed.

Fix optional tuple read

author
Woodrow Melling
date (Jun 11, 2026, 5:12 PM -0600) commit 55307618 parent 9a3f2dc4 change-id zuotrtlm
+297 -3
+68
Sources/FileTree/Components/Pair.swift
··· 1 + import Foundation 2 + 3 + public struct PairFileSystemComponent<First: FileTreeReader, Second: FileTreeReader>: FileTreeReader { 4 + public var first: First 5 + public var second: Second 6 + 7 + @inlinable public init(_ first: First, _ second: Second) { 8 + self.first = first 9 + self.second = second 10 + } 11 + 12 + public typealias Content = (First.Content, Second.Content) 13 + 14 + public func read(from url: URL) throws -> FileTreeResult<Content> { 15 + let firstResult = try first.read(from: url) 16 + let secondResult = try second.read(from: url) 17 + var diagnostics = DiagnosticReport<FileTreeLocation>() 18 + diagnostics.append(contentsOf: firstResult.diagnostics) 19 + diagnostics.append(contentsOf: secondResult.diagnostics) 20 + var graph = SourceGraph() 21 + graph.append(firstResult.graph) 22 + graph.append(secondResult.graph) 23 + 24 + switch (firstResult.output, secondResult.output) { 25 + case let (.value(firstOutput), .value(secondOutput)): 26 + return .value((firstOutput, secondOutput), graph: graph, diagnostics: diagnostics) 27 + case (.value, .invalid), (.invalid, .value), (.invalid, .invalid): 28 + return .invalid(graph: graph, diagnostics: diagnostics) 29 + } 30 + } 31 + } 32 + 33 + extension PairFileSystemComponent: FileTreeWriter where First: FileTreeWriter, Second: FileTreeWriter { 34 + public func write(_ content: Content, to url: URL) throws -> FileTreeResult<Content> { 35 + let firstResult = try first.write(content.0, to: url) 36 + let secondResult = try second.write(content.1, to: url) 37 + return results(firstResult, secondResult) 38 + } 39 + 40 + public func write( 41 + _ content: Content, 42 + to url: URL, 43 + context: FileTreeWriteContext 44 + ) throws -> FileTreeResult<Content> { 45 + let firstResult = try first.write(content.0, to: url, context: context) 46 + let secondResult = try second.write(content.1, to: url, context: context) 47 + return results(firstResult, secondResult) 48 + } 49 + 50 + private func results( 51 + _ firstResult: FileTreeResult<First.Content>, 52 + _ secondResult: FileTreeResult<Second.Content> 53 + ) -> FileTreeResult<Content> { 54 + var diagnostics = DiagnosticReport<FileTreeLocation>() 55 + diagnostics.append(contentsOf: firstResult.diagnostics) 56 + diagnostics.append(contentsOf: secondResult.diagnostics) 57 + var graph = SourceGraph() 58 + graph.append(firstResult.graph) 59 + graph.append(secondResult.graph) 60 + 61 + switch (firstResult.output, secondResult.output) { 62 + case let (.value(firstOutput), .value(secondOutput)) where !diagnostics.hasErrors: 63 + return .value((firstOutput, secondOutput), graph: graph, diagnostics: diagnostics) 64 + case (.value, .value), (.value, .invalid), (.invalid, .value), (.invalid, .invalid): 65 + return .invalid(graph: graph, diagnostics: diagnostics) 66 + } 67 + } 68 + }
+14 -3
Sources/FileTree/Core.swift
··· 100 100 component 101 101 } 102 102 103 - public static func buildBlock<each Component>(_ component: repeat each Component) -> TupleFileSystemComponent<repeat each Component> where repeat each Component: FileTreeReader { 104 - return TupleFileSystemComponent(repeat each component) 105 - } 103 + public static func buildBlock<First, Second>( 104 + _ first: First, 105 + _ second: Second 106 + ) -> PairFileSystemComponent<First, Second> 107 + where First: FileTreeReader, Second: FileTreeReader { 108 + PairFileSystemComponent(first, second) 109 + } 110 + 111 + public static func buildBlock<each Component>( 112 + _ component: repeat each Component 113 + ) -> TupleFileSystemComponent<repeat each Component> 114 + where repeat each Component: FileTreeReader { 115 + TupleFileSystemComponent(repeat each component) 116 + } 106 117 107 118 // public static func buildPartialBlock<F: FileTreeReader>(first content: F) -> F { 108 119 // content
+215
Tests/FileTreeTests/TupleWriteTests.swift
··· 85 85 ) 86 86 } 87 87 88 + @Test func tupleReadHandlesMissingOptionalConvertedFile() throws { 89 + let directory = try TupleWriteTemporaryDirectory() 90 + try Data("Meadow Stage".utf8) 91 + .write(to: directory.url.appendingPathComponent("venue", withType: "txt")) 92 + 93 + let tree = FileTree { 94 + File.Optional("event", "txt") 95 + .convert(TupleWriteTrimmingConversion()) 96 + File("venue", "txt") 97 + .convert(TupleWriteTrimmingConversion()) 98 + } 99 + 100 + let output = try tree.read(from: directory.url).output.requiredValue 101 + 102 + #expect(output.0 == nil) 103 + #expect(output.1 == "Meadow Stage") 104 + } 105 + 106 + @Test func tupleReadHandlesMissingOptionalConvertedFileBeforeDirectoryMany() throws { 107 + let directory = try TupleWriteTemporaryDirectory() 108 + let eventsURL = directory.url.appending(component: "events") 109 + let eventURL = eventsURL.appending(component: "wicked-woods") 110 + try FileManager.default.createDirectory(at: eventURL, withIntermediateDirectories: true) 111 + try Data("Wicked Woods".utf8) 112 + .write(to: eventURL.appendingPathComponent("event", withType: "txt")) 113 + 114 + let tree = FileTree { 115 + File.Optional("calendar", "txt") 116 + .convert(TupleWriteTrimmingConversion()) 117 + Directory("events") { 118 + Directory.Many { 119 + File("event", "txt") 120 + .convert(TupleWriteTrimmingConversion()) 121 + } 122 + } 123 + } 124 + 125 + let output = try tree.read(from: directory.url).output.requiredValue 126 + 127 + #expect(output.0 == nil) 128 + #expect(output.1.map(\.directoryName) == ["wicked-woods"]) 129 + #expect(output.1.map(\.components) == ["Wicked Woods"]) 130 + } 131 + 132 + @Test func convertedTupleReadHandlesMissingOptionalConvertedFileBeforeDirectoryMany() throws { 133 + let directory = try TupleWriteTemporaryDirectory() 134 + let eventsURL = directory.url.appending(component: "events") 135 + let eventURL = eventsURL.appending(component: "wicked-woods") 136 + try FileManager.default.createDirectory(at: eventURL, withIntermediateDirectories: true) 137 + try Data("Wicked Woods".utf8) 138 + .write(to: eventURL.appendingPathComponent("event", withType: "txt")) 139 + 140 + let tree = FileTree { 141 + File.Optional("calendar", "txt") 142 + .convert(TupleWriteTrimmingConversion()) 143 + Directory("events") { 144 + Directory.Many { 145 + File("event", "txt") 146 + .convert(TupleWriteTrimmingConversion()) 147 + } 148 + } 149 + } 150 + .convert(TupleWriteRepositoryConversion()) 151 + 152 + let output = try tree.read(from: directory.url).output.requiredValue 153 + 154 + #expect(output.calendar == nil) 155 + #expect(output.events.map(\.directoryName) == ["wicked-woods"]) 156 + #expect(output.events.map(\.components) == ["Wicked Woods"]) 157 + } 158 + 159 + @Test func nestedWriterReadHandlesMissingOptionalConvertedFileBeforeDirectoryMany() throws { 160 + let directory = try TupleWriteTemporaryDirectory() 161 + let eventsURL = directory.url.appending(component: "events") 162 + let eventURL = eventsURL.appending(component: "wicked-woods") 163 + try FileManager.default.createDirectory(at: eventURL, withIntermediateDirectories: true) 164 + try Data("Wicked Woods".utf8) 165 + .write(to: eventURL.appendingPathComponent("event", withType: "txt")) 166 + 167 + let output = try TupleWriteRepositoryFileTree() 168 + .read(from: directory.url) 169 + .output 170 + .requiredValue 171 + 172 + #expect(output.calendar == nil) 173 + #expect(output.events.map(\.directoryName) == ["wicked-woods"]) 174 + #expect(output.events.map(\.components.event) == ["Wicked Woods"]) 175 + #expect(output.events.map(\.components.stages) == [nil]) 176 + #expect(output.events.map(\.components.participants) == [nil]) 177 + } 178 + 88 179 @Test func directoryTupleWritePrefixesGraphAndWritesInsideDirectory() throws { 89 180 let directory = try TupleWriteTemporaryDirectory() 90 181 ··· 329 420 ) 330 421 ] 331 422 ) 423 + ) 424 + } 425 + } 426 + 427 + private struct TupleWriteRepository: Equatable { 428 + var calendar: String? 429 + var events: [DirectoryContent<String>] 430 + } 431 + 432 + private struct TupleWriteRepositoryConversion: Conversion { 433 + func apply(_ input: (String?, [DirectoryContent<String>])) throws -> TupleWriteRepository { 434 + TupleWriteRepository(calendar: input.0, events: input.1) 435 + } 436 + 437 + func unapply(_ output: TupleWriteRepository) throws -> (String?, [DirectoryContent<String>]) { 438 + (output.calendar, output.events) 439 + } 440 + } 441 + 442 + private struct TupleWriteRepositoryFileTree: FileTreeWriter { 443 + var body: some FileTreeWriter<TupleWriteNestedRepository> { 444 + FileTree { 445 + File.Optional("calendar", "txt") 446 + .convert(TupleWriteCalendarConversion()) 447 + 448 + Directory("events") { 449 + Directory.Many { 450 + TupleWriteEventFileTree() 451 + } 452 + } 453 + } 454 + .convert(TupleWriteNestedRepositoryConversion()) 455 + } 456 + } 457 + 458 + private struct TupleWriteEventFileTree: FileTreeWriter { 459 + var body: some FileTreeWriter<TupleWriteSourceEvent> { 460 + FileTree { 461 + File("event", "txt") 462 + .convert(TupleWriteTrimmingConversion()) 463 + 464 + Directory.Optional("stages") { 465 + File.Many(withExtension: "txt") 466 + .map(FileContentConversion(TupleWriteTrimmingConversion())) 467 + } 468 + 469 + Directory.Optional("participants") { 470 + File.Many(withExtension: "txt") 471 + .map(FileContentConversion(TupleWriteTrimmingConversion())) 472 + } 473 + } 474 + .convert(TupleWriteSourceEventConversion()) 475 + } 476 + } 477 + 478 + private struct TupleWriteNestedRepository: Equatable { 479 + var calendar: TupleWriteCalendar? 480 + var events: [DirectoryContent<TupleWriteSourceEvent>] 481 + } 482 + 483 + private struct TupleWriteCalendar: Equatable { 484 + var name: String 485 + var description: String? 486 + } 487 + 488 + private struct TupleWriteSourceEvent: Equatable { 489 + var event: String 490 + var stages: [FileContent<String>]? 491 + var participants: [FileContent<String>]? 492 + } 493 + 494 + private struct TupleWriteNestedRepositoryConversion: Conversion { 495 + func apply( 496 + _ input: (TupleWriteCalendar?, [DirectoryContent<TupleWriteSourceEvent>]) 497 + ) throws -> TupleWriteNestedRepository { 498 + TupleWriteNestedRepository(calendar: input.0, events: input.1) 499 + } 500 + 501 + func unapply( 502 + _ output: TupleWriteNestedRepository 503 + ) throws -> (TupleWriteCalendar?, [DirectoryContent<TupleWriteSourceEvent>]) { 504 + (output.calendar, output.events) 505 + } 506 + } 507 + 508 + private struct TupleWriteCalendarConversion: Conversion { 509 + func apply(_ input: Data) throws -> TupleWriteCalendar { 510 + TupleWriteCalendar( 511 + name: String(decoding: input, as: UTF8.self), 512 + description: nil 513 + ) 514 + } 515 + 516 + func unapply(_ output: TupleWriteCalendar) throws -> Data { 517 + Data(output.name.utf8) 518 + } 519 + } 520 + 521 + private struct TupleWriteSourceEventConversion: Conversion { 522 + func apply( 523 + _ input: ( 524 + String, 525 + [FileContent<String>]?, 526 + [FileContent<String>]? 527 + ) 528 + ) throws -> TupleWriteSourceEvent { 529 + TupleWriteSourceEvent( 530 + event: input.0, 531 + stages: input.1, 532 + participants: input.2 533 + ) 534 + } 535 + 536 + func unapply( 537 + _ output: TupleWriteSourceEvent 538 + ) throws -> ( 539 + String, 540 + [FileContent<String>]?, 541 + [FileContent<String>]? 542 + ) { 543 + ( 544 + output.event, 545 + output.stages, 546 + output.participants 332 547 ) 333 548 } 334 549 }