From 66df7219005f9fd510e75af8b822738911edcc86 Mon Sep 17 00:00:00 2001 From: TomaszLizer Date: Sat, 6 Dec 2025 00:00:36 +0100 Subject: [PATCH 1/2] Add CodableConfigurationTests --- .../AttributedStringTestSupport.swift | 8 +- .../CodableConfigurationTests.swift | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 Tests/FoundationEssentialsTests/CodableConfiguration/CodableConfigurationTests.swift diff --git a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTestSupport.swift b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTestSupport.swift index 3935af2e5..dea8102f2 100644 --- a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTestSupport.swift +++ b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTestSupport.swift @@ -112,6 +112,10 @@ extension AttributeScopes.TestAttributes { return NonCodableType(inner: inner) } } + + struct NonCodableType : Hashable { + var inner : Int + } } #if FOUNDATION_FRAMEWORK @@ -121,10 +125,6 @@ extension AttributeScopes.TestAttributes.TestBoolAttribute : MarkdownDecodableAt extension AttributeScopes.TestAttributes.TestDoubleAttribute : MarkdownDecodableAttributedStringKey {} #endif // FOUNDATION_FRAMEWORK -struct NonCodableType : Hashable { - var inner : Int -} - extension AttributeScopes { var test: TestAttributes.Type { TestAttributes.self } diff --git a/Tests/FoundationEssentialsTests/CodableConfiguration/CodableConfigurationTests.swift b/Tests/FoundationEssentialsTests/CodableConfiguration/CodableConfigurationTests.swift new file mode 100644 index 000000000..ded94ee38 --- /dev/null +++ b/Tests/FoundationEssentialsTests/CodableConfiguration/CodableConfigurationTests.swift @@ -0,0 +1,82 @@ +import Testing + +#if canImport(FoundationEssentials) +@testable import FoundationEssentials +#else +@testable import Foundation +#endif + +@Suite("CodableConfiguration") +struct CodableConfigurationTests { + + @Test + func decodingIndirectly_succeedsForNull() async throws { + let json = "{\"testObject\":null}" + let jsonData = try #require(json.data(using: .utf8)) + + let decoder = JSONDecoder() + let sut = try decoder.decode(UsingCodableConfiguration.self, from: jsonData) + #expect(sut.testObject == nil) + } + + @Test + func decodingIndirectly_succeedsForCorrectDate() async throws { + let json = "{\"testObject\":\"Hello There\"}" + let jsonData = try #require(json.data(using: .utf8)) + + let decoder = JSONDecoder() + let sut = try decoder.decode(UsingCodableConfiguration.self, from: jsonData) + #expect(sut.testObject?.value == "Hello There") + } + + @Test + func decodingIndirectly_succeedsForMissingKey() async throws { + let json = "{}" + let jsonData = try #require(json.data(using: .utf8)) + + let decoder = JSONDecoder() + let sut = try decoder.decode(UsingCodableConfiguration.self, from: jsonData) + #expect(sut.testObject == nil) + } +} + +private extension CodableConfigurationTests { + struct NonCodableType { + let value: String + } + + /// Type that uses CodableConfiguration for decoding Optional NonCodableType + struct UsingCodableConfiguration: Codable { + @CodableConfiguration(wrappedValue: nil, from: CustomConfig.self) + var testObject: NonCodableType? + } + + /// Helper object allowing to decode Date using ISO8601 scheme + struct CustomConfig: DecodingConfigurationProviding, EncodingConfigurationProviding, Sendable { + static let encodingConfiguration = CustomConfig() + static let decodingConfiguration = CustomConfig() + + func decode(from decoder: any Decoder) throws -> NonCodableType { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + return NonCodableType(value: value) + } + + func encode(object: NonCodableType, to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(object.value) + } + } +} + +extension CodableConfigurationTests.NonCodableType: CodableWithConfiguration { + typealias CustomConfig = CodableConfigurationTests.CustomConfig + + public init(from decoder: any Decoder, configuration: CustomConfig) throws { + self = try configuration.decode(from: decoder) + } + + public func encode(to encoder: any Encoder, configuration: CustomConfig) throws { + try configuration.encode(object: self, to: encoder) + } +} From 33035d38ae18a5403ac487ae611b6bb317798b08 Mon Sep 17 00:00:00 2001 From: TomaszLizer Date: Sat, 6 Dec 2025 00:01:22 +0100 Subject: [PATCH 2/2] Fix decoding edge case for `null` value --- .../FoundationEssentials/CodableWithConfiguration.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/FoundationEssentials/CodableWithConfiguration.swift b/Sources/FoundationEssentials/CodableWithConfiguration.swift index 4fcbe79c1..c4cb27e73 100644 --- a/Sources/FoundationEssentials/CodableWithConfiguration.swift +++ b/Sources/FoundationEssentials/CodableWithConfiguration.swift @@ -53,14 +53,9 @@ public extension KeyedEncodingContainer { @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public extension KeyedDecodingContainer { func decode(_: CodableConfiguration.Type, forKey key: Self.Key) throws -> CodableConfiguration { - if self.contains(key) { - let wrapper = try self.decode(CodableConfiguration.self, forKey: key) - return CodableConfiguration(wrappedValue: wrapper.wrappedValue) - } else { - return CodableConfiguration(wrappedValue: nil) - } + let wrapper = try self.decodeIfPresent(CodableConfiguration.self, forKey: key) + return CodableConfiguration(wrappedValue: wrapper?.wrappedValue) } - }