11import FluentKit
22import FluentKitExtras
33import Foundation
4+ import Logging
45import NIOCore
6+ import NIOEmbedded
57import Testing
68
79@Suite ( " FluentKit Extras " )
@@ -58,7 +60,7 @@ struct FluentKitExtrasTests {
5860 let now = Date ( )
5961 model. timestamp = now
6062
61- #expect( model. timestamp == TimestampFormatFactory < ISO8601TimestampFormat > . iso8601 . makeFormat ( ) . parse ( TimestampFormatFactory < ISO8601TimestampFormat > . iso8601 . makeFormat ( ) . serialize ( now) ! ) )
63+ #expect( model. timestamp == fluentIso8601Date ( fluentIso8601String ( now) ) )
6264 #expect( model. $timestamp. description == " @BarModel.RequiredTimestamp(key: timestamp) " )
6365 #expect( model. $timestamp. path == [ . string( " timestamp " ) ] )
6466 #expect( model. $timestamp. keys == [ . string( " timestamp " ) ] )
@@ -70,7 +72,7 @@ struct FluentKitExtrasTests {
7072 }
7173
7274 @Test
73- func pointerProperties( ) throws {
75+ func pointerProperties( ) async throws {
7476 let model = BazModel ( )
7577 model. $bar. ref = " a "
7678 model. $optionalBar. ref = " b "
@@ -98,15 +100,35 @@ struct FluentKitExtrasTests {
98100 case let filter:
99101 Issue . record ( " Incorrect filter for model.$bar.query(on:): \( String ( describing: filter) ) " )
100102 }
103+
104+ // Everything from here down is just code coverage junk
105+ model. $bar. ref = " b "
106+ model. $bar. value = . init( )
107+ #expect( model. bar. id == nil )
108+ #expect( model. $bar. description == " Pointer<BazModel, BarModel, identifier>(key: baridentifier) " )
109+ await #expect( throws: Never . self) { try await model. $bar. get ( reload: true , on: MockFluentDatabase ( [ [ [ " id " : " 1 " , " identifier " : " b " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " recursive_baridentifier " : " b " , " blooey " : " " , " phooey " : " " ] as QuickOutput ] ] ) ) }
110+ #expect( model. $bar. anyQueryableProperty === model. $bar. $ref)
111+ #expect( model. $bar. queryablePath == [ . string( " baridentifier " ) ] )
112+ #expect( model. $bar. queryableProperty === model. $bar. $ref)
113+ model. $optionalBar. ref = " b "
114+ model. $optionalBar. value = . some( nil )
115+ #expect( model. optionalBar == nil )
116+ #expect( model. $optionalBar. description == " OptionalPointer<BazModel, BarModel, identifier>(key: optional_baridentifier) " )
117+ await #expect( throws: Never . self) { try await model. $optionalBar. get ( reload: true , on: MockFluentDatabase ( ) ) }
118+ #expect( model. $optionalBar. anyQueryableProperty === model. $optionalBar. $ref)
119+ #expect( model. $optionalBar. queryablePath == [ . string( " optional_baridentifier " ) ] )
120+ #expect( model. $optionalBar. queryableProperty === model. $optionalBar. $ref)
121+
122+ #expect( throws: Never . self) { try JSONEncoder ( ) . encode ( model) }
101123 }
102124
103125 @Test
104- func referenceProperties( ) throws {
126+ func referenceProperties( ) async throws {
105127 let model = BarModel ( )
106- #expect ( throws : Never . self ) { try model. $bazs. output ( from : [ " identifier " : " a " ] as QuickOutput ) }
107- #expect ( throws : Never . self ) { try model. $optionalBaz. output ( from : [ " identifier " : " b " ] as QuickOutput ) }
108- #expect ( throws : Never . self ) { try model. $recursiveBars. output ( from : [ " identifier " : " c " ] as QuickOutput ) }
109- #expect ( throws : Never . self ) { try model. $optionalRecursiveBar. output ( from : [ " identifier " : " d " ] as QuickOutput ) }
128+ model. $bazs. fromValue = " a "
129+ model. $optionalBaz. fromValue = " b "
130+ model. $recursiveBars. fromValue = " c "
131+ model. $optionalRecursiveBar. fromValue = " d "
110132 let query1 = model. $bazs. query ( on: MockFluentDatabase ( ) ) , filter1 = try #require( query1. query. filters. first)
111133 switch filter1 {
112134 case . value( . extendedPath( let path, " bazs " , nil ) , . equality( false ) , . bind( let bind) ) :
@@ -135,6 +157,64 @@ struct FluentKitExtrasTests {
135157 #expect( String ( describing: bind) == #"Optional("d")"# )
136158 case let filter: Issue . record ( " Incorrect filter for model.$optionalRecursiveBar.query(on:): \( String ( describing: filter) ) " )
137159 }
160+
161+ // Everything from here down is just code coverage junk
162+ model. $bazs. value = [ ]
163+ model. $optionalBaz. value = . some( nil )
164+ model. $baz. value = . some( nil )
165+ model. $baz. fromValue = " a "
166+ model. $optionalBazs. value = [ ]
167+ model. $optionalBazs. fromValue = " b "
168+ #expect( model. $bazs. fromValue == " a " )
169+ #expect( model. bazs. isEmpty)
170+ #expect( model. $bazs. description == " @References<BarModel, BazModel>(for: required( \\ BazModel.$bar)) " )
171+ #expect( model. $bazs. keys == [ ] )
172+ #expect( model. $optionalBaz. fromValue == " b " )
173+ #expect( model. optionalBaz == nil )
174+ #expect( model. $optionalBaz. description == " @OptionalReference<BarModel, BazModel>(for: optional( \\ BazModel.$optionalBar)) " )
175+ #expect( model. $optionalBaz. keys == [ ] )
176+ #expect( model. $optionalBazs. query ( on: MockFluentDatabase ( ) ) . query. description == #"query read bazs filters=[bazs[optional_baridentifier] = Optional("b")]"# )
177+ #expect( model. $baz. query ( on: MockFluentDatabase ( ) ) . query. description == #"query read bazs filters=[bazs[baridentifier] = "a"]"# )
178+ model. $bazs. input ( to: QuickInput ( ) )
179+ model. $optionalBaz. input ( to: QuickInput ( ) )
180+ #expect( throws: Never . self) { try model. $bazs. output ( from: [ " identifier " : " a " , " baridentifier " : " a " ] as QuickOutput ) }
181+ #expect( throws: Never . self) { try model. $optionalBazs. output ( from: [ " identifier " : " b " , " optional_baridentifier " : " b " ] as QuickOutput ) }
182+ #expect( throws: Never . self) { try model. $optionalBaz. output ( from: [ " identifier " : " b " , " optional_baridentifier " : " b " ] as QuickOutput ) }
183+ #expect( throws: Never . self) { try model. $baz. output ( from: [ " identifier " : " a " , " baridentifier " : " a " ] as QuickOutput ) }
184+ model. identifier = " a "
185+ model. $bar. ref = " b "
186+ model. timestamp = . init( )
187+ model. another = . init( )
188+ model. group. phooey = " phooey "
189+ model. group. blooey = " blooey "
190+ #expect( throws: Never . self) { try JSONEncoder ( ) . encode ( model) }
191+ await #expect( throws: Never . self) { try await model. $bazs. get ( reload: true , on: MockFluentDatabase ( ) ) }
192+ await #expect( throws: Never . self) { try await model. $bazs. create ( BazModel ( ) , on: MockFluentDatabase ( [ [ [ " id " : " 1 " ] as QuickOutput ] ] ) ) . get ( ) }
193+ await #expect( throws: Never . self) { try await model. $optionalBazs. create ( BazModel ( ) , on: MockFluentDatabase ( [ [ [ " id " : " 1 " ] as QuickOutput ] ] ) ) . get ( ) }
194+ await #expect( throws: Never . self) { try await model. $bazs. create ( [ . init( ) , . init( ) ] , on: MockFluentDatabase ( [ [ [ " id " : " 1 " ] as QuickOutput , [ " id " : " 2 " ] as QuickOutput ] ] ) ) . get ( ) }
195+ await #expect( throws: Never . self) { try await model. $optionalBazs. create ( [ . init( ) , . init( ) ] , on: MockFluentDatabase ( [ [ [ " id " : " 1 " ] as QuickOutput , [ " id " : " 2 " ] as QuickOutput ] ] ) ) . get ( ) }
196+ await #expect( throws: Never . self) { try await model. $optionalBaz. get ( reload: true , on: MockFluentDatabase ( ) ) }
197+ await #expect( throws: Never . self) { try await model. $baz. get ( reload: true , on: MockFluentDatabase ( ) ) }
198+ await #expect( throws: Never . self) { try await model. $optionalBaz. create ( . init( ) , on: MockFluentDatabase ( [ [ [ " id " : " 1 " ] as QuickOutput ] ] ) ) . get ( ) }
199+ await #expect( throws: Never . self) { try await model. $baz. create ( . init( ) , on: MockFluentDatabase ( [ [ [ " id " : " 1 " ] as QuickOutput ] ] ) ) . get ( ) }
200+ await #expect( throws: Never . self) { try await BarModel . query ( on: MockFluentDatabase ( [
201+ [ [ " id " : " 1 " , " identifier " : " a " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " phooey " : " " , " blooey " : " " , " recursive_baridentifier " : " b " ] as QuickOutput ] ,
202+ [ [ " id " : " 2 " , " identifier " : " b " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " phooey " : " " , " blooey " : " " , " recursive_baridentifier " : " a " ] as QuickOutput ] ,
203+ [ [ " id " : " 1 " , " baridentifier " : " a " , " optional_baridentifier " : " a " ] as QuickOutput ] ,
204+ [ [ " id " : " 1 " , " baridentifier " : " a " , " optional_baridentifier " : " a " ] as QuickOutput ] ,
205+ [ [ " id " : " 1 " , " baridentifier " : " a " , " optional_baridentifier " : " a " ] as QuickOutput ] ,
206+ [ [ " id " : " 1 " , " baridentifier " : " a " , " optional_baridentifier " : " a " ] as QuickOutput ] ,
207+ [ [ " id " : " 1 " , " identifier " : " a " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " phooey " : " " , " blooey " : " " , " recursive_baridentifier " : " a " ] as QuickOutput ] ,
208+ [ [ " id " : " 1 " , " identifier " : " a " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " phooey " : " " , " blooey " : " " , " recursive_baridentifier " : " a " , " optional_recursive_baridentifier " : " a " ] as QuickOutput ] ,
209+ [ [ " id " : " 1 " , " identifier " : " a " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " phooey " : " " , " blooey " : " " , " recursive_baridentifier " : " a " , " optional_recursive_baridentifier " : " a " ] as QuickOutput ] ,
210+ [ [ " id " : " 1 " , " identifier " : " a " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " phooey " : " " , " blooey " : " " , " recursive_baridentifier " : " a " ] as QuickOutput ] ,
211+ [ [ " id " : " 2 " , " identifier " : " b " , " timestamp " : fluentIso8601String ( ) , " another " : fluentIso8601String ( ) , " phooey " : " " , " blooey " : " " , " recursive_baridentifier " : " a " ] as QuickOutput ] ,
212+ ] ) )
213+ . withDeleted ( )
214+ . with ( \. $bar) . with ( \. $optionalBar) . with ( \. $bazs) . with ( \. $optionalBazs) . with ( \. $optionalBaz) . with ( \. $baz) . with ( \. $recursiveBars) . with ( \. $optionalRecursiveBars) . with ( \. $optionalRecursiveBar) . with ( \. $recursiveBar)
215+ . with ( \. $bar, withDeleted: true ) . with ( \. $optionalBar, withDeleted: true ) . with ( \. $bazs, withDeleted: true ) . with ( \. $optionalBazs, withDeleted: true ) . with ( \. $optionalBaz, withDeleted: true ) . with ( \. $baz, withDeleted: true ) . with ( \. $recursiveBars, withDeleted: true ) . with ( \. $optionalRecursiveBars, withDeleted: true ) . with ( \. $optionalRecursiveBar, withDeleted: true ) . with ( \. $recursiveBar, withDeleted: true )
216+ . all ( )
217+ }
138218 }
139219}
140220
@@ -177,15 +257,27 @@ final class BarModel: FluentKit.Model, @unchecked Sendable {
177257 @References ( for: \. $bar)
178258 var bazs : [ BazModel ]
179259
260+ @References ( for: \. $optionalBar)
261+ var optionalBazs : [ BazModel ]
262+
180263 @OptionalReference ( for: \. $optionalBar)
181264 var optionalBaz : BazModel ?
182265
266+ @OptionalReference ( for: \. $bar)
267+ var baz : BazModel ?
268+
183269 @References ( forRecursive: \. $bar, referencedBy: \. $identifier)
184270 var recursiveBars : [ BarModel ]
185271
272+ @References ( forRecursive: \. $optionalBar, referencedBy: \. $identifier)
273+ var optionalRecursiveBars : [ BarModel ]
274+
186275 @OptionalReference ( forRecursive: \. $optionalBar, referencedBy: \. $baridentifier)
187276 var optionalRecursiveBar : BarModel ?
188277
278+ @OptionalReference ( forRecursive: \. $bar, referencedBy: \. $baridentifier)
279+ var recursiveBar : BarModel ?
280+
189281 init ( ) { }
190282}
191283
@@ -214,20 +306,35 @@ final class BopFields: FluentKit.Fields, @unchecked Sendable {
214306 init ( ) { }
215307}
216308
217- struct MockFluentDatabase : Database {
309+ final class MockFluentDatabase : Database {
218310 struct MockConfiguration : DatabaseConfiguration {
219311 var middleware : [ any AnyModelMiddleware ] = [ ]
220312 func makeDriver( for databases: Databases ) -> any DatabaseDriver { fatalError ( ) }
221313 }
222314
223315 let context : DatabaseContext = . init(
224316 configuration: MockConfiguration ( ) ,
225- logger: . init( label: " " , factory: SwiftLogNoOpLogHandler . init ( _: ) ) ,
226- eventLoop: NIOSingletons . posixEventLoopGroup. any ( )
317+ logger: . init( label: " mockdb " , factory: { l in
318+ var h = ModifiedStreamLogHandler . standardOutput ( label: l)
319+ h. logLevel = . debug
320+ return h
321+ } ) ,
322+ eventLoop: EmbeddedEventLoop ( )
227323 )
228324 let inTransaction = false
229325
230- func execute( query: DatabaseQuery , onOutput: @escaping @Sendable ( any DatabaseOutput ) -> ( ) ) -> EventLoopFuture < Void > { self . eventLoop. makeSucceededVoidFuture ( ) }
326+ nonisolated ( unsafe) var outputs: [ [ any DatabaseOutput ] ]
327+
328+ init ( _ outputs: [ [ any DatabaseOutput ] ] = [ ] ) {
329+ self . outputs = outputs
330+ }
331+
332+ func execute( query: DatabaseQuery , onOutput: @escaping @Sendable ( any DatabaseOutput ) -> ( ) ) -> EventLoopFuture < Void > {
333+ if !self . outputs. isEmpty {
334+ for output in self . outputs. removeFirst ( ) { onOutput ( output) }
335+ }
336+ return self . eventLoop. makeSucceededVoidFuture ( )
337+ }
231338 func execute( schema: DatabaseSchema ) -> EventLoopFuture < Void > { self . eventLoop. makeSucceededVoidFuture ( ) }
232339 func execute( enum: DatabaseEnum ) -> EventLoopFuture < Void > { self . eventLoop. makeSucceededVoidFuture ( ) }
233340 func transaction< T> ( _ closure: @escaping @Sendable ( any Database ) -> EventLoopFuture < T > ) -> EventLoopFuture < T > { closure ( self ) }
@@ -243,8 +350,31 @@ final class QuickOutput: DatabaseOutput, ExpressibleByDictionaryLiteral {
243350 func contains( _ key: FieldKey ) -> Bool { self . content [ key. description] != nil }
244351 func decodeNil( _ key: FieldKey ) throws -> Bool { self . content [ key. description] == nil }
245352 func decode< T: Decodable > ( _ key: FieldKey , as type: T . Type ) throws -> T {
246- guard let value = self . content [ key. description] as? T else { throw FluentError . missingField ( name: key. description) }
247- return value
353+ switch type {
354+ case is String . Type :
355+ guard let rawValue = self . content [ key. description] else { throw FluentError . missingField ( name: key. description) }
356+ return rawValue as! T
357+ case is String ? . Type:
358+ return self . content [ key. description] as! T
359+ case is Int . Type :
360+ guard let rawValue = self . content [ key. description] else { throw FluentError . missingField ( name: key. description) }
361+ guard let result = Int ( rawValue) else { throw FluentError . invalidField ( name: key. description, valueType: type, error: CancellationError ( ) ) }
362+ return result as! T
363+ case is Int ? . Type:
364+ guard let rawValue = self . content [ key. description] else { return Int ? . none as! T }
365+ guard let result = Int ( rawValue) else { throw FluentError . invalidField ( name: key. description, valueType: type, error: CancellationError ( ) ) }
366+ return result as! T
367+ case is Date . Type :
368+ guard let rawValue = self . content [ key. description] else { throw FluentError . missingField ( name: key. description) }
369+ guard let result = TimestampFormatFactory < ISO8601TimestampFormat > . iso8601. makeFormat ( ) . parse ( rawValue) else { throw FluentError . invalidField ( name: key. description, valueType: type, error: CancellationError ( ) ) }
370+ return result as! T
371+ case is Date ? . Type:
372+ guard let rawValue = self . content [ key. description] else { return Date ? . none as! T }
373+ guard let result = TimestampFormatFactory < ISO8601TimestampFormat > . iso8601. makeFormat ( ) . parse ( rawValue) else { throw FluentError . invalidField ( name: key. description, valueType: type, error: CancellationError ( ) ) }
374+ return result as! T
375+ default :
376+ throw FluentError . invalidField ( name: key. description, valueType: type, error: CancellationError ( ) )
377+ }
248378 }
249379}
250380
@@ -260,3 +390,55 @@ final class QuickInput: DatabaseInput {
260390 self . content [ key. description] = str
261391 }
262392}
393+
394+ func fluentIso8601Date( _ str: String ) -> Date ? {
395+ TimestampFormatFactory < ISO8601TimestampFormat > . iso8601. makeFormat ( ) . parse ( str)
396+ }
397+
398+ func fluentIso8601String( _ date: Date = . init( ) ) -> String {
399+ TimestampFormatFactory < ISO8601TimestampFormat > . iso8601. makeFormat ( ) . serialize ( date) !
400+ }
401+
402+ public struct ModifiedStreamLogHandler : LogHandler {
403+ public static func standardOutput( label: String ) -> Self { . init( label: label) }
404+ private let label : String
405+ public var logLevel : Logger . Level = . info, metadataProvider = LoggingSystem . metadataProvider, metadata = Logger . Metadata ( )
406+ public subscript( metadataKey key: String ) -> Logger . Metadata . Value ? { get { self . metadata [ key] } set { self . metadata [ key] = newValue } }
407+ internal init ( label: String ) { self . label = label }
408+ public func log( level: Logger . Level , message: Logger . Message , metadata explicitMetadata: Logger . Metadata ? , source: String , file: String , function: String , line: UInt ) {
409+ let prettyMetadata = self . prettify ( Self . prepareMetadata ( base: self . metadata, provider: self . metadataProvider, explicit: explicitMetadata) )
410+ print ( " \( self . timestamp ( ) ) \( level) \( self . label) : \( prettyMetadata. map { " \( $0) " } ?? " " ) [ \( source) ] \( message) " )
411+ }
412+ internal static func prepareMetadata( base: Logger . Metadata , provider: Logger . MetadataProvider ? , explicit: Logger . Metadata ? ) -> Logger . Metadata {
413+ var metadata = base
414+ if let provided = provider? . get ( ) , !provided. isEmpty { metadata. merge ( provided, uniquingKeysWith: { $1 } ) }
415+ if let explicit = explicit, !explicit. isEmpty { metadata. merge ( explicit, uniquingKeysWith: { $1 } ) }
416+ return metadata
417+ }
418+ private func prettify( _ metadata: Logger . Metadata ) -> String ? {
419+ metadata. isEmpty ? nil : metadata. lazy. sorted { $0. 0 < $1. 0 } . map { " \( $0) = \( $1. prettyDescription) " } . joined ( separator: " " )
420+ }
421+ private func timestamp( ) -> String {
422+ . init( unsafeUninitializedCapacity: 255 ) { buffer in
423+ var timestamp = time ( nil )
424+ guard let localTime = localtime ( & timestamp) else { return buffer. initialize ( fromContentsOf: " <unknown> " . utf8) }
425+ return strftime ( buffer. baseAddress!, buffer. count, " %Y-%m-%dT%H:%M:%S%z " , localTime)
426+ }
427+ }
428+ }
429+ extension Logger . MetadataValue {
430+ public var prettyDescription : String {
431+ switch self {
432+ case . dictionary( let dict) : " [ \( dict. mapValues ( \. prettyDescription) . lazy. sorted { $0. 0 < $1. 0 } . map { " \( $0) : \( $1) " } . joined ( separator: " , " ) ) ] "
433+ case . array( let list) : " [ \( list. map ( \. prettyDescription) . joined ( separator: " , " ) ) ] "
434+ case . string( let str) : #"" \#( str) ""#
435+ case . stringConvertible( let repr) :
436+ switch repr {
437+ case let repr as Bool : " \( repr) "
438+ case let repr as any FixedWidthInteger : " \( repr) "
439+ case let repr as any BinaryFloatingPoint : " \( repr) "
440+ default : #"" \#( repr. description) ""#
441+ }
442+ }
443+ }
444+ }
0 commit comments