Skip to content

Commit

Permalink
Add support for custom hard failures
Browse files Browse the repository at this point in the history
  • Loading branch information
kyouko-taiga committed Nov 7, 2022
1 parent 63da2be commit 36ac47e
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 8 deletions.
4 changes: 2 additions & 2 deletions Sources/Durian/Choose.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ where First.Context == Second.Context,
/// The second combinator.
public let secondCombinator: Second

/// Creates a combinator that applies `first`, or backtracks and applies `second` if `first`
/// returned a soft failure.
/// Creates a combinator that applies `first`, or backtracks and applies `second` when `first`
/// returns a soft failure.
public init(_ first: First, or second: Second) {
self.firstCombinator = first
self.secondCombinator = second
Expand Down
17 changes: 13 additions & 4 deletions Sources/Durian/Combinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,23 @@ extension Combinator {
Combine(self, and: other)
}

/// Creates a combinator that applies `self`, or backtracks and applies `other` if `self`
/// returned a soft failure.
/// Creates a combinator that applies `self` and then `other`, producing hard failures with
/// `makeHardFailure` when `other` returns a soft failure .
public func and<Other: Combinator>(
_ other: Other,
else makeHardFailure: @escaping (inout Context) -> Error
) -> Combine<Self, Other> {
Combine(self, and: other, else: makeHardFailure)
}

/// Creates a combinator that applies `self`, or backtracks and applies `other` when `self`
/// return a soft failure.
public func or<Other: Combinator>(_ other: Other) -> Choose<Self, Other> {
Choose(self, or: other)
}

/// Creates a combinator that applies `self`, or backtracks and applies `other` if `self`
/// returned any kind of failure.
/// Creates a combinator that applies `self`, or backtracks and applies `other` when `self`
/// returns any kind of failure.
public func orCatch<Other: Combinator>(
andApply other: Other
) -> TryCatch<Self, Other> {
Expand Down
16 changes: 15 additions & 1 deletion Sources/Durian/Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,32 @@ where First.Context == Second.Context
/// The second combinator.
public let secondCombinator: Second

/// A closure that produces a hard failure when `secondCombinator` returns a soft failure.
public let makeHardFailure: (inout Context) -> Error

/// Creates a combinator that applies `first` and then `second`.
public init(_ first: First, and second: Second) {
self.init(first, and: second, else: { _ in HardFailure() })
}

/// Creates a combinator that applies `first` and then `second`, producing hard failures with
/// `makeHardFailure` when `second` returns a soft failure.
public init(
_ first: First,
and second: Second,
else makeHardFailure: @escaping (inout Context) -> Error
) {
self.firstCombinator = first
self.secondCombinator = second
self.makeHardFailure = makeHardFailure
}

public func parse(_ context: inout Context) throws -> Element? {
if let a = try firstCombinator.parse(&context) {
if let b = try secondCombinator.parse(&context) {
return (a, b)
} else {
throw HardFailure()
throw makeHardFailure(&context)
}
} else {
return nil
Expand Down
11 changes: 10 additions & 1 deletion Sources/Durian/Maybe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ public struct Maybe<Base: Combinator>: Combinator where Base.Context: Restorable
/// `self` or `other` did.
public func andCollapsingSoftFailures<Other: Combinator>(
_ other: Other
) -> Apply<Base.Context, (Element, Other.Element)> where Other.Context == Context {
andCollapsingSoftFailures(other, else: { _ in HardFailure() })
}

/// Creates a combinator that applies `self` and then `other` without committing unless either
/// `self` or `other` did, producing hard failures with `makeHardFailure`.
public func andCollapsingSoftFailures<Other: Combinator>(
_ other: Other,
else makeHardFailure: @escaping (inout Context) -> Error
) -> Apply<Base.Context, (Element, Other.Element)> where Other.Context == Context {
Apply({ (context) in
if let a = try self.parse(&context) {
Expand All @@ -35,7 +44,7 @@ public struct Maybe<Base: Combinator>: Combinator where Base.Context: Restorable
} else if a == nil {
return nil
} else {
throw HardFailure()
throw makeHardFailure(&context)
}
} else {
return nil
Expand Down
51 changes: 51 additions & 0 deletions Tests/DurianTests/DurianTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ final class DurianTests: XCTestCase {
XCTAssertEqual(context, "c")
}

func testCombineWithCustomHardFailure() throws {
let a = PopFirst<Substring>(if: { $0 == "a" })
let b = PopFirst<Substring>(if: { $0 == "b" })
let aAndB = a.and(b, else: { _ in TaggedError(tag: 42) })

let input = "ac"
var context = input.suffix(from: input.startIndex)
do {
_ = try aAndB.parse(&context)
XCTFail("expected hard failure")
} catch let error {
let e = try XCTUnwrap(error as? TaggedError)
XCTAssertEqual(e.tag, 42)
XCTAssertEqual(context, "c")
}
}

func testMaybeSucess() {
let a = PopFirst<Substring>(if: { $0 == "a" })
let maybeA = maybe(a)
Expand All @@ -91,6 +108,34 @@ final class DurianTests: XCTestCase {
XCTAssertEqual(context, "bac")
}

func testMaybeCollapsing() {
let a = PopFirst<Substring>(if: { $0 == "a" })
let b = PopFirst<Substring>(if: { $0 == "b" })
let maybeAAndB = maybe(a).andCollapsingSoftFailures(b)

let input = "cc"
var context = input.suffix(from: input.startIndex)
XCTAssertNil(try maybeAAndB.parse(&context))
XCTAssertEqual(context, "cc")
}

func testMaybeCollapsingWithCustomHardFailure() throws {
let a = PopFirst<Substring>(if: { $0 == "a" })
let b = PopFirst<Substring>(if: { $0 == "b" })
let maybeAAndB = maybe(a).andCollapsingSoftFailures(b, else: { _ in TaggedError(tag: 42) })

let input = "ac"
var context = input.suffix(from: input.startIndex)
do {
_ = try maybeAAndB.parse(&context)
XCTFail("expected hard failure")
} catch let error {
let e = try XCTUnwrap(error as? TaggedError)
XCTAssertEqual(e.tag, 42)
XCTAssertEqual(context, "c")
}
}

func testOneOrMany() {
let a = PopFirst<Substring>(if: { $0 == "a" })
let oneOrManyA = a+
Expand Down Expand Up @@ -201,6 +246,12 @@ final class DurianTests: XCTestCase {

}

struct TaggedError: Error {

let tag: Int

}

extension Substring: Restorable {

public func backup() -> Substring { self }
Expand Down

0 comments on commit 36ac47e

Please sign in to comment.