Skip to content

Commit badf6a0

Browse files
committed
Some tests
1 parent ab5a8b1 commit badf6a0

14 files changed

Lines changed: 371 additions & 33 deletions

Sources/Feather/Queries/Fail.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ extension Queries {
1313
/// The error to throw on execution
1414
let error: any Error
1515

16+
/// Initializes a query that always fails with an error.
17+
/// This is useful for unit tests and previews to test
18+
/// how a part of an application behaives when an error
19+
/// is thrown.
20+
///
21+
/// - Parameter error: The error to throw
1622
public init(_ error: any Error) {
1723
self.error = error
1824
}
@@ -34,9 +40,9 @@ extension Queries {
3440

3541
func start(
3642
onChange: @escaping (Output) -> Void,
37-
onError: @escaping (any Error) -> Void
43+
onComplete: @escaping (Error?) -> Void
3844
) {
39-
onError(error)
45+
onComplete(error)
4046
}
4147

4248
func cancel() {}

Sources/Feather/Queries/Just.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ extension Queries {
5656

5757
func start(
5858
onChange: @escaping (Output) -> Void,
59-
onError: @escaping (any Error) -> Void
59+
onComplete: @escaping (Error?) -> Void
6060
) {
6161
onChange(output)
62+
// Complete instantly
63+
onComplete(nil)
6264
}
6365

6466
func cancel() {}

Sources/Feather/Queries/Map.swift

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,34 @@ extension Queries {
1313
/// The upstream query to transform
1414
let base: Base
1515
/// The transform to apply to the output
16-
let transform: @Sendable (Base.Output) throws -> Output
16+
let transform: @Sendable (Base.Input, Base.Output) throws -> Output
1717

1818
public func execute(with input: Base.Input) async throws -> Output {
19-
try await transform(base.execute(with: input))
19+
try await transform(input, base.execute(with: input))
2020
}
2121

2222
public func observe(with input: Base.Input) -> any QueryObservation<Output> {
23-
return Observation(base: base.observe(with: input), transform: transform)
23+
return Observation(base: base.observe(with: input), input: input, transform: transform)
2424
}
2525

2626
struct Observation: QueryObservation {
2727
let base: any QueryObservation<Base.Output>
28+
let input: Base.Input
2829
/// The transform to apply to the output
29-
let transform: @Sendable (Base.Output) throws -> Output
30+
let transform: @Sendable (Base.Input, Base.Output) throws -> Output
3031

3132
func start(
3233
onChange: @escaping @Sendable (Output) -> Void,
33-
onError: @escaping @Sendable (any Error) -> Void
34+
onComplete: @escaping @Sendable (Error?) -> Void
3435
) {
3536
base.start { upstream in
3637
do {
37-
try onChange(transform(upstream))
38+
try onChange(transform(input, upstream))
3839
} catch {
39-
onError(error)
40+
onComplete(error)
4041
}
41-
} onError: { error in
42-
onError(error)
42+
} onComplete: { error in
43+
onComplete(error)
4344
}
4445
}
4546

@@ -67,7 +68,7 @@ extension Queries.Map: DatabaseQuery where Base: DatabaseQuery {
6768
with input: Base.Input,
6869
tx: borrowing Transaction
6970
) throws -> Output {
70-
return try transform(base.execute(with: input, tx: tx))
71+
return try transform(input, base.execute(with: input, tx: tx))
7172
}
7273
}
7374

@@ -79,20 +80,43 @@ public extension Query {
7980
func map<NewOutput>(
8081
_ transform: @Sendable @escaping (Output) throws -> NewOutput
8182
) -> Queries.Map<Self, NewOutput> {
82-
return Queries.Map(base: self, transform: transform)
83+
return Queries.Map(base: self) { _, entity in try transform(entity) }
8384
}
8485

8586

86-
/// If a `nil` value is returned from the query, then a `FeatherError.entityWasNotFound`
87-
/// will be thrown instead.
87+
/// If a `nil` value is returned from the query, then an will throw an error.
8888
///
89+
/// The input closure for the error takes the `input` as a parameter.
90+
/// This allows for less ambiguous erros to be thrown by passing an id
91+
/// or any other identifying information.
92+
///
93+
/// ```swift
94+
/// query.throwIfNotFound { id in NotFound(id: id) }
95+
/// ```
96+
///
97+
/// If the `error` is `nil` then it will default to a `entityNotFound` erro
98+
/// to be thrown.
99+
///
100+
/// - Parameter error: A closure to construct the error to be thrown.
89101
/// - Returns: A query with a non optional result type, that will throw in case of an error.
90-
func throwIfNotFound<Wrapped>() -> Queries.Map<Self, Wrapped> where Output == Wrapped? {
91-
return Queries.Map(base: self) { entity in
92-
guard let entity else {
93-
throw FeatherError.entityWasNotFound
94-
}
95-
102+
func throwIfNotFound<Wrapped>(
103+
_ error: (@Sendable (Input) -> Error)? = nil
104+
) -> Queries.Map<Self, Wrapped> where Output == Wrapped? {
105+
return Queries.Map(base: self) { input, entity in
106+
guard let entity else { throw error?(input) ?? FeatherError.entityWasNotFound }
107+
return entity
108+
}
109+
}
110+
111+
/// If a `nil` value is returned from the query, then an will throw an error.
112+
///
113+
/// - Parameter error: The error to throw if `nil`
114+
/// - Returns: A query with a non optional result type, that will throw in case of an error.
115+
func throwIfNotFound<Wrapped>(
116+
_ error: @Sendable @autoclosure @escaping () -> Error
117+
) -> Queries.Map<Self, Wrapped> where Output == Wrapped? {
118+
return Queries.Map(base: self) { input, entity in
119+
guard let entity else { throw error() }
96120
return entity
97121
}
98122
}
@@ -107,7 +131,7 @@ public extension Query {
107131
func replaceNil<Wrapped>(
108132
with value: @Sendable @autoclosure @escaping () -> Wrapped
109133
) -> Queries.Map<Self, Wrapped> where Output == Wrapped? {
110-
return Queries.Map(base: self) { entity in
134+
return Queries.Map(base: self) { _, entity in
111135
return entity ?? value()
112136
}
113137
}

Sources/Feather/Queries/MapInput.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ extension Queries.MapInput: DatabaseQuery where Base: DatabaseQuery {
4747

4848
public extension Query {
4949
/// Transforms the input value before passing it to the query.
50+
/// Allows you to change the input type of a query. Useful if
51+
/// merging multiple queries together using `then`.
5052
///
5153
/// - Parameter transform: The closure to transform the input
5254
/// - Returns: A query with a input type of the resulting closure.

Sources/Feather/Queries/Test.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ extension Queries {
3535
public private(set) var cancelObservationCallCount = 0
3636
private let lock = NSLock()
3737

38-
public init(execute:@escaping @Sendable (Input) throws -> Output) {
38+
public init(execute: @escaping @Sendable (Input) throws -> Output) {
3939
self.execute = execute
4040
}
4141

@@ -75,14 +75,15 @@ extension Queries {
7575

7676
func start(
7777
onChange: @escaping (Output) -> Void,
78-
onError: @escaping (any Error) -> Void
78+
onComplete: @escaping (Error?) -> Void
7979
) {
8080
query.incrementStartObservationCallCount()
8181

8282
do {
8383
try onChange(query.execute(input))
84+
onComplete(nil)
8485
} catch {
85-
onError(error)
86+
onComplete(error)
8687
}
8788
}
8889

Sources/Feather/QueryObservation.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public final class DatabaseQueryObservation<Query>: DatabaseSubscriber, QueryObs
1616
private let queue = Queue()
1717

1818
private var onChange: (@Sendable (Query.Output) -> Void)?
19-
private var onError: (@Sendable (Error) -> Void)?
19+
private var onComplete: (@Sendable (Error?) -> Void)?
2020

2121
init(
2222
query: Query,
@@ -35,7 +35,7 @@ public final class DatabaseQueryObservation<Query>: DatabaseSubscriber, QueryObs
3535
public func cancel() {
3636
lock.withLock {
3737
onChange = nil
38-
onError = nil
38+
onComplete = nil
3939
}
4040

4141
query.connection.cancel(subscriber: self)
@@ -44,11 +44,11 @@ public final class DatabaseQueryObservation<Query>: DatabaseSubscriber, QueryObs
4444

4545
public func start(
4646
onChange: @escaping @Sendable (Query.Output) -> Void,
47-
onError: @escaping @Sendable (Error) -> Void
47+
onComplete: @escaping @Sendable (Error?) -> Void
4848
) {
4949
lock.withLock {
5050
self.onChange = onChange
51-
self.onError = onError
51+
self.onComplete = onComplete
5252
}
5353

5454
query.connection.observe(subscriber: self)
@@ -68,7 +68,7 @@ public final class DatabaseQueryObservation<Query>: DatabaseSubscriber, QueryObs
6868
let output = try await query.execute(with: input)
6969
onChange(output)
7070
} catch {
71-
onError?(error)
71+
onComplete?(error)
7272
cancel()
7373
}
7474
}
@@ -85,7 +85,7 @@ public protocol QueryObservation<Output>: Sendable, AsyncSequence {
8585

8686
func start(
8787
onChange: @escaping @Sendable (Output) -> Void,
88-
onError: @escaping @Sendable (Error) -> Void
88+
onComplete: @escaping @Sendable (Error?) -> Void
8989
)
9090

9191
func cancel()
@@ -100,7 +100,7 @@ extension QueryObservation {
100100
AsyncThrowingStream<Output, Error> { continuation in
101101
start { output in
102102
continuation.yield(output)
103-
} onError: { error in
103+
} onComplete: { error in
104104
continuation.finish(throwing: error)
105105
}
106106

Tests/FeatherTests/ConnectionPoolTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,17 @@ struct ConnectionPoolTests {
4242
}
4343
}
4444
}
45+
46+
@Test func modificationsAreRolledBackOnError() async throws {
47+
let db = try TestDB.inMemory()
48+
struct Err: Error {}
49+
50+
try? await db.connection.begin(.write) { tx in
51+
try db.insertFoo.execute(with: 1, tx: tx)
52+
throw Err()
53+
}
54+
55+
let foos = try await db.selectFoos.execute()
56+
#expect(foos.isEmpty)
57+
}
4558
}

Tests/FeatherTests/FailTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// FailTests.swift
3+
// Feather
4+
//
5+
// Created by Wes Wickwire on 6/16/25.
6+
//
7+
8+
import Testing
9+
@testable import Feather
10+
11+
@Suite
12+
struct FailTests {
13+
struct ExpectedError: Equatable, Error {}
14+
15+
@Test func executeThrowsInputError() async {
16+
let query = Queries.Fail<(), ()>(ExpectedError())
17+
18+
await #expect(throws: ExpectedError.self) {
19+
try await query.execute()
20+
}
21+
}
22+
23+
@Test func observeThrowsInputError() async {
24+
let query = Queries.Fail<(), ()>(ExpectedError())
25+
26+
await #expect(throws: ExpectedError.self) {
27+
for try await _ in query.observe() {}
28+
}
29+
}
30+
}

Tests/FeatherTests/JustTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// JustTests.swift
3+
// Feather
4+
//
5+
// Created by Wes Wickwire on 6/16/25.
6+
//
7+
8+
import Testing
9+
@testable import Feather
10+
11+
@Suite
12+
struct JustTests {
13+
@Test func executeReturnsDefinedOutput() async throws {
14+
let output = try await Queries.Just<Int, String>("foo").execute(with: 1)
15+
#expect(output == "foo")
16+
}
17+
18+
@Test func observeReturnsOutputOnceAndFinishes() async throws {
19+
let query = Queries.Just<Int, String>("foo")
20+
var count = 0
21+
22+
for try await value in query.observe(with: 1) {
23+
count += 1
24+
#expect(value == "foo")
25+
}
26+
27+
#expect(count == 1)
28+
}
29+
30+
@Test func arrayOutputDefaultInitIsEmpty() async throws {
31+
let query = Queries.Just<(), [String]>()
32+
let output = try await query.execute()
33+
#expect(output == [])
34+
}
35+
36+
@Test func optionalOutputDefaultInitIsNil() async throws {
37+
let query = Queries.Just<(), String?>()
38+
let output = try await query.execute()
39+
#expect(output == nil)
40+
}
41+
42+
@Test func voidOutputDefaultInitIsVoid() async throws {
43+
// Silly test but i dont want to delete it on accident
44+
let _ = Queries.Just<(), ()>()
45+
}
46+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// MapInputTests.swift
3+
// Feather
4+
//
5+
// Created by Wes Wickwire on 6/16/25.
6+
//
7+
8+
import Testing
9+
@testable import Feather
10+
11+
@Suite
12+
struct MapInputTests {
13+
@Test func mapInputMapsInput() async throws {
14+
let query = Queries.Just<String, Int>(100)
15+
let newInput: any Query<Int, Int> = query.mapInput(to: Int.self) { $0.description }
16+
let output = try await newInput.execute(with: 1)
17+
#expect(output == 100)
18+
}
19+
}
20+

0 commit comments

Comments
 (0)