@@ -16,122 +16,193 @@ extension ABI {
1616 /// A type implementing the JSON encoding of ``Attachment`` for the ABI entry
1717 /// point and event stream output.
1818 ///
19- /// This type is not part of the public interface of the testing library. It
20- /// assists in converting values to JSON; clients that consume this JSON are
21- /// expected to write their own decoders.
22- struct EncodedAttachment < V> : Sendable where V: ABI . Version {
23- /// The path where the attachment was written.
24- var path : String ?
19+ /// You can use this type and its conformance to [`Codable`](https://developer.apple.com/documentation/swift/codable),
20+ /// when integrating the testing library with development tools. It is not
21+ /// part of the testing library's public interface.
22+ public struct EncodedAttachment < V> : Sendable where V: ABI . Version {
23+ /// The different kinds of encoded attachment.
24+ fileprivate enum Kind : Sendable {
25+ /// The attachment has already been saved to disk and we have its local
26+ /// file system path.
27+ case savedAtPath( String )
28+
29+ /// The attachment is stored in memory and we have its serialized form.
30+ case inMemory( [ UInt8 ] )
31+
32+ /// The attachment has not been saved nor serialized yet and we still have
33+ /// it as an attachable value.
34+ case abstract( Attachment < AnyAttachable > )
35+
36+ /// An error occurred when the attachment was encoded that prevented it
37+ /// from being properly serialized.
38+ case error( ABI . EncodedError < V > )
39+ }
40+
41+ /// The kind of encoded attachment.
42+ fileprivate var kind : Kind
2543
2644 /// The preferred name of the attachment.
2745 ///
2846 /// - Warning: Attachments' preferred names are not yet part of the JSON
2947 /// schema.
3048 var _preferredName : String ?
49+ }
50+ }
3151
32- /// The raw content of the attachment, if available.
33- ///
34- /// The value of this property is set if the attachment was not first saved
35- /// to a file. It may also be `nil` if an error occurred while trying to get
36- /// the original attachment's serialized representation.
37- ///
38- /// - Warning: Inline attachment content is not yet part of the JSON schema.
39- var _bytes : Bytes ?
52+ // MARK: - Codable
4053
41- init ( encoding attachment: borrowing Attachment < AnyAttachable > , in eventContext: borrowing Event . Context ) {
42- path = attachment. fileSystemPath
54+ extension ABI . EncodedAttachment : Codable {
55+ private enum CodingKeys : String , CodingKey {
56+ case path
57+ case preferredName = " _preferredName "
58+ case bytes = " _bytes "
59+ case error = " _error "
60+ }
4361
44- if V . includesExperimentalFields {
45- _preferredName = attachment . preferredName
62+ public func encode ( to encoder : any Encoder ) throws {
63+ var container = encoder . container ( keyedBy : CodingKeys . self )
4664
47- if path == nil {
48- _bytes = try ? attachment. withUnsafeBytes { bytes in
49- return Bytes ( rawValue: [ UInt8] ( bytes) )
65+ func encodeBytes( _ bytes: UnsafeRawBufferPointer ) throws {
66+ #if canImport(Foundation)
67+ // If possible, encode this structure as Base64 data.
68+ try bytes. withUnsafeBytes { bytes in
69+ let data = Data ( bytesNoCopy: . init( mutating: bytes. baseAddress!) , count: bytes. count, deallocator: . none)
70+ try container. encode ( data. base64EncodedString ( ) , forKey: . bytes)
71+ }
72+ #else
73+ // Otherwise, it's an array of integers.
74+ try container. encode ( bytes, forKey: . bytes)
75+ #endif
76+ }
77+
78+ switch kind {
79+ case let . savedAtPath( path) :
80+ try container. encode ( path, forKey: . path)
81+ case let . abstract( attachment) :
82+ if V . includesExperimentalFields {
83+ var errorWhileEncoding : ( any Error ) ?
84+ do {
85+ try attachment. withUnsafeBytes { bytes in
86+ do {
87+ try encodeBytes ( bytes)
88+ } catch {
89+ // An error occurred during encoding rather than coming from the
90+ // attachment itself. Preserve it and throw it before returning.
91+ errorWhileEncoding = error
92+ }
5093 }
94+ } catch {
95+ // An error occurred while serializing the attachment. Encode it
96+ // separately for recovery on the calling side.
97+ let error = ABI . EncodedError< V> ( encoding: error)
98+ try container. encode ( error, forKey: . error)
5199 }
100+ if let errorWhileEncoding {
101+ throw errorWhileEncoding
102+ }
103+ }
104+ case let . inMemory( bytes) :
105+ if V . includesExperimentalFields {
106+ try bytes. withUnsafeBytes ( encodeBytes)
107+ }
108+ case let . error( error) :
109+ if V . includesExperimentalFields {
110+ try container. encode ( error, forKey: . error)
52111 }
53112 }
54-
55- /// A structure representing the bytes of an attachment.
56- struct Bytes : Sendable , RawRepresentable {
57- var rawValue : [ UInt8 ]
113+ if V . includesExperimentalFields {
114+ try container. encodeIfPresent ( _preferredName, forKey: . preferredName)
58115 }
59116 }
60- }
61117
62- // MARK: - Codable
118+ public init ( from decoder: any Decoder ) throws {
119+ let container = try decoder. container ( keyedBy: CodingKeys . self)
63120
64- extension ABI . EncodedAttachment : Codable { }
121+ kind = try {
122+ if let path = try container. decodeIfPresent ( String . self, forKey: . path) {
123+ return . savedAtPath( path)
124+ }
65125
66- extension ABI . EncodedAttachment . Bytes : Codable {
67- func encode( to encoder: any Encoder ) throws {
126+ if V . includesExperimentalFields {
68127#if canImport(Foundation)
69- // If possible, encode this structure as Base64 data.
70- try rawValue. withUnsafeBytes { rawValue in
71- let data = Data ( bytesNoCopy: . init( mutating: rawValue. baseAddress!) , count: rawValue. count, deallocator: . none)
72- var container = encoder. singleValueContainer ( )
73- try container. encode ( data)
74- }
75- #else
76- // Otherwise, it's an array of integers.
77- var container = encoder. singleValueContainer ( )
78- try container. encode ( rawValue)
128+ // If possible, decode a whole Foundation Data object.
129+ if let data = try ? container. decodeIfPresent ( Data . self, forKey: . bytes) {
130+ return . inMemory( [ UInt8] ( data) )
131+ }
79132#endif
80- }
81133
82- init ( from decoder: any Decoder ) throws {
83- let container = try decoder. singleValueContainer ( )
134+ // Fall back to trying to decode an array of integers.
135+ if let bytes = try container. decodeIfPresent ( [ UInt8 ] . self, forKey: . bytes) {
136+ return . inMemory( bytes)
137+ }
84138
85- #if canImport(Foundation)
86- // If possible, decode a whole Foundation Data object.
87- if let data = try ? container. decode ( Data . self) {
88- self . init ( rawValue: [ UInt8] ( data) )
89- return
90- }
91- #endif
139+ // Finally, look for an error caught during encoding.
140+ if let error = try container. decodeIfPresent ( ABI . EncodedError< V> . self , forKey: . error) {
141+ return . error( error)
142+ }
143+ }
144+
145+ // Couldn't find anything to decode.
146+ throw DecodingError . valueNotFound (
147+ String . self,
148+ DecodingError . Context (
149+ codingPath: decoder. codingPath + [ CodingKeys . path] ,
150+ debugDescription: " Encoded attachment did not include any persistent representation. "
151+ )
152+ )
153+ } ( )
92154
93- // Fall back to trying to decode an array of integers.
94- let bytes = try container. decode ( [ UInt8 ] . self)
95- self . init ( rawValue : bytes )
155+ if V . includesExperimentalFields {
156+ _preferredName = try container. decodeIfPresent ( String . self, forKey : . preferredName )
157+ }
96158 }
97159}
98160
99161// MARK: - Attachable
100162
101163extension ABI . EncodedAttachment : Attachable {
102- var estimatedAttachmentByteCount : Int ? {
103- _bytes? . rawValue. count
164+ public var estimatedAttachmentByteCount : Int ? {
165+ switch kind {
166+ case . savedAtPath, . error:
167+ return nil
168+ case let . inMemory( bytes) :
169+ return bytes. count
170+ case let . abstract( attachment) :
171+ return attachment. attachableValue. estimatedAttachmentByteCount
172+ }
104173 }
105174
106175 /// An error type that is thrown when ``ABI/EncodedAttachment`` cannot satisfy
107176 /// a request for the underlying attachment's bytes.
108177 fileprivate struct BytesUnavailableError : Error { }
109178
110- borrowing func withUnsafeBytes< R> ( for attachment: borrowing Attachment < Self > , _ body: ( UnsafeRawBufferPointer ) throws -> R ) throws -> R {
111- if let bytes = _bytes? . rawValue {
112- return try bytes. withUnsafeBytes ( body)
113- }
114-
179+ public borrowing func withUnsafeBytes< R> ( for attachment: borrowing Attachment < Self > , _ body: ( UnsafeRawBufferPointer ) throws -> R ) throws -> R {
180+ switch kind {
181+ case let . savedAtPath( path) :
115182#if !SWT_NO_FILE_IO
116- guard let path else {
117- throw BytesUnavailableError ( )
118- }
119183#if canImport(Foundation)
120- // Leverage Foundation's file-mapping logic since we're using Data anyway.
121- let url = URL ( fileURLWithPath: path, isDirectory: false )
122- let bytes = try Data ( contentsOf: url, options: [ . mappedIfSafe] )
184+ // Leverage Foundation's file-mapping logic since we're using Data anyway.
185+ let url = URL ( fileURLWithPath: path, isDirectory: false )
186+ let bytes = try Data ( contentsOf: url, options: [ . mappedIfSafe] )
123187#else
124- let fileHandle = try FileHandle ( forReadingAtPath: path)
125- let bytes = try fileHandle. readToEnd ( )
188+ let fileHandle = try FileHandle ( forReadingAtPath: path)
189+ let bytes = try fileHandle. readToEnd ( )
126190#endif
127- return try bytes. withUnsafeBytes ( body)
191+ return try bytes. withUnsafeBytes ( body)
128192#else
129- // Cannot read the attachment from disk on this platform.
130- throw BytesUnavailableError ( )
193+ // Cannot read the attachment from disk on this platform.
194+ throw BytesUnavailableError ( )
131195#endif
196+ case let . inMemory( bytes) :
197+ return try bytes. withUnsafeBytes ( body)
198+ case let . abstract( attachment) :
199+ return try attachment. withUnsafeBytes ( body)
200+ case let . error( error) :
201+ throw error
202+ }
132203 }
133204
134- borrowing func preferredName( for attachment: borrowing Attachment < Self > , basedOn suggestedName: String ) -> String {
205+ public borrowing func preferredName( for attachment: borrowing Attachment < Self > , basedOn suggestedName: String ) -> String {
135206 _preferredName ?? suggestedName
136207 }
137208}
@@ -141,3 +212,66 @@ extension ABI.EncodedAttachment.BytesUnavailableError: CustomStringConvertible {
141212 " The attachment's content could not be deserialized. "
142213 }
143214}
215+
216+ // MARK: - Conversion to/from library types
217+
218+ extension ABI . EncodedAttachment {
219+ /// Initialize an instance of this type from the given value.
220+ ///
221+ /// - Parameters:
222+ /// - attachment: The attachment to initialize this instance from.
223+ public init ( encoding attachment: borrowing Attachment < AnyAttachable > ) {
224+ if let path = attachment. fileSystemPath {
225+ kind = . savedAtPath( path)
226+ } else {
227+ kind = . abstract( copy attachment)
228+ }
229+
230+ if V . includesExperimentalFields {
231+ _preferredName = attachment. preferredName
232+ }
233+ }
234+
235+ /// Initialize an instance of this type from the given value.
236+ ///
237+ /// - Parameters:
238+ /// - attachment: The attachment to initialize this instance from.
239+ public init ( encoding attachment: borrowing Attachment < some Attachable & Sendable & ~ Copyable> ) {
240+ let attachmentCopy = Attachment < AnyAttachable > ( copy attachment)
241+ self . init ( encoding: attachmentCopy)
242+ }
243+ }
244+
245+ @_spi ( ForToolsIntegrationOnly)
246+ extension Attachment where AttachableValue == AnyAttachable {
247+ /// Initialize an instance of this type from the given value.
248+ ///
249+ /// - Parameters:
250+ /// - event: The encoded event to initialize this instance from.
251+ ///
252+ /// If `event` does not represent an attached value, the initializer returns
253+ /// `nil`.
254+ public init ? < V> ( decoding event: ABI . EncodedEvent < V > ) {
255+ guard let attachment = event. attachment else {
256+ return nil
257+ }
258+ self . init ( decoding: attachment)
259+ if let sourceLocation = event. _sourceLocation. flatMap ( SourceLocation . init ( decoding: ) ) {
260+ self . sourceLocation = sourceLocation
261+ }
262+ }
263+
264+ /// Initialize an instance of this type from the given value.
265+ ///
266+ /// - Parameters:
267+ /// - attachment: The encoded attachment to initialize this instance from.
268+ public init ? < V> ( decoding attachment: ABI . EncodedAttachment < V > ) {
269+ switch attachment. kind {
270+ case let . abstract( attachment) :
271+ self = attachment // No need to nest it further.
272+ default :
273+ let attachmentCopy = Attachment < ABI . EncodedAttachment < V > > ( attachment, sourceLocation: . unknown)
274+ self . init ( attachmentCopy)
275+ }
276+ }
277+ }
0 commit comments