Last active
March 26, 2020 11:55
-
-
Save calebkleveter/856903cef71314abe645f34009e034ee to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import class NIO.ThreadSpecificVariable | |
import Foundation | |
import FluentKit | |
// MARK: - FormattedTimestampProperty | |
extension AnyModel { | |
var formattedTimestamps: [AnyFormattedTimestamp] { | |
return self.properties.compactMap { $0 as? AnyFormattedTimestamp } | |
} | |
} | |
extension Model { | |
public typealias FormattedTimestamp<Value> = FormattedTimestampProperty<Self, Value> | |
where Value: _DateType & Codable | |
} | |
@propertyWrapper | |
public final class FormattedTimestampProperty<M, Timestamp>: AnyFormattedTimestamp where M: Model, Timestamp: _DateType & Codable { | |
public let key: FieldKey | |
public let trigger: TimestampTrigger | |
public let formatter: TimestampFormatter | |
var outputValue: Value? | |
var inputValue: Value? | |
var timestamp: Date? { | |
get { self.value?.date } | |
set { self.value?.date = newValue } | |
} | |
public var projectedValue: FormattedTimestampProperty<Model, Value> { self } | |
public var wrappedValue: Value { | |
get { | |
guard let value = self.value else { | |
fatalError("Cannot access field before it is initialized or fetched: \(self.key)") | |
} | |
return value | |
} | |
set { | |
self.value = newValue | |
} | |
} | |
public convenience init(key: FieldKey, on trigger: TimestampTrigger) { | |
self.init(key: key, on: trigger, formatter: .iso8601) | |
} | |
public init(key: FieldKey, on trigger: TimestampTrigger, formatter: TimestampFormatter) { | |
self.key = key | |
self.trigger = trigger | |
self.formatter = formatter | |
self.inputValue = Timestamp.defaultValue | |
} | |
} | |
extension FormattedTimestampProperty: PropertyProtocol { | |
public typealias Model = M | |
public typealias Value = Timestamp | |
public var value: Timestamp? { | |
get { | |
if let value = self.inputValue { return value } | |
else if let value = self.outputValue { return value } | |
else { return nil } | |
} | |
set { | |
self.inputValue = newValue | |
} | |
} | |
} | |
extension FormattedTimestampProperty: AnyProperty { | |
public var nested: [AnyProperty] { [] } | |
public var path: [FieldKey] { [self.key] } | |
public func input(to input: inout DatabaseInput) { | |
input.values[self.key] = .bind(self.inputValue?.string(using: self.formatter)) | |
} | |
public func output(from output: DatabaseOutput) throws { | |
if output.contains([self.key]) { | |
self.inputValue = nil | |
do { | |
self.outputValue = try Value.decode(from: output, at: self.key, using: self.formatter) | |
} catch { | |
throw FluentError.invalidField( | |
name: self.key.description, | |
valueType: Value.self, | |
error: error | |
) | |
} | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(self.wrappedValue.string(using: self.formatter)) | |
} | |
public func decode(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let valueType = Value.self as? AnyOptionalType.Type { | |
// Hacks for supporting optionals in @Field. | |
// Using @OptionalField is preferred moving forward. | |
if container.decodeNil() { | |
self.wrappedValue = (valueType.nil as! Value) | |
} else { | |
let string = container.decodeNil() ? nil : try container.decode(String.self) | |
let date = string.flatMap(self.formatter.date(from:)) | |
self.wrappedValue = Value.defaultValue | |
self.wrappedValue.date = date | |
} | |
} else { | |
let string = container.decodeNil() ? nil : try container.decode(String.self) | |
let date = string.flatMap(self.formatter.date(from:)) | |
self.wrappedValue = Value.defaultValue | |
self.wrappedValue.date = date | |
} | |
} | |
} | |
protocol AnyFormattedTimestamp: class { | |
var path: [FieldKey] { get } | |
var trigger: TimestampTrigger { get } | |
var timestamp: Date? { get set } | |
} | |
// MARK: - FormattedTimestampSetter | |
extension AnyModel { | |
func setTimestamps(for event: ModelEvent) { | |
switch event { | |
case .create: | |
self.formattedTimestamps.filter { $0.trigger == .create || $0.trigger == .update }.forEach { $0.timestamp = Date() } | |
case .update: | |
self.formattedTimestamps.filter { $0.trigger == .update }.forEach { $0.timestamp = Date() } | |
case .softDelete, .delete(false): | |
self.formattedTimestamps.filter { $0.trigger == .delete }.forEach { $0.timestamp = Date() } | |
case .restore: | |
self.formattedTimestamps.filter { $0.trigger == .update }.forEach { $0.timestamp = Date() } | |
self.formattedTimestamps.filter { $0.trigger == .delete }.forEach { $0.timestamp = nil } | |
case .delete(true): | |
break | |
} | |
} | |
} | |
public final class FormattedTimestampSetter: AnyModelMiddleware { | |
private let modelType: AnyModel.Type? | |
public init(_ modelType: AnyModel.Type? = nil) { | |
self.modelType = modelType | |
} | |
public func handle(_ event: ModelEvent, _ model: AnyModel, on database: Database, chainingTo next: AnyModelResponder) -> EventLoopFuture<Void> { | |
if self.modelType != nil { | |
guard self.modelType == type(of: model) else { return next.handle(event, model, on: database) } | |
} | |
model.setTimestamps(for: .create) | |
return next.handle(event, model, on: database) | |
} | |
} | |
// MARK: - TimestampFormatter | |
public protocol DateFormatterProtocol { | |
func string(from date: Date) -> String | |
func date(from string: String) -> Date? | |
} | |
extension DateFormatter: DateFormatterProtocol { } | |
extension ISO8601DateFormatter: DateFormatterProtocol { } | |
public struct TimestampFormatter { | |
public static let iso8601 = TimestampFormatter("iso8601", formatter: ISO8601DateFormatter.init) | |
public let identifier: String | |
public let initialize: () -> DateFormatterProtocol | |
public init(_ identifier: String, formatter initializer: @escaping () -> DateFormatterProtocol) { | |
self.identifier = identifier | |
self.initialize = initializer | |
} | |
public func date(from string: String) -> Date? { | |
TimestampFormatterContainer.current.currentValue?.get(self).date(from: string) | |
} | |
public func string(from date: Date) -> String { | |
TimestampFormatterContainer.current.currentValue?.get(self).string(from: date) ?? date.description | |
} | |
} | |
private final class TimestampFormatterContainer { | |
static var current: ThreadSpecificVariable<TimestampFormatterContainer> = .init(value: .init()) | |
var formatters: [String: DateFormatterProtocol] | |
private init() { | |
self.formatters = [:] | |
} | |
func get(_ formatter: TimestampFormatter) -> DateFormatterProtocol { | |
if let cached = self.formatters[formatter.identifier] { return cached } | |
let new = formatter.initialize() | |
self.formatters[formatter.identifier] = new | |
return new | |
} | |
} | |
extension TimestampFormatter { | |
static let myFormat = TimestampFormatter("myFormat", formatter: { | |
let formatter = DateFormatter() | |
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" | |
formatter.timeZone = TimeZone(secondsFromGMT: +3) | |
formatter.locale = Locale(identifier: "en_US_POSIX") | |
return formatter | |
}) | |
} | |
// MARK: - _DateType | |
public protocol _DateType { | |
static var defaultValue: Self { get } | |
static func decode(from output: DatabaseOutput, at path: FieldKey, using formatter: TimestampFormatter) throws -> Self | |
init() | |
var date: Date? { get set } | |
func string(using formatter: TimestampFormatter) -> String? | |
} | |
extension Date: _DateType { | |
public static var defaultValue: Date { .init() } | |
public static func decode(from output: DatabaseOutput, at key: FieldKey, using formatter: TimestampFormatter) throws -> Date { | |
let string = try output.decode(key, as: String.self) | |
guard let date = formatter.date(from: string) else { | |
throw DecodingError.dataCorrupted(.init(codingPath: [key], debugDescription: "Cannot convert string '\(string)' to a date")) | |
} | |
return date | |
} | |
public var date: Date? { | |
get { self } | |
set { self = newValue ?? self } | |
} | |
public func string(using formatter: TimestampFormatter) -> String? { | |
return formatter.string(from: self) | |
} | |
} | |
extension Optional: _DateType where Wrapped == Date { | |
public static var defaultValue: Optional<Date> { nil } | |
public static func decode(from output: DatabaseOutput, at key: FieldKey, using formatter: TimestampFormatter) throws -> Date? { | |
guard output.contains(key) else { return nil } | |
guard let string = try output.decode(key, as: String?.self) else { return nil } | |
guard let date = formatter.date(from: string) else { | |
throw DecodingError.dataCorrupted(.init(codingPath: [key], debugDescription: "Cannot convert string '\(string)' to a date")) | |
} | |
return date | |
} | |
public init() { self = Date() } | |
public var date: Date? { | |
get { self } | |
set { self = newValue ?? self } | |
} | |
public func string(using formatter: TimestampFormatter) -> String? { | |
return self.map(formatter.string(from:)) | |
} | |
} | |
extension FieldKey: CodingKey { | |
public init?(intValue: Int) { return nil } | |
public init?(stringValue: String) { | |
self = .string(stringValue) | |
} | |
public var intValue: Int? { nil } | |
public var stringValue: String { self.description } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment