A type safe swift ResultBuilder DSL for structured directories
0

Configure Feed

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

Propagate diagnostics through content conversions

author
Woodrow Melling
date (Jun 11, 2026, 5:12 PM -0600) commit 23f6c0d0 parent d70c0bde change-id sszrvkmn
+177
+118
Sources/FileTree/Conversions.swift
··· 161 161 try input.map { try self.conversion.apply($0) } 162 162 } 163 163 164 + public func apply( 165 + _ input: FileContent<AppliedConversion.Input>, 166 + in graph: SourceGraph 167 + ) -> Conversions.Result<FileContent<AppliedConversion.Output>> { 168 + let result = conversion.apply(input.data, in: graph.rooted(at: input)) 169 + 170 + switch result.output { 171 + case let .value(output) where !result.diagnostics.hasErrors: 172 + do { 173 + return .value( 174 + try FileContent( 175 + fileName: input.fileName, 176 + fileType: input.fileType, 177 + data: output 178 + ), 179 + diagnostics: result.diagnostics 180 + ) 181 + } catch { 182 + return .invalid( 183 + diagnostics: result.diagnostics.merging( 184 + .init( 185 + diagnostics: [ 186 + Diagnostic( 187 + severity: .error, 188 + code: "file-tree.file-content.conversion.failed", 189 + message: String(describing: error) 190 + ) 191 + ] 192 + ) 193 + ) 194 + ) 195 + } 196 + 197 + case .value, .invalid: 198 + return .invalid(diagnostics: result.diagnostics) 199 + } 200 + } 201 + 164 202 public func unapply(_ output: FileContent<AppliedConversion.Output>) throws -> FileContent<AppliedConversion.Input> { 165 203 try output.map { try self.conversion.unapply($0) } 166 204 } 205 + 206 + public func unapply( 207 + _ output: FileContent<AppliedConversion.Output>, 208 + in graph: SourceGraph 209 + ) -> Conversions.Result<FileContent<AppliedConversion.Input>> { 210 + let result = conversion.unapply(output.data, in: graph.rooted(at: output)) 211 + 212 + switch result.output { 213 + case let .value(input) where !result.diagnostics.hasErrors: 214 + do { 215 + return .value( 216 + try FileContent( 217 + fileName: output.fileName, 218 + fileType: output.fileType, 219 + data: input 220 + ), 221 + diagnostics: result.diagnostics 222 + ) 223 + } catch { 224 + return .invalid( 225 + diagnostics: result.diagnostics.merging( 226 + .init( 227 + diagnostics: [ 228 + Diagnostic( 229 + severity: .error, 230 + code: "file-tree.file-content.print.failed", 231 + message: String(describing: error) 232 + ) 233 + ] 234 + ) 235 + ) 236 + ) 237 + } 238 + 239 + case .value, .invalid: 240 + return .invalid(diagnostics: result.diagnostics) 241 + } 242 + } 167 243 } 168 244 169 245 extension FileContentConversion: Sendable where AppliedConversion: Sendable {} ··· 187 263 try input.map { try self.conversion.apply($0) } 188 264 } 189 265 266 + public func apply( 267 + _ input: DirectoryContent<AppliedConversion.Input>, 268 + in graph: SourceGraph 269 + ) -> Conversions.Result<DirectoryContent<AppliedConversion.Output>> { 270 + let result = conversion.apply(input.components, in: graph.rooted(at: input)) 271 + 272 + switch result.output { 273 + case let .value(output) where !result.diagnostics.hasErrors: 274 + return .value( 275 + DirectoryContent( 276 + directoryName: input.directoryName, 277 + components: output 278 + ), 279 + diagnostics: result.diagnostics 280 + ) 281 + 282 + case .value, .invalid: 283 + return .invalid(diagnostics: result.diagnostics) 284 + } 285 + } 286 + 190 287 public func unapply(_ output: DirectoryContent<AppliedConversion.Output>) throws -> DirectoryContent<AppliedConversion.Input> { 191 288 try output.map { try self.conversion.unapply($0) } 289 + } 290 + 291 + public func unapply( 292 + _ output: DirectoryContent<AppliedConversion.Output>, 293 + in graph: SourceGraph 294 + ) -> Conversions.Result<DirectoryContent<AppliedConversion.Input>> { 295 + let result = conversion.unapply(output.components, in: graph.rooted(at: output)) 296 + 297 + switch result.output { 298 + case let .value(input) where !result.diagnostics.hasErrors: 299 + return .value( 300 + DirectoryContent( 301 + directoryName: output.directoryName, 302 + components: input 303 + ), 304 + diagnostics: result.diagnostics 305 + ) 306 + 307 + case .value, .invalid: 308 + return .invalid(diagnostics: result.diagnostics) 309 + } 192 310 } 193 311 } 194 312
+59
Tests/FileTreeTests/ConversionResultTests.swift
··· 150 150 ] 151 151 ) 152 152 } 153 + 154 + @Test func fileContentConversionMergesDiagnosticsFromWrappedConversion() throws { 155 + let input = try FileContent( 156 + fileName: "event", 157 + fileType: "txt", 158 + data: Data("Wicked Woods".utf8) 159 + ) 160 + 161 + let result = FileContentConversion(WarningEventNameConversion()) 162 + .apply(input, in: .node(.file("event.txt"))) 163 + let output = try result.output.requiredValue 164 + 165 + #expect(output.fileName == "event") 166 + #expect(output.fileType == "txt") 167 + #expect(output.data == "Wicked Woods") 168 + #expect(result.diagnostics.diagnostics.map(\.code.rawValue) == ["ome.event.description.missing"]) 169 + } 170 + 171 + @Test func fileContentConversionReturnsInvalidFromWrappedConversion() throws { 172 + let input = try FileContent( 173 + fileName: "event", 174 + fileType: "txt", 175 + data: Data("".utf8) 176 + ) 177 + 178 + let result = FileContentConversion(RequiredEventNameConversion()) 179 + .apply(input, in: .node(.file("event.txt"))) 180 + 181 + #expect(result.output.isInvalid) 182 + #expect(result.diagnostics.diagnostics.map(\.code.rawValue) == ["ome.event.name.required"]) 183 + } 184 + 185 + @Test func fileContentConversionPrintMergesDiagnosticsFromWrappedConversion() throws { 186 + let output = try FileContent( 187 + fileName: "event", 188 + fileType: "txt", 189 + data: "Wicked Woods" 190 + ) 191 + 192 + let result = FileContentConversion(WarningPrintEventNameConversion()) 193 + .unapply(output, in: .node(.file("event.txt"))) 194 + let input = try result.output.requiredValue 195 + 196 + #expect(input.fileName == "event") 197 + #expect(input.fileType == "txt") 198 + #expect(String(decoding: input.data, as: UTF8.self) == "Wicked Woods") 199 + #expect(result.diagnostics.diagnostics.map(\.code.rawValue) == ["ome.event.name.printed"]) 200 + } 201 + 202 + @Test func directoryContentConversionMergesDiagnosticsFromWrappedConversion() { 203 + let result = DirectoryContentConversion(WarningEventNameConversion()) 204 + .apply( 205 + DirectoryContent(directoryName: "wicked-woods", components: Data("Wicked Woods".utf8)), 206 + in: .node(.directory("wicked-woods")) 207 + ) 208 + 209 + #expect(result.output == .value(DirectoryContent(directoryName: "wicked-woods", components: "Wicked Woods"))) 210 + #expect(result.diagnostics.diagnostics.map(\.code.rawValue) == ["ome.event.description.missing"]) 211 + } 153 212 } 154 213 155 214 private struct WarningEventNameConversion: Conversion {