Skip to content

Commit 4a39dfa

Browse files
committed
Add an associated AttachmentMetadata type to Attachable.
This PR adds a new associated type to `Attachable` that can be used to supply additional metadata to an attachment that is specific to that type. Metadata is always optional and the default type is `Never` (i.e. by default there is no metadata.) The `Encodable` and `NSSecureCoding` conformances in the Foundation cross-import overlay have been updated such that this type equals a new structure that describes the format to use as well as options to pass to the JSON encoder (if one is used) and user info to pass to the plist or JSON encoders (if used.) We must use this type even for types that conform only to `NSSecureCoding`, otherwise we get compile-time errors about the type being ambiguous if a type conforms to both protocols and to `Attachable`.
1 parent 4be1dfe commit 4a39dfa

File tree

8 files changed

+314
-88
lines changed

8 files changed

+314
-88
lines changed

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable+NSSecureCoding.swift

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public import Foundation
2020

2121
@_spi(Experimental)
2222
extension Attachable where Self: Encodable & NSSecureCoding {
23+
public typealias AttachmentMetadata = EncodableAttachmentMetadata
24+
2325
@_documentation(visibility: private)
2426
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
2527
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Attachable+Encodable.swift

+19-2
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ private import Foundation
2828
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
2929
/// creation of the buffer.
3030
func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for attachment: borrowing Attachment<E>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable {
31-
let format = try EncodingFormat(for: attachment)
31+
let format = try EncodableAttachmentMetadata.Format(for: attachment)
3232

3333
let data: Data
3434
switch format {
3535
case let .propertyListFormat(propertyListFormat):
3636
let plistEncoder = PropertyListEncoder()
3737
plistEncoder.outputFormat = propertyListFormat
38+
if let metadata = attachment.metadata as? EncodableAttachmentMetadata {
39+
plistEncoder.userInfo = metadata.userInfo
40+
}
3841
data = try plistEncoder.encode(attachableValue)
3942
case .default:
4043
// The default format is JSON.
@@ -44,7 +47,19 @@ func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for at
4447
// require it be exported with (at least) package visibility which would
4548
// create a visible external dependency on Foundation in the main testing
4649
// library target.
47-
data = try JSONEncoder().encode(attachableValue)
50+
let jsonEncoder = JSONEncoder()
51+
if let metadata = attachment.metadata as? EncodableAttachmentMetadata {
52+
jsonEncoder.userInfo = metadata.userInfo
53+
54+
if let options = metadata.jsonEncodingOptions {
55+
jsonEncoder.outputFormatting = options.outputFormatting
56+
jsonEncoder.dateEncodingStrategy = options.dateEncodingStrategy
57+
jsonEncoder.dataEncodingStrategy = options.dataEncodingStrategy
58+
jsonEncoder.nonConformingFloatEncodingStrategy = options.nonConformingFloatEncodingStrategy
59+
jsonEncoder.keyEncodingStrategy = options.keyEncodingStrategy
60+
}
61+
}
62+
data = try jsonEncoder.encode(attachableValue)
4863
}
4964

5065
return try data.withUnsafeBytes(body)
@@ -55,6 +70,8 @@ func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for at
5570
// protocol for types that already support Codable.
5671
@_spi(Experimental)
5772
extension Attachable where Self: Encodable {
73+
public typealias AttachmentMetadata = EncodableAttachmentMetadata
74+
5875
/// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder)
5976
/// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder),
6077
/// then call a function and pass that buffer to it.

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/Attachable+NSSecureCoding.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public import Foundation
1717
// NSKeyedArchiver for encoding.
1818
@_spi(Experimental)
1919
extension Attachable where Self: NSSecureCoding {
20+
public typealias AttachmentMetadata = EncodableAttachmentMetadata
21+
2022
/// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver)
2123
/// into a buffer, then call a function and pass that buffer to it.
2224
///
@@ -51,7 +53,7 @@ extension Attachable where Self: NSSecureCoding {
5153
/// some other path extension, that path extension must represent a type
5254
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist).
5355
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
54-
let format = try EncodingFormat(for: attachment)
56+
let format = try EncodableAttachmentMetadata.Format(for: attachment)
5557

5658
var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true)
5759
switch format {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
#if canImport(Foundation)
12+
@_spi(Experimental) import Testing
13+
public import Foundation
14+
15+
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
16+
private import UniformTypeIdentifiers
17+
#endif
18+
19+
/// An enumeration describing the encoding formats supported by default when
20+
/// encoding a value that conforms to ``Testing/Attachable`` and either
21+
/// [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
22+
/// or [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding).
23+
@_spi(Experimental)
24+
public struct EncodableAttachmentMetadata: Sendable {
25+
/// An enumeration describing the encoding formats supported by default when
26+
/// encoding a value that conforms to ``Testing/Attachable`` and either
27+
/// [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
28+
/// or [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding).
29+
@_spi(Experimental)
30+
public enum Format: Sendable {
31+
/// The encoding format to use by default.
32+
///
33+
/// The specific format this case corresponds to depends on if we are encoding
34+
/// an `Encodable` value or an `NSSecureCoding` value.
35+
case `default`
36+
37+
/// A property list format.
38+
///
39+
/// - Parameters:
40+
/// - format: The corresponding property list format.
41+
///
42+
/// OpenStep-style property lists are not supported.
43+
case propertyListFormat(_ format: PropertyListSerialization.PropertyListFormat)
44+
45+
/// The JSON format.
46+
case json
47+
}
48+
49+
/// The format the attachable value should be encoded as.
50+
public var format: Format
51+
52+
/// A type describing the various JSON encoding options to use if
53+
/// [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder)
54+
/// is used to encode the attachable value.
55+
public struct JSONEncodingOptions: Sendable {
56+
/// The output format to produce.
57+
public var outputFormatting: JSONEncoder.OutputFormatting
58+
59+
/// The strategy to use in encoding dates.
60+
public var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy
61+
62+
/// The strategy to use in encoding binary data.
63+
public var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy
64+
65+
/// The strategy to use in encoding non-conforming numbers.
66+
public var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy
67+
68+
/// The strategy to use for encoding keys.
69+
public var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy
70+
}
71+
72+
/// JSON encoding options to use if [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder)
73+
/// is used to encode the attachable value.
74+
///
75+
/// The default value of this property is `nil`, meaning that the default
76+
/// options are used when encoding an attachable value as JSON. If an
77+
/// attachable value is encoded in a format other than JSON, the value of this
78+
/// property is ignored.
79+
public var jsonEncodingOptions: JSONEncodingOptions?
80+
81+
/// A user info dictionary to provide to the property list encoder or JSON
82+
/// encoder when encoding the attachable value.
83+
///
84+
/// The value of this property is ignored when encoding an attachable value
85+
/// that conforms to [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding)
86+
/// but does not conform to [`Encodable`](https://developer.apple.com/documentation/swift/encodable).
87+
public var userInfo: [CodingUserInfoKey: any Sendable]
88+
89+
public init(format: Format, jsonEncodingOptions: JSONEncodingOptions? = nil, userInfo: [CodingUserInfoKey: any Sendable] = [:]) {
90+
self.format = format
91+
self.jsonEncodingOptions = jsonEncodingOptions
92+
self.userInfo = userInfo
93+
}
94+
}
95+
96+
// MARK: -
97+
98+
@_spi(Experimental)
99+
extension EncodableAttachmentMetadata.JSONEncodingOptions {
100+
public init(
101+
outputFormatting: JSONEncoder.OutputFormatting? = nil,
102+
dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil,
103+
dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil,
104+
nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil,
105+
keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil
106+
) {
107+
self = .default
108+
self.outputFormatting = outputFormatting ?? self.outputFormatting
109+
self.dateEncodingStrategy = dateEncodingStrategy ?? self.dateEncodingStrategy
110+
self.dataEncodingStrategy = dataEncodingStrategy ?? self.dataEncodingStrategy
111+
self.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy ?? self.nonConformingFloatEncodingStrategy
112+
self.keyEncodingStrategy = keyEncodingStrategy ?? self.keyEncodingStrategy
113+
}
114+
115+
/// An instance of this type representing the default JSON encoding options
116+
/// used by [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder).
117+
public static let `default`: Self = {
118+
// Get the default values from a real JSONEncoder for max authenticity!
119+
let encoder = JSONEncoder()
120+
121+
return Self(
122+
outputFormatting: encoder.outputFormatting,
123+
dateEncodingStrategy: encoder.dateEncodingStrategy,
124+
dataEncodingStrategy: encoder.dataEncodingStrategy,
125+
nonConformingFloatEncodingStrategy: encoder.nonConformingFloatEncodingStrategy,
126+
keyEncodingStrategy: encoder.keyEncodingStrategy
127+
)
128+
}()
129+
}
130+
131+
// MARK: -
132+
133+
extension EncodableAttachmentMetadata.Format {
134+
/// Initialize an instance of this type representing the content type or media
135+
/// type of the specified attachment.
136+
///
137+
/// - Parameters:
138+
/// - attachment: The attachment that will be encoded.
139+
///
140+
/// - Throws: If the attachment's content type or media type is unsupported.
141+
init(for attachment: borrowing Attachment<some Attachable>) throws {
142+
if let metadata = attachment.metadata {
143+
if let format = metadata as? Self {
144+
self = format
145+
return
146+
} else if let metadata = metadata as? EncodableAttachmentMetadata {
147+
self = metadata.format
148+
return
149+
} else if let propertyListFormat = metadata as? PropertyListSerialization.PropertyListFormat {
150+
self = .propertyListFormat(propertyListFormat)
151+
return
152+
}
153+
}
154+
155+
let ext = (attachment.preferredName as NSString).pathExtension
156+
157+
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
158+
// If the caller explicitly wants to encode their data as either XML or as a
159+
// property list, use PropertyListEncoder. Otherwise, we'll fall back to
160+
// JSONEncoder below.
161+
if #available(_uttypesAPI, *), let contentType = UTType(filenameExtension: ext) {
162+
if contentType == .data {
163+
self = .default
164+
} else if contentType.conforms(to: .json) {
165+
self = .json
166+
} else if contentType.conforms(to: .xml) {
167+
self = .propertyListFormat(.xml)
168+
} else if contentType.conforms(to: .binaryPropertyList) || contentType == .propertyList {
169+
self = .propertyListFormat(.binary)
170+
} else if contentType.conforms(to: .propertyList) {
171+
self = .propertyListFormat(.openStep)
172+
} else {
173+
let contentTypeDescription = contentType.localizedDescription ?? contentType.identifier
174+
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The content type '\(contentTypeDescription)' cannot be used to attach an instance of \(type(of: self)) to a test."])
175+
}
176+
return
177+
}
178+
#endif
179+
180+
if ext.isEmpty {
181+
// No path extension? No problem! Default data.
182+
self = .default
183+
} else if ext.caseInsensitiveCompare("plist") == .orderedSame {
184+
self = .propertyListFormat(.binary)
185+
} else if ext.caseInsensitiveCompare("xml") == .orderedSame {
186+
self = .propertyListFormat(.xml)
187+
} else if ext.caseInsensitiveCompare("json") == .orderedSame {
188+
self = .json
189+
} else {
190+
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The path extension '.\(ext)' cannot be used to attach an instance of \(type(of: self)) to a test."])
191+
}
192+
}
193+
}
194+
#endif

Diff for: Sources/Overlays/_Testing_Foundation/Attachments/EncodingFormat.swift

-84
This file was deleted.

Diff for: Sources/Testing/Attachments/Attachable.swift

+18
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@
2727
/// that conforms to ``AttachableContainer`` to act as a proxy.
2828
@_spi(Experimental)
2929
public protocol Attachable: ~Copyable {
30+
/// A type containing additional metadata about an instance of this attachable
31+
/// type that a developer can optionally include when creating an attachment.
32+
///
33+
/// Instances of this type can contain metadata that is not contained directly
34+
/// in the attachable value itself. An instance of this type can be passed to
35+
/// the initializers of ``Attachment`` and then accessed later via
36+
/// ``Attachment/metadata``. Metadata is always optional; if your attachable
37+
/// value _must_ include some value, consider adding it as a property of that
38+
/// type instead of adding it as metadata.
39+
///
40+
/// When implementing ``withUnsafeBufferPointer(for:_:)``, you can access the
41+
/// attachment's ``Attachment/metadata`` property to get the metadata that was
42+
/// passed when the attachment was created.
43+
///
44+
/// By default, this type is equal to [`Never`](https://developer.apple.com/documentation/swift/never),
45+
/// meaning that an attachable value has no metadata associated with it.
46+
associatedtype AttachmentMetadata: Sendable & Copyable = Never
47+
3048
/// An estimate of the number of bytes of memory needed to store this value as
3149
/// an attachment.
3250
///

0 commit comments

Comments
 (0)