Skip to content
This repository has been archived by the owner on Nov 16, 2020. It is now read-only.

Change DateFormat to use the full ISO8601 date format by default #45

Merged
merged 8 commits into from
Jun 11, 2019
59 changes: 47 additions & 12 deletions Sources/TemplateKit/Tag/DateFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,39 @@
///
/// If no date format is supplied, a default will be used.
public final class DateFormat: TagRenderer {
public typealias DateFormatterFactory = () -> DateFormatter

private let defaultDateFormatterFactory: DateFormatterFactory

private static let dateAndTimeFormatterFactory: DateFormatterFactory = {
let dateFormatter = DateFormatter()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DateFormatter is a class and I don't think it's thread safe. This probably needs to be a factory method that produces a new formatter each time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spotted the same issue and checked; it is thread safe: https://developer.apple.com/documentation/foundation/dateformatter#1680059

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://bugs.swift.org/browse/SR-7745

That documentation references Darwin platforms. I'm not sure it holds true for Linux. Vapor has definitely had issues with DateFormatter and multi-threading in the past. Maybe they are resolved now? Vapor 3 must support Ubuntu 14.04 and Swift 4.1.x though. So even if multi-threading has been fixed recently, we can't really rely on that.

dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return 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(defaultDateFormatterFactory: DateFormat.dateAndTimeFormatterFactory)
}

/// Creates a new `DateFormat` tag renderer.
public init() {}
/// - parameter defaultDateFormatter: The date formatter to use when the tag invocation
/// does not specify a date format.
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(defaultDateFormatterFactory: DateFormat.iso8601FormatterFactory)

/// See `TagRenderer`.
public func render(tag: TagContext) throws -> Future<TemplateData> {
Expand All @@ -28,21 +59,24 @@ public final class DateFormat: TagRenderer {
tag.context.userInfo[DateFormatterCache.userInfoKey] = dateFormatterCache
}

let dateFormat: String
/// Set format as the second param or default to ISO-8601 format.
if tag.parameters.count == 2, let param = tag.parameters[1].string {
dateFormat = param
} else {
dateFormat = "yyyy-MM-dd HH:mm:ss"
let dateFormatter: DateFormatter
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 dateFormatter: DateFormatter
if let formatter = dateFormatterCache.dateFormatters[dateFormat] {
let cacheKey = dateFormat ?? DateFormatterCache.defaultFormatterPlaceholderKey
if let formatter = dateFormatterCache.dateFormatters[cacheKey] {
dateFormatter = formatter
} else {
dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
dateFormatterCache.dateFormatters[dateFormat] = dateFormatter
if let dateFormat = dateFormat {
dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
} else {
dateFormatter = self.defaultDateFormatterFactory()
}
dateFormatterCache.dateFormatters[cacheKey] = dateFormatter
}

/// Return formatted date
Expand All @@ -52,6 +86,7 @@ public final class DateFormat: TagRenderer {

private class DateFormatterCache {
static let userInfoKey = "TemplateKit.DateFormatterCache"
static let defaultFormatterPlaceholderKey = "DEFAULT"

var dateFormatters: [String: DateFormatter] = [:]
}
56 changes: 45 additions & 11 deletions Tests/TemplateKitTests/TemplateDataEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,7 @@ class TemplateDataEncoderTests: XCTestCase {
let user = User(id: nil, name: "Vapor")
let profile = Profile(currentUser: Future.map(on: worker) { user })

let data = try TemplateDataEncoder().testEncode(profile, on: worker)
print(data)
let data = try TemplateDataEncoder().testEncode(profile)
let container = BasicContainer(config: .init(), environment: .testing, services: .init(), on: worker)

let renderer = PlaintextRenderer(viewsDir: "/", on: container)
Expand All @@ -276,20 +275,20 @@ class TemplateDataEncoderTests: XCTestCase {
XCTAssertEqual(String(data: view.data, encoding: .utf8), "headVaportail")
}

// https://github.com/vapor/template-kit/issues/20
func testGH20() throws {
private func checkDateFormatting(
dateFormat: DateFormat, dateFormatter: DateFormatter, file: StaticString = #file, line: UInt = #line) throws {
func wrap(_ syntax: TemplateSyntaxType) -> TemplateSyntax {
return TemplateSyntax(type: syntax, source: TemplateSource(file: "test", line: 0, column: 0, range: 0..<1))
}

let path: [CodingKey] = [
BasicKey.init("date"),
]

let worker = EmbeddedEventLoop()
let container = BasicContainer(config: .init(), environment: .testing, services: .init(), on: worker)
let renderer = PlaintextRenderer(viewsDir: "/", on: container)
renderer.tags["date"] = DateFormat()
renderer.tags["date"] = dateFormat
let ast: [TemplateSyntax] = [
wrap(.tag(TemplateTag(
name: "date",
Expand All @@ -299,16 +298,51 @@ 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))
XCTAssertEqual(String(data: view.data, encoding: .utf8), formatter.string(from: date))
XCTAssertEqual(String(data: view.data, encoding: .utf8), dateFormatter.string(from: date), file: file, line: line)
}

// https://github.com/vapor/template-kit/issues/20
func testGH20() throws {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
try checkDateFormatting(dateFormat: DateFormat(), dateFormatter: dateFormatter)
}

func testISO8601DateFormat() 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"
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() {
Expand Down
2 changes: 2 additions & 0 deletions Tests/TemplateKitTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension TemplateDataEncoderTests {
static let __allTests__TemplateDataEncoderTests = [
("testArray", testArray),
("testComplexEncodable", testComplexEncodable),
("testDateFormatterThreadSafety", testDateFormatterThreadSafety),
("testDictionary", testDictionary),
("testDouble", testDouble),
("testEncodable", testEncodable),
Expand All @@ -35,6 +36,7 @@ extension TemplateDataEncoderTests {
("testEncodingPerformanceExampleModelJSONBaseline", testEncodingPerformanceExampleModelJSONBaseline),
("testGH10", testGH10),
("testGH20", testGH20),
("testISO8601DateFormat", testISO8601DateFormat),
("testNestedArray", testNestedArray),
("testNestedDictionary", testNestedDictionary),
("testNestedEncodable", testNestedEncodable),
Expand Down