Skip to content

Commit b0e0cf1

Browse files
Events & Model Improvements (#82)
Add Events, EventBus, Listener Add ModelProperty Add Timestamps Add Model Events Add Model is more extendable Add Eager load sugar
1 parent 748dbce commit b0e0cf1

File tree

61 files changed

+1185
-821
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1185
-821
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ on:
99

1010
jobs:
1111
test-macos:
12-
if: ${{ false }} # disable until macOS 12 (with concurrency) runners are available.
1312
runs-on: macos-12
1413
env:
15-
DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer
14+
DEVELOPER_DIR: /Applications/Xcode_13.3.app/Contents/Developer
1615
steps:
1716
- uses: actions/checkout@v2
1817
- name: Build

Sources/Alchemy/Application/Application+Services.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extension Application {
3434
Loop.config()
3535
}
3636

37+
Container.bind(.singleton, value: EventBus())
3738
Container.bind(.singleton, value: Router())
3839
Container.bind(.singleton, value: Scheduler())
3940
Container.bind(.singleton) { container -> NIOThreadPool in

Sources/Alchemy/Auth/BasicAuthable.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ extension BasicAuthable {
106106
throw error
107107
}
108108

109-
let passwordHash = try firstRow.get(passwordKeyString).value.string()
109+
guard let passwordHash = try firstRow[passwordKeyString]?.string() else {
110+
throw DatabaseError("Missing column \(passwordKeyString) on row of type \(name(of: Self.self))")
111+
}
112+
110113
guard try verify(password: password, passwordHash: passwordHash) else {
111114
throw error
112115
}

Sources/Alchemy/Events/Event.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// An app-wide event fired by an `EventBus`.
2+
public protocol Event {
3+
/// The key for which the event is registered in the `EventBus`. Defaults to
4+
/// the type name.
5+
static var registrationKey: String { get }
6+
}
7+
8+
extension Event {
9+
public static var registrationKey: String { name(of: Self.self) }
10+
}
11+
12+
extension Event {
13+
/// Fire this event on an `EventBus`.
14+
public func fire(on events: EventBus = Events) async throws {
15+
try await events.fire(self)
16+
}
17+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import NIOConcurrencyHelpers
2+
3+
public final class EventBus: Service {
4+
public struct Identifier: ServiceIdentifier {
5+
private let hashable: AnyHashable
6+
public init(hashable: AnyHashable) { self.hashable = hashable }
7+
}
8+
9+
public enum Handler<E: Event>: AnyHandler {
10+
public typealias Closure = (E) async throws -> Void
11+
case closure(Closure)
12+
}
13+
14+
private var registeredHandlers: [String: [AnyHandler]] = [:]
15+
private var lock = Lock()
16+
17+
public func on<E: Event>(_ event: E.Type, action: @escaping Handler<E>.Closure) {
18+
let _handlers = lock.withLock { registeredHandlers[E.registrationKey] ?? [] }
19+
guard let existingHandlers = _handlers as? [Handler<E>] else { return }
20+
registeredHandlers[E.registrationKey] = existingHandlers + [.closure(action)]
21+
}
22+
23+
public func on<L: Listener>(listener: L.Type) {
24+
let _handlers = lock.withLock { registeredHandlers[L.ObservedEvent.registrationKey] ?? [] }
25+
guard let existingHandlers = _handlers as? [Handler<L.ObservedEvent>] else { return }
26+
registeredHandlers[L.ObservedEvent.registrationKey] = existingHandlers + [.closure {
27+
try await L(event: $0).handle()
28+
}]
29+
}
30+
31+
public func fire<E: Event>(_ event: E) async throws {
32+
let _handlers = lock.withLock { registeredHandlers[E.registrationKey] ?? [] }
33+
guard let handlers = _handlers as? [Handler<E>] else {
34+
return
35+
}
36+
37+
for handler in handlers {
38+
switch handler {
39+
case .closure(let closure):
40+
try await closure(event)
41+
}
42+
}
43+
}
44+
}
45+
46+
extension Listener {
47+
fileprivate func handle() async throws { try await run() }
48+
}
49+
50+
extension Listener where Self: Job {
51+
fileprivate func handle() async throws { try await dispatch() }
52+
}
53+
54+
private protocol AnyHandler {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// Processes an Event
2+
public protocol Listener {
3+
associatedtype ObservedEvent: Event
4+
init(event: ObservedEvent)
5+
func run() async throws
6+
}

Sources/Alchemy/SQL/Database/Core/DatabaseError.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ public struct DatabaseError: Error {
1010
init(_ message: String) {
1111
self.message = message
1212
}
13+
14+
static func missingColumn(_ column: String) -> DatabaseError {
15+
DatabaseError("Missing column named `\(column)`.")
16+
}
1317
}

Sources/Alchemy/SQL/Database/Core/SQL.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ extension SQL: SQLConvertible {
2020
}
2121

2222
extension SQL: SQLValueConvertible {
23-
public var value: SQLValue {
23+
public var sqlValue: SQLValue {
2424
.string(statement)
2525
}
2626
}
Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,65 @@
11
import Foundation
22

3-
/// A row of data returned from a database. Various database packages
4-
/// can use this as an abstraction around their internal row types.
5-
public protocol SQLRow {
6-
/// The `String` names of all columns that have values in this row.
7-
var columns: Set<String> { get }
3+
public struct SQLField: Equatable {
4+
public let column: String
5+
public let value: SQLValue
6+
}
7+
8+
/// A row of data returned by an SQL query.
9+
public struct SQLRow {
10+
public let fields: [SQLField]
11+
public let lookupTable: [String: Int]
812

9-
/// Get the `SQLValue` of a column from this row.
10-
///
11-
/// - Parameter column: The column to get the value for.
12-
/// - Throws: A `DatabaseError` if the column does not exist on
13-
/// this row.
14-
/// - Returns: The value at `column`.
15-
func get(_ column: String) throws -> SQLValue
13+
public var fieldDictionary: [String: SQLValue] {
14+
Dictionary(fields.map { ($0.column, $0.value) }, uniquingKeysWith: { current, _ in current })
15+
}
16+
17+
init(fields: [SQLField]) {
18+
self.fields = fields
19+
self.lookupTable = Dictionary(fields.enumerated().map { ($1.column, $0) }, uniquingKeysWith: { current, _ in current })
20+
}
21+
22+
public func contains(_ column: String) -> Bool {
23+
lookupTable[column] != nil
24+
}
1625

26+
public subscript(_ index: Int) -> SQLValue {
27+
fields[index].value
28+
}
29+
30+
public subscript(_ column: String) -> SQLValue? {
31+
guard let index = lookupTable[column] else { return nil }
32+
return fields[index].value
33+
}
34+
35+
public func require(_ column: String) throws -> SQLValue {
36+
try self[column].unwrap(or: DatabaseError.missingColumn(column))
37+
}
38+
}
39+
40+
extension SQLRow: ExpressibleByDictionaryLiteral {
41+
public init(dictionaryLiteral elements: (String, SQLValueConvertible)...) {
42+
self.init(fields: elements.enumerated().map { SQLField(column: $1.0, value: $1.1.sqlValue) })
43+
}
44+
}
45+
46+
extension SQLRow {
1747
/// Decode a `Model` type `D` from this row.
1848
///
1949
/// The default implementation of this function populates the
2050
/// properties of `D` with the value of the column named the
2151
/// same as the property.
2252
///
2353
/// - Parameter type: The type to decode from this row.
24-
func decode<D: Model>(_ type: D.Type) throws -> D
25-
}
26-
27-
extension SQLRow {
54+
public func decode<M: Model>(_ type: M.Type) throws -> M {
55+
try M(from: SQLRowDecoder(row: self, keyMapping: M.keyMapping, jsonDecoder: M.jsonDecoder))
56+
}
57+
2858
public func decode<D: Decodable>(
2959
_ type: D.Type,
3060
keyMapping: DatabaseKeyMapping = .useDefaultKeys,
3161
jsonDecoder: JSONDecoder = JSONDecoder()
3262
) throws -> D {
3363
try D(from: SQLRowDecoder(row: self, keyMapping: keyMapping, jsonDecoder: jsonDecoder))
3464
}
35-
36-
public func decode<M: Model>(_ type: M.Type) throws -> M {
37-
try M(from: SQLRowDecoder(row: self, keyMapping: M.keyMapping, jsonDecoder: M.jsonDecoder))
38-
}
39-
40-
/// Subscript for convenience access.
41-
public subscript(column: String) -> SQLValue? {
42-
columns.contains(column) ? try? get(column) : nil
43-
}
4465
}

Sources/Alchemy/SQL/Database/Core/SQLValue.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public enum SQLValue: Equatable, Hashable, CustomStringConvertible {
4545
return "SQLValue.null"
4646
}
4747
}
48+
49+
public static var now: SQLValue { .date(Date()) }
4850
}
4951

5052
/// Extension for easily accessing the unwrapped contents of an `SQLValue`.

0 commit comments

Comments
 (0)