Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add an associated AttachmentMetadata type to Attachable. #824

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public import Foundation

@_spi(Experimental)
extension Attachable where Self: Encodable & NSSecureCoding {
public typealias AttachmentMetadata = EncodableAttachmentMetadata

@_documentation(visibility: private)
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
try _Testing_Foundation.withUnsafeBufferPointer(encoding: self, for: attachment, body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ private import Foundation
/// - Throws: Whatever is thrown by `body`, or any error that prevented the
/// creation of the buffer.
func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for attachment: borrowing Attachment<E>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R where E: Attachable & Encodable {
let format = try EncodingFormat(for: attachment)
let format = try EncodableAttachmentMetadata.Format(for: attachment)

let data: Data
switch format {
case let .propertyListFormat(propertyListFormat):
let plistEncoder = PropertyListEncoder()
plistEncoder.outputFormat = propertyListFormat
if let metadata = attachment.metadata as? EncodableAttachmentMetadata {
plistEncoder.userInfo = metadata.userInfo
}
data = try plistEncoder.encode(attachableValue)
case .default:
// The default format is JSON.
Expand All @@ -44,7 +47,19 @@ func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for at
// require it be exported with (at least) package visibility which would
// create a visible external dependency on Foundation in the main testing
// library target.
data = try JSONEncoder().encode(attachableValue)
let jsonEncoder = JSONEncoder()
if let metadata = attachment.metadata as? EncodableAttachmentMetadata {
jsonEncoder.userInfo = metadata.userInfo

if let options = metadata.jsonEncodingOptions {
jsonEncoder.outputFormatting = options.outputFormatting
jsonEncoder.dateEncodingStrategy = options.dateEncodingStrategy
jsonEncoder.dataEncodingStrategy = options.dataEncodingStrategy
jsonEncoder.nonConformingFloatEncodingStrategy = options.nonConformingFloatEncodingStrategy
jsonEncoder.keyEncodingStrategy = options.keyEncodingStrategy
}
}
data = try jsonEncoder.encode(attachableValue)
}

return try data.withUnsafeBytes(body)
Expand All @@ -55,6 +70,8 @@ func withUnsafeBufferPointer<E, R>(encoding attachableValue: borrowing E, for at
// protocol for types that already support Codable.
@_spi(Experimental)
extension Attachable where Self: Encodable {
public typealias AttachmentMetadata = EncodableAttachmentMetadata

/// Encode this value into a buffer using either [`PropertyListEncoder`](https://developer.apple.com/documentation/foundation/propertylistencoder)
/// or [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder),
/// then call a function and pass that buffer to it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public import Foundation
// NSKeyedArchiver for encoding.
@_spi(Experimental)
extension Attachable where Self: NSSecureCoding {
public typealias AttachmentMetadata = EncodableAttachmentMetadata

/// Encode this object using [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver)
/// into a buffer, then call a function and pass that buffer to it.
///
Expand Down Expand Up @@ -51,7 +53,7 @@ extension Attachable where Self: NSSecureCoding {
/// some other path extension, that path extension must represent a type
/// that conforms to [`UTType.propertyList`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/propertylist).
public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
let format = try EncodingFormat(for: attachment)
let format = try EncodableAttachmentMetadata.Format(for: attachment)

var data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true)
switch format {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

#if canImport(Foundation)
@_spi(Experimental) import Testing
public import Foundation

#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
private import UniformTypeIdentifiers
#endif

/// An enumeration describing the encoding formats supported by default when
/// encoding a value that conforms to ``Testing/Attachable`` and either
/// [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
/// or [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding).
@_spi(Experimental)
public struct EncodableAttachmentMetadata: Sendable {
/// An enumeration describing the encoding formats supported by default when
/// encoding a value that conforms to ``Testing/Attachable`` and either
/// [`Encodable`](https://developer.apple.com/documentation/swift/encodable)
/// or [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding).
@_spi(Experimental)
public enum Format: Sendable {
/// The encoding format to use by default.
///
/// The specific format this case corresponds to depends on if we are encoding
/// an `Encodable` value or an `NSSecureCoding` value.
case `default`

/// A property list format.
///
/// - Parameters:
/// - format: The corresponding property list format.
///
/// OpenStep-style property lists are not supported.
case propertyListFormat(_ format: PropertyListSerialization.PropertyListFormat)

/// The JSON format.
case json
}

/// The format the attachable value should be encoded as.
public var format: Format

/// A type describing the various JSON encoding options to use if
/// [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder)
/// is used to encode the attachable value.
public struct JSONEncodingOptions: Sendable {
/// The output format to produce.
public var outputFormatting: JSONEncoder.OutputFormatting

/// The strategy to use in encoding dates.
public var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy

/// The strategy to use in encoding binary data.
public var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy

/// The strategy to use in encoding non-conforming numbers.
public var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy

/// The strategy to use for encoding keys.
public var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy
}

/// JSON encoding options to use if [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder)
/// is used to encode the attachable value.
///
/// The default value of this property is `nil`, meaning that the default
/// options are used when encoding an attachable value as JSON. If an
/// attachable value is encoded in a format other than JSON, the value of this
/// property is ignored.
public var jsonEncodingOptions: JSONEncodingOptions?

/// A user info dictionary to provide to the property list encoder or JSON
/// encoder when encoding the attachable value.
///
/// The value of this property is ignored when encoding an attachable value
/// that conforms to [`NSSecureCoding`](https://developer.apple.com/documentation/foundation/nssecurecoding)
/// but does not conform to [`Encodable`](https://developer.apple.com/documentation/swift/encodable).
public var userInfo: [CodingUserInfoKey: any Sendable]

public init(format: Format, jsonEncodingOptions: JSONEncodingOptions? = nil, userInfo: [CodingUserInfoKey: any Sendable] = [:]) {
self.format = format
self.jsonEncodingOptions = jsonEncodingOptions
self.userInfo = userInfo
}
}

// MARK: -

@_spi(Experimental)
extension EncodableAttachmentMetadata.JSONEncodingOptions {
public init(
outputFormatting: JSONEncoder.OutputFormatting? = nil,
dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil,
dataEncodingStrategy: JSONEncoder.DataEncodingStrategy? = nil,
nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy? = nil,
keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy? = nil
) {
self = .default
self.outputFormatting = outputFormatting ?? self.outputFormatting
self.dateEncodingStrategy = dateEncodingStrategy ?? self.dateEncodingStrategy
self.dataEncodingStrategy = dataEncodingStrategy ?? self.dataEncodingStrategy
self.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy ?? self.nonConformingFloatEncodingStrategy
self.keyEncodingStrategy = keyEncodingStrategy ?? self.keyEncodingStrategy
}

/// An instance of this type representing the default JSON encoding options
/// used by [`JSONEncoder`](https://developer.apple.com/documentation/foundation/jsonencoder).
public static let `default`: Self = {
// Get the default values from a real JSONEncoder for max authenticity!
let encoder = JSONEncoder()

return Self(
outputFormatting: encoder.outputFormatting,
dateEncodingStrategy: encoder.dateEncodingStrategy,
dataEncodingStrategy: encoder.dataEncodingStrategy,
nonConformingFloatEncodingStrategy: encoder.nonConformingFloatEncodingStrategy,
keyEncodingStrategy: encoder.keyEncodingStrategy
)
}()
}

// MARK: -

extension EncodableAttachmentMetadata.Format {
/// Initialize an instance of this type representing the content type or media
/// type of the specified attachment.
///
/// - Parameters:
/// - attachment: The attachment that will be encoded.
///
/// - Throws: If the attachment's content type or media type is unsupported.
init(for attachment: borrowing Attachment<some Attachable>) throws {
if let metadata = attachment.metadata {
if let format = metadata as? Self {
self = format
return
} else if let metadata = metadata as? EncodableAttachmentMetadata {
self = metadata.format
return
} else if let propertyListFormat = metadata as? PropertyListSerialization.PropertyListFormat {
self = .propertyListFormat(propertyListFormat)
return
}
}

let ext = (attachment.preferredName as NSString).pathExtension

#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
// If the caller explicitly wants to encode their data as either XML or as a
// property list, use PropertyListEncoder. Otherwise, we'll fall back to
// JSONEncoder below.
if #available(_uttypesAPI, *), let contentType = UTType(filenameExtension: ext) {
if contentType == .data {
self = .default
} else if contentType.conforms(to: .json) {
self = .json
} else if contentType.conforms(to: .xml) {
self = .propertyListFormat(.xml)
} else if contentType.conforms(to: .binaryPropertyList) || contentType == .propertyList {
self = .propertyListFormat(.binary)
} else if contentType.conforms(to: .propertyList) {
self = .propertyListFormat(.openStep)
} else {
let contentTypeDescription = contentType.localizedDescription ?? contentType.identifier
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The content type '\(contentTypeDescription)' cannot be used to attach an instance of \(type(of: self)) to a test."])
}
return
}
#endif

if ext.isEmpty {
// No path extension? No problem! Default data.
self = .default
} else if ext.caseInsensitiveCompare("plist") == .orderedSame {
self = .propertyListFormat(.binary)
} else if ext.caseInsensitiveCompare("xml") == .orderedSame {
self = .propertyListFormat(.xml)
} else if ext.caseInsensitiveCompare("json") == .orderedSame {
self = .json
} else {
throw CocoaError(.propertyListWriteInvalid, userInfo: [NSLocalizedDescriptionKey: "The path extension '.\(ext)' cannot be used to attach an instance of \(type(of: self)) to a test."])
}
}
}
#endif

This file was deleted.

18 changes: 18 additions & 0 deletions Sources/Testing/Attachments/Attachable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@
/// that conforms to ``AttachableContainer`` to act as a proxy.
@_spi(Experimental)
public protocol Attachable: ~Copyable {
/// A type containing additional metadata about an instance of this attachable
/// type that a developer can optionally include when creating an attachment.
///
/// Instances of this type can contain metadata that is not contained directly
/// in the attachable value itself. An instance of this type can be passed to
/// the initializers of ``Attachment`` and then accessed later via
/// ``Attachment/metadata``. Metadata is always optional; if your attachable
/// value _must_ include some value, consider adding it as a property of that
/// type instead of adding it as metadata.
///
/// When implementing ``withUnsafeBufferPointer(for:_:)``, you can access the
/// attachment's ``Attachment/metadata`` property to get the metadata that was
/// passed when the attachment was created.
///
/// By default, this type is equal to [`Never`](https://developer.apple.com/documentation/swift/never),
/// meaning that an attachable value has no metadata associated with it.
associatedtype AttachmentMetadata: Sendable & Copyable = Never

/// An estimate of the number of bytes of memory needed to store this value as
/// an attachment.
///
Expand Down
Loading