Skip to content

Dedicated Module and Improvements to Conversions #365

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 13 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,24 @@ public struct AnyConversion<Input, Output>: 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.
///
Expand Down
51 changes: 51 additions & 0 deletions Sources/Conversions/Core/Conversion.swift
Original file line number Diff line number Diff line change
@@ -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<Input, Output> {
// 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")
}
}
26 changes: 26 additions & 0 deletions Sources/Conversions/Core/ConversionBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// ConversionBuilder.swift
// swift-parsing
//
// Created by Woodrow Melling on 10/24/24.
//

@resultBuilder
public enum ConversionBuilder{
public static func buildBlock<T>() -> Conversions.Identity<T> {
Conversions.Identity()
}
public static func buildPartialBlock<C: Conversion>(first conversion: C) -> C {
conversion
}

public static func buildPartialBlock<
C0: Conversion,
C1: Conversion
>(
accumulated c0: C0,
next c1: C1
) -> Conversions.Map<C0, C1> where C0.Output == C1.Input {
Conversions.Map(upstream: c0, downstream: c1)
}
}
37 changes: 37 additions & 0 deletions Sources/Conversions/Core/Inverted.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Inverted.swift
// swift-parsing
//
// Created by Woodrow Melling on 10/28/24.
//

extension Conversions {
public struct Inverted<C: Conversion>: 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<Self> {
Conversions.Inverted(self)
}
}
File renamed without changes.
25 changes: 25 additions & 0 deletions Sources/Conversions/DataString.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
File renamed without changes.
File renamed without changes.
108 changes: 108 additions & 0 deletions Sources/Conversions/MapValues.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// MapValues.swift
// OpenFestival
//
// Created by Woodrow Melling on 10/31/24.
//



extension Conversions {
public struct MapValues<C: Conversion>: 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<KeyConversion: Conversion, ValueConversion: Conversion>: 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<NewKey, NewValue>(
_ keyTransform: @escaping (Key) throws -> NewKey,
_ valueTransform: @escaping (Value) throws -> NewValue
) rethrows -> [NewKey: NewValue] {
return try Dictionary<NewKey, NewValue>(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<T: Sendable>(
_ 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)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ extension Conversion {
/// struct back into a tuple of values.
@inlinable
public static func memberwise<Values, Struct>(
_ initializer: @escaping (Values) -> Struct
_ initializer: @escaping @Sendable (Values) -> Struct
) -> Self where Self == Conversions.Memberwise<Values, Struct> {
.init(initializer: initializer)
}
Expand All @@ -126,10 +126,10 @@ extension Conversion {
extension Conversions {
public struct Memberwise<Values, Struct>: 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
}

Expand Down
Loading