diff --git a/Sources/StructuredFieldValues/Decoder/BareInnerListDecoder.swift b/Sources/StructuredFieldValues/Decoder/BareInnerListDecoder.swift index 5eb4d25..8de16f0 100644 --- a/Sources/StructuredFieldValues/Decoder/BareInnerListDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/BareInnerListDecoder.swift @@ -66,16 +66,20 @@ extension BareInnerListDecoder: UnkeyedDecodingContainer { self.decoder.pop() } - if type is Data.Type { + switch type { + case is Data.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Data.self) as! T - } else if type is Decimal.Type { + case is Decimal.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Decimal.self) as! T - } else if type is Date.Type { + case is Date.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Date.self) as! T - } else { + case is DisplayString.Type: + let container = try self.decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! T + default: return try type.init(from: self.decoder) } } diff --git a/Sources/StructuredFieldValues/Decoder/BareItemDecoder.swift b/Sources/StructuredFieldValues/Decoder/BareItemDecoder.swift index 3136010..3abf85b 100644 --- a/Sources/StructuredFieldValues/Decoder/BareItemDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/BareItemDecoder.swift @@ -141,6 +141,14 @@ extension BareItemDecoder: SingleValueDecodingContainer { return Date(timeIntervalSince1970: Double(date)) } + func decode(_: DisplayString.Type) throws -> DisplayString { + guard case .displayString(let string) = self.item else { + throw StructuredHeaderError.invalidTypeForItem + } + + return DisplayString(rawValue: string) + } + func decodeNil() -> Bool { // Items are never nil. false @@ -182,6 +190,8 @@ extension BareItemDecoder: SingleValueDecodingContainer { return try self.decode(Decimal.self) as! T case is Date.Type: return try self.decode(Date.self) as! T + case is DisplayString.Type: + return try self.decode(DisplayString.self) as! T default: throw StructuredHeaderError.invalidTypeForItem } diff --git a/Sources/StructuredFieldValues/Decoder/DictionaryKeyedContainer.swift b/Sources/StructuredFieldValues/Decoder/DictionaryKeyedContainer.swift index 20b1860..42f7efb 100644 --- a/Sources/StructuredFieldValues/Decoder/DictionaryKeyedContainer.swift +++ b/Sources/StructuredFieldValues/Decoder/DictionaryKeyedContainer.swift @@ -49,16 +49,20 @@ extension DictionaryKeyedContainer: KeyedDecodingContainerProtocol { self.decoder.pop() } - if type is Data.Type { + switch type { + case is Data.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Data.self) as! T - } else if type is Decimal.Type { + case is Decimal.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Decimal.self) as! T - } else if type is Date.Type { + case is Date.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Date.self) as! T - } else { + case is DisplayString.Type: + let container = try self.decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! T + default: return try type.init(from: self.decoder) } } diff --git a/Sources/StructuredFieldValues/Decoder/KeyedInnerListDecoder.swift b/Sources/StructuredFieldValues/Decoder/KeyedInnerListDecoder.swift index 5f4cb09..9f28e81 100644 --- a/Sources/StructuredFieldValues/Decoder/KeyedInnerListDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/KeyedInnerListDecoder.swift @@ -54,16 +54,20 @@ extension KeyedInnerListDecoder: KeyedDecodingContainerProtocol { self.decoder.pop() } - if type is Data.Type { + switch type { + case is Data.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Data.self) as! T - } else if type is Decimal.Type { + case is Decimal.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Decimal.self) as! T - } else if type is Date.Type { + case is Date.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Date.self) as! T - } else { + case is DisplayString.Type: + let container = try self.decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! T + default: return try type.init(from: self.decoder) } } diff --git a/Sources/StructuredFieldValues/Decoder/KeyedItemDecoder.swift b/Sources/StructuredFieldValues/Decoder/KeyedItemDecoder.swift index b46b197..ca50611 100644 --- a/Sources/StructuredFieldValues/Decoder/KeyedItemDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/KeyedItemDecoder.swift @@ -54,16 +54,20 @@ extension KeyedItemDecoder: KeyedDecodingContainerProtocol { self.decoder.pop() } - if type is Data.Type { + switch type { + case is Data.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Data.self) as! T - } else if type is Decimal.Type { + case is Decimal.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Decimal.self) as! T - } else if type is Date.Type { + case is Date.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Date.self) as! T - } else { + case is DisplayString.Type: + let container = try self.decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! T + default: return try type.init(from: self.decoder) } } diff --git a/Sources/StructuredFieldValues/Decoder/KeyedTopLevelListDecoder.swift b/Sources/StructuredFieldValues/Decoder/KeyedTopLevelListDecoder.swift index af2c781..811b65f 100644 --- a/Sources/StructuredFieldValues/Decoder/KeyedTopLevelListDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/KeyedTopLevelListDecoder.swift @@ -54,16 +54,20 @@ extension KeyedTopLevelListDecoder: KeyedDecodingContainerProtocol { self.decoder.pop() } - if type is Data.Type { + switch type { + case is Data.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Data.self) as! T - } else if type is Decimal.Type { + case is Decimal.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Decimal.self) as! T - } else if type is Date.Type { + case is Date.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Date.self) as! T - } else { + case is DisplayString.Type: + let container = try self.decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! T + default: return try type.init(from: self.decoder) } } diff --git a/Sources/StructuredFieldValues/Decoder/ParametersDecoder.swift b/Sources/StructuredFieldValues/Decoder/ParametersDecoder.swift index 10d35b7..80976a5 100644 --- a/Sources/StructuredFieldValues/Decoder/ParametersDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/ParametersDecoder.swift @@ -49,16 +49,20 @@ extension ParametersDecoder: KeyedDecodingContainerProtocol { self.decoder.pop() } - if type is Data.Type { + switch type { + case is Data.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Data.self) as! T - } else if type is Decimal.Type { + case is Decimal.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Decimal.self) as! T - } else if type is Date.Type { + case is Date.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Date.self) as! T - } else { + case is DisplayString.Type: + let container = try self.decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! T + default: return try type.init(from: self.decoder) } } diff --git a/Sources/StructuredFieldValues/Decoder/StructuredFieldValueDecoder.swift b/Sources/StructuredFieldValues/Decoder/StructuredFieldValueDecoder.swift index 87d9b69..c9d8861 100644 --- a/Sources/StructuredFieldValues/Decoder/StructuredFieldValueDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/StructuredFieldValueDecoder.swift @@ -111,16 +111,20 @@ extension StructuredFieldValueDecoder { // An escape hatch here for top-level data: if we don't do this, it'll ask for // an unkeyed container and get very confused. - if type is Data.Type { + switch type { + case is Data.Type: let container = try decoder.singleValueContainer() return try container.decode(Data.self) as! StructuredField - } else if type is Decimal.Type { + case is Decimal.Type: let container = try decoder.singleValueContainer() return try container.decode(Decimal.self) as! StructuredField - } else if type is Date.Type { + case is Date.Type: let container = try decoder.singleValueContainer() return try container.decode(Date.self) as! StructuredField - } else { + case is DisplayString.Type: + let container = try decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! StructuredField + default: return try type.init(from: decoder) } } diff --git a/Sources/StructuredFieldValues/Decoder/TopLevelListDecoder.swift b/Sources/StructuredFieldValues/Decoder/TopLevelListDecoder.swift index 83fd430..b7399c7 100644 --- a/Sources/StructuredFieldValues/Decoder/TopLevelListDecoder.swift +++ b/Sources/StructuredFieldValues/Decoder/TopLevelListDecoder.swift @@ -66,16 +66,20 @@ extension TopLevelListDecoder: UnkeyedDecodingContainer { self.decoder.pop() } - if type is Data.Type { + switch type { + case is Data.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Data.self) as! T - } else if type is Decimal.Type { + case is Decimal.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Decimal.self) as! T - } else if type is Date.Type { + case is Date.Type: let container = try self.decoder.singleValueContainer() return try container.decode(Date.self) as! T - } else { + case is DisplayString.Type: + let container = try self.decoder.singleValueContainer() + return try container.decode(DisplayString.self) as! T + default: return try type.init(from: self.decoder) } } diff --git a/Sources/StructuredFieldValues/DisplayString.swift b/Sources/StructuredFieldValues/DisplayString.swift new file mode 100644 index 0000000..2dfcf02 --- /dev/null +++ b/Sources/StructuredFieldValues/DisplayString.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2020-2024 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A type that represents the Display String Structured Type. +public struct DisplayString: RawRepresentable, Codable, Equatable, Hashable { + public typealias RawValue = String + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } +} diff --git a/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift b/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift index 3ab1ae1..7c56dee 100644 --- a/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift +++ b/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift @@ -150,19 +150,20 @@ class _StructuredFieldEncoder { fileprivate func encodeItemField(_ data: StructuredField) throws -> [UInt8] { self.push(key: .init(stringValue: ""), newStorage: .itemHeader) - // There's an awkward special hook here: if the outer type is `Data` or `Decimal`, - // we skip the regular encoding path. This is because otherwise `Data` will - // ask for an unkeyed container and `Decimal` for a keyed one, - // and it all falls apart. + // There's an awkward special hook here: if the outer type is `Data`, `Decimal`, `Date` or + // `DisplayString`, we skip the regular encoding path. // // Everything else goes through the normal flow. - if let value = data as? Data { - try self.encode(value) - } else if let value = data as? Decimal { - try self.encode(value) - } else if let value = data as? Date { - try self.encode(value) - } else { + switch data { + case is Data: + try self.encode(data) + case is Decimal: + try self.encode(data) + case is Date: + try self.encode(data) + case is DisplayString: + try self.encode(data) + default: try data.encode(to: self) } @@ -316,6 +317,10 @@ extension _StructuredFieldEncoder: SingleValueEncodingContainer { try self.currentStackEntry.storage.insertBareItem(.date(date)) } + func encode(_ data: DisplayString) throws { + try self.currentStackEntry.storage.insertBareItem(.displayString(data.rawValue)) + } + func encode(_ value: T) throws where T: Encodable { switch value { case let value as UInt8: @@ -352,6 +357,8 @@ extension _StructuredFieldEncoder: SingleValueEncodingContainer { try self.encode(value) case let value as Date: try self.encode(value) + case let value as DisplayString: + try self.encode(value) default: throw StructuredHeaderError.invalidTypeForItem } @@ -480,6 +487,10 @@ extension _StructuredFieldEncoder { try self.currentStackEntry.storage.appendBareItem(.date(date)) } + func append(_ value: DisplayString) throws { + try self.currentStackEntry.storage.appendBareItem(.displayString(value.rawValue)) + } + func append(_ value: T) throws where T: Encodable { switch value { case let value as UInt8: @@ -516,6 +527,8 @@ extension _StructuredFieldEncoder { try self.append(value) case let value as Date: try self.append(value) + case let value as DisplayString: + try self.append(value) default: // Some other codable type. switch self.currentStackEntry.storage { @@ -658,6 +671,12 @@ extension _StructuredFieldEncoder { try self.currentStackEntry.storage.insertBareItem(.date(date), atKey: key) } + func encode(_ value: DisplayString, forKey key: String) throws { + let key = self.sanitizeKey(key) + let displayString = value.rawValue + try self.currentStackEntry.storage.insertBareItem(.displayString(displayString), atKey: key) + } + func encode(_ value: T, forKey key: String) throws where T: Encodable { let key = self.sanitizeKey(key) @@ -696,6 +715,8 @@ extension _StructuredFieldEncoder { try self.encode(value, forKey: key) case let value as Date: try self.encode(value, forKey: key) + case let value as DisplayString: + try self.encode(value, forKey: key) default: // Ok, we don't know what this is. This can only happen for a dictionary, or // for anything with parameters, or for lists, or for inner lists. diff --git a/Tests/StructuredFieldValuesTests/StructuredFieldDecoderTests.swift b/Tests/StructuredFieldValuesTests/StructuredFieldDecoderTests.swift index 5d14101..d56350a 100644 --- a/Tests/StructuredFieldValuesTests/StructuredFieldDecoderTests.swift +++ b/Tests/StructuredFieldValuesTests/StructuredFieldDecoderTests.swift @@ -611,4 +611,137 @@ final class StructuredFieldDecoderTests: XCTestCase { try StructuredFieldValueDecoder().decode(from: Array(headerField.utf8)) ) } + + func testDecodingDisplayStringAsTopLevelData() throws { + XCTAssertEqual( + ItemField(DisplayString(rawValue: "füü")), + try StructuredFieldValueDecoder().decode(from: Array("%\"f%c3%bc%c3%bc\"".utf8)) + ) + } + + func testDecodingDisplayStringAsParameterisedData() throws { + struct Item: StructuredFieldValue, Equatable { + static let structuredFieldType: StructuredFieldType = .item + var item: DisplayString + var parameters: [String: Float] + } + + XCTAssertEqual( + Item( + item: DisplayString(rawValue: "füü"), + parameters: [:] + ), + try StructuredFieldValueDecoder().decode( + Item.self, + from: Array("%\"f%c3%bc%c3%bc\"".utf8) + ) + ) + + XCTAssertEqual( + Item(item: DisplayString(rawValue: "füü"), parameters: ["q": 0.8]), + try StructuredFieldValueDecoder().decode( + Item.self, + from: Array("%\"f%c3%bc%c3%bc\";q=0.8".utf8) + ) + ) + } + + func testDecodingDisplayStringInParameterField() throws { + struct Item: StructuredFieldValue, Equatable { + static let structuredFieldType: StructuredFieldType = .item + var item: Int + var parameters: [String: DisplayString] + } + + XCTAssertEqual( + Item(item: 1, parameters: ["q": DisplayString(rawValue: "füü")]), + try StructuredFieldValueDecoder().decode( + Item.self, + from: Array("1;q=%\"f%c3%bc%c3%bc\"".utf8) + ) + ) + } + + func testDecodingDisplayStringInOuterListRaw() throws { + XCTAssertEqual( + List( + [ + DisplayString(rawValue: "füü"), + DisplayString(rawValue: "foo \"bar\" \\ baz"), + ] + ), + try StructuredFieldValueDecoder().decode( + from: Array("%\"f%c3%bc%c3%bc\", %\"foo %22bar%22 \\ baz\"".utf8) + ) + ) + } + + func testDecodingDisplayStringInInnerListRaw() throws { + XCTAssertEqual( + List( + Array( + repeating: [ + DisplayString(rawValue: "füü"), + DisplayString(rawValue: "foo \"bar\" \\ baz"), + ], + count: 2 + ) + ), + try StructuredFieldValueDecoder().decode( + from: Array( + """ + (%\"f%c3%bc%c3%bc\" %\"foo %22bar%22 \\ baz\"), (%\"f%c3%bc%c3%bc\" %\"foo \ + %22bar%22 \\ baz\") + """.utf8 + ) + ) + ) + } + + func testDecodingDisplayStringInInnerListKeyed() throws { + struct ListField: Codable, Equatable { + var items: [DisplayString] + var parameters: [String: Bool] + } + XCTAssertEqual( + List( + Array( + repeating: ListField( + items: [ + DisplayString(rawValue: "füü"), + DisplayString(rawValue: "foo \"bar\" \\ baz"), + ], + parameters: ["foo": true] + ), + count: 2 + ) + ), + try StructuredFieldValueDecoder().decode( + from: Array( + """ + (%\"f%c3%bc%c3%bc\" %\"foo %22bar%22 \\ baz\");foo, (%\"f%c3%bc%c3%bc\" %\"foo \ + %22bar%22 \\ baz\");foo + """.utf8 + ) + ) + ) + } + + func testDecodingDisplayStringInDictionaries() throws { + struct DictionaryField: StructuredFieldValue, Equatable { + static let structuredFieldType: StructuredFieldType = .dictionary + var bin: DisplayString + var box: DisplayString + } + + XCTAssertEqual( + DictionaryField( + bin: DisplayString(rawValue: "füü"), + box: DisplayString(rawValue: "foo \"bar\" \\ baz") + ), + try StructuredFieldValueDecoder().decode( + from: Array("bin=%\"f%c3%bc%c3%bc\", box=%\"foo %22bar%22 \\ baz\"".utf8) + ) + ) + } } diff --git a/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift b/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift index af12517..a6ce1b8 100644 --- a/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift +++ b/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift @@ -53,6 +53,12 @@ final class StructuredFieldEncoderTests: XCTestCase { Array("@-1659578233".utf8), try encoder.encode(ItemField(Date(timeIntervalSince1970: -1_659_578_233))) ) + + // Display String + XCTAssertEqual( + Array("%\"f%c3%bc%c3%bc\"".utf8), + try encoder.encode(ItemField(DisplayString(rawValue: "füü"))) + ) } func testEncodeKeyedItemHeader() throws { @@ -126,6 +132,20 @@ final class StructuredFieldEncoderTests: XCTestCase { ) ) ) + + // Display String + XCTAssertEqual( + Array("%\"f%c3%bc%c3%bc\";x".utf8), + try encoder.encode( + KeyedItem(item: DisplayString(rawValue: "füü"), parameters: ["x": true]) + ) + ) + XCTAssertEqual( + Array("%\"foo %22bar%22 \\ baz\"".utf8), + try encoder.encode( + KeyedItem(item: DisplayString(rawValue: "foo \"bar\" \\ baz"), parameters: [:]) + ) + ) } func testEncodeKeyedItemHeaderWithParamsAsStruct() throws { @@ -232,6 +252,16 @@ final class StructuredFieldEncoderTests: XCTestCase { ) ) ) + + // Display String + XCTAssertEqual( + Array("%\"f%c3%bc%c3%bc\", %\"foo %22bar%22 \\ baz\"".utf8), + try encoder.encode( + List( + [DisplayString(rawValue: "füü"), DisplayString(rawValue: "foo \"bar\" \\ baz")] + ) + ) + ) } func testListFieldInnerItemsWithDict() throws {