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