diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index 6cff9130..e0b593a5 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -25,14 +25,37 @@ public protocol DateTranscoder: Sendable { } /// A transcoder for dates encoded as an ISO-8601 string (in RFC 3339 format). -public struct ISO8601DateTranscoder: DateTranscoder { +public struct ISO8601DateTranscoder: DateTranscoder, @unchecked Sendable { + + /// The lock protecting the formatter. + private let lock: NSLock + + /// The underlying date formatter. + private let locked_formatter: ISO8601DateFormatter + + /// Creates a new transcoder with the provided options. + /// - Parameter options: Options to override the default ones. If you provide nil here, the default options + /// are used. + public init(options: ISO8601DateFormatter.Options? = nil) { + let formatter = ISO8601DateFormatter() + if let options { formatter.formatOptions = options } + lock = NSLock() + lock.name = "com.apple.swift-openapi-generator.runtime.ISO8601DateTranscoder" + locked_formatter = formatter + } /// Creates and returns an ISO 8601 formatted string representation of the specified date. - public func encode(_ date: Date) throws -> String { ISO8601DateFormatter().string(from: date) } + public func encode(_ date: Date) throws -> String { + lock.lock() + defer { lock.unlock() } + return locked_formatter.string(from: date) + } /// Creates and returns a date object from the specified ISO 8601 formatted string representation. public func decode(_ dateString: String) throws -> Date { - guard let date = ISO8601DateFormatter().date(from: dateString) else { + lock.lock() + defer { lock.unlock() } + guard let date = locked_formatter.date(from: dateString) else { throw DecodingError.dataCorrupted( .init(codingPath: [], debugDescription: "Expected date string to be ISO8601-formatted.") ) @@ -44,6 +67,11 @@ public struct ISO8601DateTranscoder: DateTranscoder { extension DateTranscoder where Self == ISO8601DateTranscoder { /// A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format). public static var iso8601: Self { ISO8601DateTranscoder() } + + /// A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format) with fractional seconds. + public static var iso8601WithFractionalSeconds: Self { + ISO8601DateTranscoder(options: [.withInternetDateTime, .withFractionalSeconds]) + } } extension JSONEncoder.DateEncodingStrategy { diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Configuration.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Configuration.swift new file mode 100644 index 00000000..6027c423 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Configuration.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) import OpenAPIRuntime + +final class Test_Configuration: Test_Runtime { + + func testDateTranscoder_iso8601() throws { + let transcoder: any DateTranscoder = .iso8601 + XCTAssertEqual(try transcoder.encode(testDate), testDateString) + XCTAssertEqual(testDate, try transcoder.decode(testDateString)) + } + + func testDateTranscoder_iso8601WithFractionalSeconds() throws { + let transcoder: any DateTranscoder = .iso8601WithFractionalSeconds + XCTAssertEqual(try transcoder.encode(testDateWithFractionalSeconds), testDateWithFractionalSecondsString) + XCTAssertEqual(testDateWithFractionalSeconds, try transcoder.decode(testDateWithFractionalSecondsString)) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 7f1f2255..9de89902 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -42,6 +42,10 @@ class Test_Runtime: XCTestCase { var testDateString: String { "2023-01-18T10:04:11Z" } + var testDateWithFractionalSeconds: Date { Date(timeIntervalSince1970: 1_674_036_251.123) } + + var testDateWithFractionalSecondsString: String { "2023-01-18T10:04:11.123Z" } + var testDateEscapedString: String { "2023-01-18T10%3A04%3A11Z" } var testDateStringData: Data { Data(testDateString.utf8) }