From ca5f4c742de5be698ad217d9b011dabb9d1c1641 Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Fri, 22 Mar 2019 12:45:10 +0100 Subject: [PATCH 1/4] Change `DateFormat` to use the full ISO8601 date format by default --- Sources/TemplateKit/Tag/DateFormat.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/TemplateKit/Tag/DateFormat.swift b/Sources/TemplateKit/Tag/DateFormat.swift index bb5f8db..5347755 100644 --- a/Sources/TemplateKit/Tag/DateFormat.swift +++ b/Sources/TemplateKit/Tag/DateFormat.swift @@ -22,7 +22,10 @@ public final class DateFormat: TagRenderer { if tag.parameters.count == 2, let param = tag.parameters[1].string { formatter.dateFormat = param } else { - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + formatter.calendar = Calendar(identifier: .iso8601) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" } /// Return formatted date From 03df0c2f20e78c0492cd71aeb6e42427305e1190 Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Wed, 27 Mar 2019 09:53:59 +0100 Subject: [PATCH 2/4] Fix the tests. --- Tests/TemplateKitTests/TemplateDataEncoderTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/TemplateKitTests/TemplateDataEncoderTests.swift b/Tests/TemplateKitTests/TemplateDataEncoderTests.swift index 836cdcd..2c5f437 100644 --- a/Tests/TemplateKitTests/TemplateDataEncoderTests.swift +++ b/Tests/TemplateKitTests/TemplateDataEncoderTests.swift @@ -152,7 +152,6 @@ class TemplateDataEncoderTests: XCTestCase { let profile = Profile(currentUser: Future.map(on: worker) { user }) let data = try TemplateDataEncoder().testEncode(profile) - print(data) let container = BasicContainer(config: .init(), environment: .testing, services: .init(), on: worker) let renderer = PlaintextRenderer(viewsDir: "/", on: container) @@ -189,15 +188,16 @@ class TemplateDataEncoderTests: XCTestCase { ] let date = Date() let data = try TemplateDataEncoder().testEncode(["date": date]) - print(data) let view = try TemplateSerializer( renderer: renderer, context: TemplateDataContext(data: data), using: container ).serialize(ast: ast).wait() let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - print(formatter.string(from: date)) + formatter.calendar = Calendar(identifier: .iso8601) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" XCTAssertEqual(String(data: view.data, encoding: .utf8), formatter.string(from: date)) } From 0b8991c86bb4801ced3b4b36ddf8f40aa0073115 Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Mon, 29 Apr 2019 13:57:22 +0200 Subject: [PATCH 3/4] Use a default date formatter factory for additional thread safety. --- Sources/TemplateKit/Tag/DateFormat.swift | 43 ++++++++++++++---------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/Sources/TemplateKit/Tag/DateFormat.swift b/Sources/TemplateKit/Tag/DateFormat.swift index 9284dec..3aa5184 100644 --- a/Sources/TemplateKit/Tag/DateFormat.swift +++ b/Sources/TemplateKit/Tag/DateFormat.swift @@ -4,37 +4,39 @@ /// /// If no date format is supplied, a default will be used. public final class DateFormat: TagRenderer { - private let defaultDateFormatter: DateFormatter + public typealias DateFormatterFactory = () -> DateFormatter - private static let dateAndTimeFormatter: DateFormatter = { + private let defaultDateFormatterFactory: DateFormatterFactory + + private static let dateAndTimeFormatterFactory: DateFormatterFactory = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" return dateFormatter - }() + } - private static let iso8601Formatter: DateFormatter = { + private static let iso8601FormatterFactory: DateFormatterFactory = { let dateFormatter = DateFormatter() dateFormatter.calendar = Calendar(identifier: .iso8601) dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" return dateFormatter - }() + } /// Creates a new `DateFormat` tag renderer. public convenience init() { - self.init(defaultDateFormatter: DateFormat.dateAndTimeFormatter) + self.init(defaultDateFormatterFactory: DateFormat.dateAndTimeFormatterFactory) } /// Creates a new `DateFormat` tag renderer. /// - parameter defaultDateFormatter: The date formatter to use when the tag invocation /// does not specify a date format. - public init(defaultDateFormatter: DateFormatter) { - self.defaultDateFormatter = defaultDateFormatter + public init(defaultDateFormatterFactory: @escaping DateFormatterFactory) { + self.defaultDateFormatterFactory = defaultDateFormatterFactory } /// A `DateFormat` tag renderer that uses ISO 8601 date formatting by default. - public static let iso8601 = DateFormat(defaultDateFormatter: DateFormat.iso8601Formatter) + public static let iso8601 = DateFormat(defaultDateFormatterFactory: DateFormat.iso8601FormatterFactory) /// See `TagRenderer`. public func render(tag: TagContext) throws -> Future { @@ -58,17 +60,23 @@ public final class DateFormat: TagRenderer { } let dateFormatter: DateFormatter - /// Set format as the second param or default to ISO-8601 format. - if tag.parameters.count == 2, let dateFormat = tag.parameters[1].string { - if let formatter = dateFormatterCache.dateFormatters[dateFormat] { - dateFormatter = formatter - } else { + var dateFormat: String? + if tag.parameters.count == 2 { + /// Set format as the second param. If that's not available, we'll use `self.defaultDateFormatterFactory`. + dateFormat = tag.parameters[1].string + } + + let cacheKey = dateFormat ?? DateFormatterCache.defaultFormatterPlaceholderKey + if let formatter = dateFormatterCache.dateFormatters[cacheKey] { + dateFormatter = formatter + } else { + if let dateFormat = dateFormat { dateFormatter = DateFormatter() dateFormatter.dateFormat = dateFormat - dateFormatterCache.dateFormatters[dateFormat] = dateFormatter + } else { + dateFormatter = self.defaultDateFormatterFactory() } - } else { - dateFormatter = self.defaultDateFormatter + dateFormatterCache.dateFormatters[cacheKey] = dateFormatter } /// Return formatted date @@ -78,6 +86,7 @@ public final class DateFormat: TagRenderer { private class DateFormatterCache { static let userInfoKey = "TemplateKit.DateFormatterCache" + static let defaultFormatterPlaceholderKey = "DEFAULT" var dateFormatters: [String: DateFormatter] = [:] } From f3ba20d78e300464a852525c6ab30051004d0776 Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Tue, 11 Jun 2019 09:38:18 +0200 Subject: [PATCH 4/4] Add a DateFormatter stress test. --- .../TemplateDataEncoderTests.swift | 23 +++++++++++++++++++ Tests/TemplateKitTests/XCTestManifests.swift | 2 ++ 2 files changed, 25 insertions(+) diff --git a/Tests/TemplateKitTests/TemplateDataEncoderTests.swift b/Tests/TemplateKitTests/TemplateDataEncoderTests.swift index 6b65ca7..f05125e 100644 --- a/Tests/TemplateKitTests/TemplateDataEncoderTests.swift +++ b/Tests/TemplateKitTests/TemplateDataEncoderTests.swift @@ -321,6 +321,29 @@ class TemplateDataEncoderTests: XCTestCase { dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" try checkDateFormatting(dateFormat: .iso8601, dateFormatter: dateFormatter) } + + func testDateFormatterThreadSafety() throws { + let dateFormatter = DateFormatter() + dateFormatter.calendar = Calendar(identifier: .iso8601) + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" + + let date = Date() + let expectedString = dateFormatter.string(from: date) + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 8 + queue.isSuspended = true + + for _ in 0..<50_000 { + queue.addOperation { + XCTAssertEqual(dateFormatter.string(from: date), expectedString) + } + } + + queue.isSuspended = false + queue.waitUntilAllOperationsAreFinished() + } func testTemplabeByteScannerPeak() { let scanner = TemplateByteScanner(data: Data(), file: "empty") diff --git a/Tests/TemplateKitTests/XCTestManifests.swift b/Tests/TemplateKitTests/XCTestManifests.swift index 2e10713..fe2ed99 100644 --- a/Tests/TemplateKitTests/XCTestManifests.swift +++ b/Tests/TemplateKitTests/XCTestManifests.swift @@ -24,6 +24,7 @@ extension TemplateDataEncoderTests { static let __allTests__TemplateDataEncoderTests = [ ("testArray", testArray), ("testComplexEncodable", testComplexEncodable), + ("testDateFormatterThreadSafety", testDateFormatterThreadSafety), ("testDictionary", testDictionary), ("testDouble", testDouble), ("testEncodable", testEncodable), @@ -35,6 +36,7 @@ extension TemplateDataEncoderTests { ("testEncodingPerformanceExampleModelJSONBaseline", testEncodingPerformanceExampleModelJSONBaseline), ("testGH10", testGH10), ("testGH20", testGH20), + ("testISO8601DateFormat", testISO8601DateFormat), ("testNestedArray", testNestedArray), ("testNestedDictionary", testNestedDictionary), ("testNestedEncodable", testNestedEncodable),