diff --git a/Package.resolved b/Package.resolved index 908ee008e9..2a874dae52 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "5eadfee9e0b253b1df4d25252f723cb36734ddef72daf92d852b242f15fa076d", "pins" : [ { "identity" : "swift-argument-parser", @@ -27,6 +28,15 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, { "identity" : "swift-docc-plugin", "kind" : "remoteSourceControl", @@ -41,10 +51,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7", - "version" : "1.0.0" + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 6a6a93bba5..d2b4dd1389 100644 --- a/Package.swift +++ b/Package.swift @@ -14,17 +14,30 @@ let package = Package( .library( name: "Parsing", targets: ["Parsing"] - ) + ), + .library(name: "Conversions", targets: ["Parsing"]) ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), .package(url: "https://github.com/google/swift-benchmark", from: "0.1.1"), ], targets: [ .target( name: "Parsing", - dependencies: [.product(name: "CasePaths", package: "swift-case-paths")] + dependencies: [ + .product(name: "CasePaths", package: "swift-case-paths"), + .product(name: "CustomDump", package: "swift-custom-dump"), + .target(name: "Conversions") + ] + ), + .target( + name: "Conversions", + dependencies: [ + .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "CasePaths", package: "swift-case-paths") + ] ), .testTarget( name: "ParsingTests", diff --git a/Sources/Parsing/Conversions/BinaryFloatingPoint.swift b/Sources/Conversions/BinaryFloatingPoint.swift similarity index 100% rename from Sources/Parsing/Conversions/BinaryFloatingPoint.swift rename to Sources/Conversions/BinaryFloatingPoint.swift diff --git a/Sources/Parsing/ConvertingError.swift b/Sources/Conversions/ConvertingError.swift similarity index 100% rename from Sources/Parsing/ConvertingError.swift rename to Sources/Conversions/ConvertingError.swift diff --git a/Sources/Parsing/Conversions/AnyConversion.swift b/Sources/Conversions/Core/AnyConversion.swift similarity index 89% rename from Sources/Parsing/Conversions/AnyConversion.swift rename to Sources/Conversions/Core/AnyConversion.swift index 17a0a7f01c..43973a43e3 100644 --- a/Sources/Parsing/Conversions/AnyConversion.swift +++ b/Sources/Conversions/Core/AnyConversion.swift @@ -121,6 +121,24 @@ public struct AnyConversion: Conversion { self._unapply = conversion.unapply } + + /// Creates a conversion that wraps the given closures in its ``apply(_:)`` and ``unapply(_:)`` + /// methods + /// + /// - Parameters: + /// - apply: A closure that attempts to convert an input into an output. `apply` is executed + /// each time the ``apply(_:)`` method is called on the resulting conversion + /// - unapply: A closure that attempts to convert an output into an input. `unapply` is executed + /// each time the ``unapply(_:)`` method is called on the resulting conversion. + @inlinable + public init( + apply: @escaping (Input) throws -> Output, + unapply: @escaping (Output) throws -> Input + ) { + self._apply = apply + self._unapply = unapply + } + /// Creates a conversion that wraps the given closures in its ``apply(_:)`` and ``unapply(_:)`` /// methods, throwing an error when `nil` is returned. /// diff --git a/Sources/Conversions/Core/Conversion.swift b/Sources/Conversions/Core/Conversion.swift new file mode 100644 index 0000000000..52fa8418e6 --- /dev/null +++ b/Sources/Conversions/Core/Conversion.swift @@ -0,0 +1,51 @@ + +/// Declares a type that can asynchronously transform an `Input` value into an `Output` value *and* transform an +/// `Output` value back into an `Input` value. +/// +/// Useful in bidirectionally tranforming types, like when writing something to the disk. +/// printability via ``Parser/map(_:)-18m9d``. +@rethrows public protocol Conversion { + // The type of values this conversion converts from. + associatedtype Input + + // The type of values this conversion converts to. + associatedtype Output + + associatedtype Body + + /// Attempts to asynchronously transform an input into an output. + /// + /// See ``Conversion/apply(_:)`` for the reverse process. + /// + /// - Parameter input: An input value. + /// - Returns: A transformed output value. + @Sendable func apply(_ input: Input) throws -> Output + + /// Attempts to asynchronously transform an output back into an input. + /// + /// The reverse process of ``Conversion/apply(_:)``. + /// + /// - Parameter output: An output value. + /// - Returns: An "un"-transformed input value. + @Sendable func unapply(_ input: Output) throws -> Input + + @ConversionBuilder + var body: Body { get } +} + +extension Conversion +where Body: Conversion, Body.Input == Input, Body.Output == Output { + public func apply(_ input: Input) throws -> Output { + try self.body.apply(input) + } + + public func unapply(_ output: Output) throws -> Input { + try self.body.unapply(output) + } +} + +extension Conversion where Body == Never { + public var body: Body { + return fatalError("Body of \(Self.self) should never be called") + } +} diff --git a/Sources/Conversions/Core/ConversionBuilder.swift b/Sources/Conversions/Core/ConversionBuilder.swift new file mode 100644 index 0000000000..6fdf757937 --- /dev/null +++ b/Sources/Conversions/Core/ConversionBuilder.swift @@ -0,0 +1,26 @@ +// +// ConversionBuilder.swift +// swift-parsing +// +// Created by Woodrow Melling on 10/24/24. +// + +@resultBuilder +public enum ConversionBuilder{ + public static func buildBlock() -> Conversions.Identity { + Conversions.Identity() + } + public static func buildPartialBlock(first conversion: C) -> C { + conversion + } + + public static func buildPartialBlock< + C0: Conversion, + C1: Conversion + >( + accumulated c0: C0, + next c1: C1 + ) -> Conversions.Map where C0.Output == C1.Input { + Conversions.Map(upstream: c0, downstream: c1) + } +} diff --git a/Sources/Parsing/Conversions/ConversionMap.swift b/Sources/Conversions/Core/ConversionMap.swift similarity index 100% rename from Sources/Parsing/Conversions/ConversionMap.swift rename to Sources/Conversions/Core/ConversionMap.swift diff --git a/Sources/Parsing/Conversions/Conversions.swift b/Sources/Conversions/Core/Conversions.swift similarity index 100% rename from Sources/Parsing/Conversions/Conversions.swift rename to Sources/Conversions/Core/Conversions.swift diff --git a/Sources/Conversions/Core/Inverted.swift b/Sources/Conversions/Core/Inverted.swift new file mode 100644 index 0000000000..7f992a8800 --- /dev/null +++ b/Sources/Conversions/Core/Inverted.swift @@ -0,0 +1,37 @@ +// +// Inverted.swift +// swift-parsing +// +// Created by Woodrow Melling on 10/28/24. +// + +extension Conversions { + public struct Inverted: Conversion { + public var conversion: C + + @inlinable + public init(_ conversion: C) { + self.conversion = conversion + } + + @inlinable + @inline(__always) + public func apply(_ input: C.Output) throws -> C.Input { + try conversion.unapply(input) + } + + @inlinable + @inline(__always) + public func unapply(_ output: C.Input) throws -> C.Output { + try conversion.apply(output) + } + } +} + +public extension Conversion { + @inlinable + @inline(__always) + func inverted() -> Conversions.Inverted { + Conversions.Inverted(self) + } +} diff --git a/Sources/Parsing/Conversions/Data.swift b/Sources/Conversions/Data.swift similarity index 100% rename from Sources/Parsing/Conversions/Data.swift rename to Sources/Conversions/Data.swift diff --git a/Sources/Conversions/DataString.swift b/Sources/Conversions/DataString.swift new file mode 100644 index 0000000000..58ac8a27c6 --- /dev/null +++ b/Sources/Conversions/DataString.swift @@ -0,0 +1,25 @@ +// +// DataString.swift +// OpenFestival +// +// Created by Woodrow Melling on 10/25/24. +// + +import Foundation + + +extension Conversions { + public struct DataToString: Conversion { + public typealias Input = Data + public typealias Output = String + public init() {} + + public func apply(_ input: Data) -> String { + String(decoding: input, as: UTF8.self) + } + + public func unapply(_ output: String) -> Data { + Data(output.utf8) + } + } +} diff --git a/Sources/Parsing/EmptyInitializable.swift b/Sources/Conversions/EmptyInitializable.swift similarity index 100% rename from Sources/Parsing/EmptyInitializable.swift rename to Sources/Conversions/EmptyInitializable.swift diff --git a/Sources/Parsing/Conversions/Enum.swift b/Sources/Conversions/Enum.swift similarity index 100% rename from Sources/Parsing/Conversions/Enum.swift rename to Sources/Conversions/Enum.swift diff --git a/Sources/Parsing/Conversions/FixedWidthInteger.swift b/Sources/Conversions/FixedWidthInteger.swift similarity index 100% rename from Sources/Parsing/Conversions/FixedWidthInteger.swift rename to Sources/Conversions/FixedWidthInteger.swift diff --git a/Sources/Parsing/Conversions/Identity.swift b/Sources/Conversions/Identity.swift similarity index 100% rename from Sources/Parsing/Conversions/Identity.swift rename to Sources/Conversions/Identity.swift diff --git a/Sources/Parsing/Conversions/JSON.swift b/Sources/Conversions/JSON.swift similarity index 100% rename from Sources/Parsing/Conversions/JSON.swift rename to Sources/Conversions/JSON.swift diff --git a/Sources/Parsing/Conversions/LosslessStringConvertible.swift b/Sources/Conversions/LosslessStringConvertible.swift similarity index 100% rename from Sources/Parsing/Conversions/LosslessStringConvertible.swift rename to Sources/Conversions/LosslessStringConvertible.swift diff --git a/Sources/Conversions/MapValues.swift b/Sources/Conversions/MapValues.swift new file mode 100644 index 0000000000..35e86d29ab --- /dev/null +++ b/Sources/Conversions/MapValues.swift @@ -0,0 +1,108 @@ +// +// MapValues.swift +// OpenFestival +// +// Created by Woodrow Melling on 10/31/24. +// + + + +extension Conversions { + public struct MapValues: Conversion { + var transform: C + + public init(_ transform: C) { + self.transform = transform + } + + public init(@ConversionBuilder _ build: () -> C) { + self.transform = build() + } + + public func apply(_ input: [C.Input]) throws -> [C.Output] { + try input.map(transform.apply) + } + + public func unapply(_ output: [C.Output]) throws -> [C.Input] { + try output.map(transform.unapply) + } + } + +} + + + +extension Conversions { + public struct MapKVPairs: Conversion + where KeyConversion.Input: Hashable & Sendable, + KeyConversion.Output: Hashable & Sendable, + ValueConversion.Input: Sendable, + ValueConversion.Output: Sendable + { + public typealias Input = [KeyConversion.Input: ValueConversion.Input] + public typealias Output = [KeyConversion.Output: ValueConversion.Output] + + var keyConversion: KeyConversion + var valueConversion: ValueConversion + + public init(keyConversion: KeyConversion, valueConversion: ValueConversion) { + self.keyConversion = keyConversion + self.valueConversion = valueConversion + } + + public func apply(_ input: Input) throws -> Output { + try input.mapKVPairs( + keyConversion.apply, + valueConversion.apply + ) + } + + public func unapply(_ output: Output) throws -> Input { + try output.mapKVPairs( + keyConversion.unapply, + valueConversion.unapply + ) + } + } +} + +extension Dictionary { + func mapKVPairs( + _ keyTransform: @escaping (Key) throws -> NewKey, + _ valueTransform: @escaping (Value) throws -> NewValue + ) rethrows -> [NewKey: NewValue] { + return try Dictionary(uniqueKeysWithValues: self.map { + return try (keyTransform($0.0), valueTransform($0.1)) + }) + } +} + +extension Sequence { + /// - Parameters: + /// - closure: Transformation to apply to each element + /// - Returns: Array of transformed elements in original order + public func concurrentMap( + _ transform: @escaping @Sendable (Element) async throws -> T + ) async rethrows -> [T] where Element: Sendable { + return try await withThrowingTaskGroup(of: (value: T, offset: Int).self) { group in + for (id, element) in self.enumerated() { + group.addTask { + try await (value: transform(element), offset: id) + } + } + + var array: [(value: T, offset: Int)] = [] + array.reserveCapacity(self.underestimatedCount) + for try await result in group { + array.append(result) + } + + // Could this sort be avoided somehow? Maybe with an OrderedDictionary? + return array.sorted { lhs, rhs in + lhs.offset < rhs.offset + } + .map(\.value) + } + } + +} diff --git a/Sources/Parsing/Conversions/Memberwise.swift b/Sources/Conversions/Memberwise.swift similarity index 97% rename from Sources/Parsing/Conversions/Memberwise.swift rename to Sources/Conversions/Memberwise.swift index a1d1925950..fae0fff6be 100644 --- a/Sources/Parsing/Conversions/Memberwise.swift +++ b/Sources/Conversions/Memberwise.swift @@ -117,7 +117,7 @@ extension Conversion { /// struct back into a tuple of values. @inlinable public static func memberwise( - _ initializer: @escaping (Values) -> Struct + _ initializer: @escaping @Sendable (Values) -> Struct ) -> Self where Self == Conversions.Memberwise { .init(initializer: initializer) } @@ -126,10 +126,10 @@ extension Conversion { extension Conversions { public struct Memberwise: Conversion { @usableFromInline - let initializer: (Values) -> Struct + let initializer: @Sendable (Values) -> Struct @usableFromInline - init(initializer: @escaping (Values) -> Struct) { + init(initializer: @escaping @Sendable (Values) -> Struct) { self.initializer = initializer } diff --git a/Sources/Conversions/ParseableFormatStyleConversion.swift b/Sources/Conversions/ParseableFormatStyleConversion.swift new file mode 100644 index 0000000000..5b53d7a179 --- /dev/null +++ b/Sources/Conversions/ParseableFormatStyleConversion.swift @@ -0,0 +1,65 @@ +//#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS) +// import Foundation +// +// @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +// extension Conversion { +// /// A conversion that wraps a parseable format style. +// /// +// /// This conversion forwards its ``apply(_:)`` and ``unapply(_:)`` methods to the underlying +// /// `ParseableFormatStyle` by invoking its parse strategy's `parse` method and its `format` +// /// method. +// /// +// /// See ``formatted(_:)-swift.method`` for a fluent version of this interface that transforms an +// /// existing conversion. +// /// +// /// - Parameter style: A parseable format style. +// /// - Returns: A conversion from a string to the given type. +// public static func formatted