Skip to content

Instantly share code, notes, and snippets.

@calebkleveter
Last active March 26, 2020 11:55
Show Gist options
  • Save calebkleveter/856903cef71314abe645f34009e034ee to your computer and use it in GitHub Desktop.
Save calebkleveter/856903cef71314abe645f34009e034ee to your computer and use it in GitHub Desktop.
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