diff --git a/DictionaryCoder.xcodeproj/project.pbxproj b/DictionaryCoder.xcodeproj/project.pbxproj index f70f984..395dfae 100644 --- a/DictionaryCoder.xcodeproj/project.pbxproj +++ b/DictionaryCoder.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ C001ACAA2F027A4B008C37A5 /* MutexStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C001ACA72F027A47008C37A5 /* MutexStorage.swift */; }; C001ACAB2F027A4B008C37A5 /* MutexStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C001ACA72F027A47008C37A5 /* MutexStorage.swift */; }; C006F1AA2406B1F000C3F783 /* DictionaryDataDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFA23E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift */; }; + 34C8CEDCE88D06F5994D6149 /* DictionaryDecimalDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD64BF90D40C01B95CBB75B /* DictionaryDecimalDecodingStrategy.swift */; }; C006F1AB2406B1F000C3F783 /* DictionaryDateDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF923E1E0AE00F12CCB /* DictionaryDateDecodingStrategy.swift */; }; C006F1AC2406B1F000C3F783 /* DictionaryKeyDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF823E1E0AE00F12CCB /* DictionaryKeyDecodingStrategy.swift */; }; C006F1AD2406B1F000C3F783 /* DictionaryNonConformingFloatDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFB23E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift */; }; @@ -26,6 +27,7 @@ C006F1B22406B1F500C3F783 /* DictionarySingleValueDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFD23E1E0AF00F12CCB /* DictionarySingleValueDecodingContainer.swift */; }; C006F1B32406B1F500C3F783 /* DictionaryUnkeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFE23E1E0AF00F12CCB /* DictionaryUnkeyedDecodingContainer.swift */; }; C006F1B42406B1FA00C3F783 /* DictionaryDataEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0423E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift */; }; + 16E10BC5FDEC1D729242BA52 /* DictionaryDecimalEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB19ABEC86D0C1E9BEF2848 /* DictionaryDecimalEncodingStrategy.swift */; }; C006F1B52406B1FA00C3F783 /* DictionaryDateEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0323E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift */; }; C006F1B62406B1FA00C3F783 /* DictionaryKeyEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0223E1E0AF00F12CCB /* DictionaryKeyEncodingStrategy.swift */; }; C006F1B72406B1FA00C3F783 /* DictionaryNonConformingFloatEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0523E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift */; }; @@ -39,14 +41,17 @@ C006F1BF2406B1FE00C3F783 /* DictionarySingleValueEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0923E1E0AF00F12CCB /* DictionarySingleValueEncodingContainer.swift */; }; C006F1C02406B1FE00C3F783 /* DictionaryUnkeyedEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0723E1E0AF00F12CCB /* DictionaryUnkeyedEncodingContainer.swift */; }; C006F1C12406B20400C3F783 /* AnyCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF323E1E0AE00F12CCB /* AnyCodingKey.swift */; }; - C006F1C22406B20400C3F783 /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB5A23E1E23B00F12CCB /* Optional+Extensions.swift */; }; + 40B78C975BEC5E40F990D9AF /* DictionaryISO8601Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */; }; C006F1C32406B20400C3F783 /* RangeReplaceableCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF223E1E0AE00F12CCB /* RangeReplaceableCollection+Extensions.swift */; }; C013EB0F23E1E0AF00F12CCB /* RangeReplaceableCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF223E1E0AE00F12CCB /* RangeReplaceableCollection+Extensions.swift */; }; C013EB1023E1E0AF00F12CCB /* RangeReplaceableCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF223E1E0AE00F12CCB /* RangeReplaceableCollection+Extensions.swift */; }; C013EB1123E1E0AF00F12CCB /* RangeReplaceableCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF223E1E0AE00F12CCB /* RangeReplaceableCollection+Extensions.swift */; }; C013EB1223E1E0AF00F12CCB /* AnyCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF323E1E0AE00F12CCB /* AnyCodingKey.swift */; }; + D0C83D1B21B28BEB577B4B03 /* DictionaryISO8601Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */; }; C013EB1323E1E0AF00F12CCB /* AnyCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF323E1E0AE00F12CCB /* AnyCodingKey.swift */; }; + 1FEE72E38D243B1482E72824 /* DictionaryISO8601Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */; }; C013EB1423E1E0AF00F12CCB /* AnyCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF323E1E0AE00F12CCB /* AnyCodingKey.swift */; }; + 6D33E77C3C18283A7BADD948 /* DictionaryISO8601Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */; }; C013EB1523E1E0AF00F12CCB /* DictionaryDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF523E1E0AE00F12CCB /* DictionaryDecoder.swift */; }; C013EB1623E1E0AF00F12CCB /* DictionaryDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF523E1E0AE00F12CCB /* DictionaryDecoder.swift */; }; C013EB1723E1E0AF00F12CCB /* DictionaryDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF523E1E0AE00F12CCB /* DictionaryDecoder.swift */; }; @@ -60,8 +65,11 @@ C013EB1F23E1E0AF00F12CCB /* DictionaryDateDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF923E1E0AE00F12CCB /* DictionaryDateDecodingStrategy.swift */; }; C013EB2023E1E0AF00F12CCB /* DictionaryDateDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAF923E1E0AE00F12CCB /* DictionaryDateDecodingStrategy.swift */; }; C013EB2123E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFA23E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift */; }; + 26898CBB383FF7FDFD5F95E8 /* DictionaryDecimalDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD64BF90D40C01B95CBB75B /* DictionaryDecimalDecodingStrategy.swift */; }; C013EB2223E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFA23E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift */; }; + 51AE1E45DC1E17FA460E14A9 /* DictionaryDecimalDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD64BF90D40C01B95CBB75B /* DictionaryDecimalDecodingStrategy.swift */; }; C013EB2323E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFA23E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift */; }; + 6766F9B09526EA43F23ABC51 /* DictionaryDecimalDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD64BF90D40C01B95CBB75B /* DictionaryDecimalDecodingStrategy.swift */; }; C013EB2423E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFB23E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift */; }; C013EB2523E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFB23E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift */; }; C013EB2623E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EAFB23E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift */; }; @@ -84,8 +92,11 @@ C013EB3723E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0323E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift */; }; C013EB3823E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0323E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift */; }; C013EB3923E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0423E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift */; }; + E3C3287C6B11499A5E5B00C1 /* DictionaryDecimalEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB19ABEC86D0C1E9BEF2848 /* DictionaryDecimalEncodingStrategy.swift */; }; C013EB3A23E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0423E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift */; }; + 9345D64F759EDFE41E32C67E /* DictionaryDecimalEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB19ABEC86D0C1E9BEF2848 /* DictionaryDecimalEncodingStrategy.swift */; }; C013EB3B23E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0423E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift */; }; + 80CA8A45A144A2E368CEF354 /* DictionaryDecimalEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB19ABEC86D0C1E9BEF2848 /* DictionaryDecimalEncodingStrategy.swift */; }; C013EB3C23E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0523E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift */; }; C013EB3D23E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0523E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift */; }; C013EB3E23E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0523E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift */; }; @@ -116,9 +127,6 @@ C013EB5723E1E0AF00F12CCB /* DictionaryComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0E23E1E0AF00F12CCB /* DictionaryComponent.swift */; }; C013EB5823E1E0AF00F12CCB /* DictionaryComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0E23E1E0AF00F12CCB /* DictionaryComponent.swift */; }; C013EB5923E1E0AF00F12CCB /* DictionaryComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB0E23E1E0AF00F12CCB /* DictionaryComponent.swift */; }; - C013EB5B23E1E23B00F12CCB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB5A23E1E23B00F12CCB /* Optional+Extensions.swift */; }; - C013EB5C23E1E23B00F12CCB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB5A23E1E23B00F12CCB /* Optional+Extensions.swift */; }; - C013EB5D23E1E23B00F12CCB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB5A23E1E23B00F12CCB /* Optional+Extensions.swift */; }; C013EB6F23E1E2F300F12CCB /* DictionaryDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB6123E1E28700F12CCB /* DictionaryDecoderTests.swift */; }; C013EB7023E1E2F300F12CCB /* DictionaryEncoderStrategiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB5F23E1E28700F12CCB /* DictionaryEncoderStrategiesTests.swift */; }; C013EB7123E1E2F300F12CCB /* DictionaryEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB6023E1E28700F12CCB /* DictionaryEncoderTests.swift */; }; @@ -177,11 +185,13 @@ C006F1A22406B13E00C3F783 /* DictionaryCoder.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DictionaryCoder.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C013EAF223E1E0AE00F12CCB /* RangeReplaceableCollection+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RangeReplaceableCollection+Extensions.swift"; sourceTree = ""; }; C013EAF323E1E0AE00F12CCB /* AnyCodingKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCodingKey.swift; sourceTree = ""; }; + 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryISO8601Style.swift; sourceTree = ""; }; C013EAF523E1E0AE00F12CCB /* DictionaryDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDecoder.swift; sourceTree = ""; }; C013EAF623E1E0AE00F12CCB /* DictionaryKeyedDecodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryKeyedDecodingContainer.swift; sourceTree = ""; }; C013EAF823E1E0AE00F12CCB /* DictionaryKeyDecodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryKeyDecodingStrategy.swift; sourceTree = ""; }; C013EAF923E1E0AE00F12CCB /* DictionaryDateDecodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDateDecodingStrategy.swift; sourceTree = ""; }; C013EAFA23E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDataDecodingStrategy.swift; sourceTree = ""; }; + FFD64BF90D40C01B95CBB75B /* DictionaryDecimalDecodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDecimalDecodingStrategy.swift; sourceTree = ""; }; C013EAFB23E1E0AF00F12CCB /* DictionaryNonConformingFloatDecodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryNonConformingFloatDecodingStrategy.swift; sourceTree = ""; }; C013EAFC23E1E0AF00F12CCB /* DictionaryComponentDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryComponentDecoder.swift; sourceTree = ""; }; C013EAFD23E1E0AF00F12CCB /* DictionarySingleValueDecodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionarySingleValueDecodingContainer.swift; sourceTree = ""; }; @@ -190,6 +200,7 @@ C013EB0223E1E0AF00F12CCB /* DictionaryKeyEncodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryKeyEncodingStrategy.swift; sourceTree = ""; }; C013EB0323E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDateEncodingStrategy.swift; sourceTree = ""; }; C013EB0423E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDataEncodingStrategy.swift; sourceTree = ""; }; + EFB19ABEC86D0C1E9BEF2848 /* DictionaryDecimalEncodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDecimalEncodingStrategy.swift; sourceTree = ""; }; C013EB0523E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryNonConformingFloatEncodingStrategy.swift; sourceTree = ""; }; C013EB0623E1E0AF00F12CCB /* DictionaryAnyKeyedEncodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryAnyKeyedEncodingContainer.swift; sourceTree = ""; }; C013EB0723E1E0AF00F12CCB /* DictionaryUnkeyedEncodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryUnkeyedEncodingContainer.swift; sourceTree = ""; }; @@ -200,7 +211,6 @@ C013EB0C23E1E0AF00F12CCB /* DictionaryKeyedEncodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryKeyedEncodingContainer.swift; sourceTree = ""; }; C013EB0D23E1E0AF00F12CCB /* DictionaryComponentEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryComponentEncoder.swift; sourceTree = ""; }; C013EB0E23E1E0AF00F12CCB /* DictionaryComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryComponent.swift; sourceTree = ""; }; - C013EB5A23E1E23B00F12CCB /* Optional+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = ""; }; C013EB5E23E1E28700F12CCB /* DictionaryDecoderStrategiesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryDecoderStrategiesTests.swift; sourceTree = ""; }; C013EB5F23E1E28700F12CCB /* DictionaryEncoderStrategiesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryEncoderStrategiesTests.swift; sourceTree = ""; }; C013EB6023E1E28700F12CCB /* DictionaryEncoderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryEncoderTests.swift; sourceTree = ""; }; @@ -305,7 +315,7 @@ children = ( C001ACA62F027A3B008C37A5 /* Mutex */, C013EAF323E1E0AE00F12CCB /* AnyCodingKey.swift */, - C013EB5A23E1E23B00F12CCB /* Optional+Extensions.swift */, + 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */, C013EAF223E1E0AE00F12CCB /* RangeReplaceableCollection+Extensions.swift */, ); path = Tools; @@ -328,6 +338,7 @@ isa = PBXGroup; children = ( C013EAFA23E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift */, + FFD64BF90D40C01B95CBB75B /* DictionaryDecimalDecodingStrategy.swift */, C013EAF923E1E0AE00F12CCB /* DictionaryDateDecodingStrategy.swift */, C013EAFF23E1E0AF00F12CCB /* DictionaryDecodingOptions.swift */, C013EAF823E1E0AE00F12CCB /* DictionaryKeyDecodingStrategy.swift */, @@ -356,6 +367,7 @@ isa = PBXGroup; children = ( C013EB0423E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift */, + EFB19ABEC86D0C1E9BEF2848 /* DictionaryDecimalEncodingStrategy.swift */, C013EB0323E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift */, C013EB0B23E1E0AF00F12CCB /* DictionaryEncodingOptions.swift */, C013EB0223E1E0AF00F12CCB /* DictionaryKeyEncodingStrategy.swift */, @@ -774,9 +786,9 @@ buildActionMask = 2147483647; files = ( C006F1BB2406B1FE00C3F783 /* DictionaryComponentEncoder.swift in Sources */, - C006F1C22406B20400C3F783 /* Optional+Extensions.swift in Sources */, C006F1AC2406B1F000C3F783 /* DictionaryKeyDecodingStrategy.swift in Sources */, C006F1C12406B20400C3F783 /* AnyCodingKey.swift in Sources */, + 40B78C975BEC5E40F990D9AF /* DictionaryISO8601Style.swift in Sources */, C006F1C32406B20400C3F783 /* RangeReplaceableCollection+Extensions.swift in Sources */, C006F1B52406B1FA00C3F783 /* DictionaryDateEncodingStrategy.swift in Sources */, C006F1BD2406B1FE00C3F783 /* DictionaryEncodingOptions.swift in Sources */, @@ -785,10 +797,12 @@ C006F1B32406B1F500C3F783 /* DictionaryUnkeyedDecodingContainer.swift in Sources */, C006F1BC2406B1FE00C3F783 /* DictionaryEncoder.swift in Sources */, C006F1B42406B1FA00C3F783 /* DictionaryDataEncodingStrategy.swift in Sources */, + 16E10BC5FDEC1D729242BA52 /* DictionaryDecimalEncodingStrategy.swift in Sources */, C006F1B02406B1F500C3F783 /* DictionaryDecodingOptions.swift in Sources */, C006F1B22406B1F500C3F783 /* DictionarySingleValueDecodingContainer.swift in Sources */, C006F1AE2406B1F500C3F783 /* DictionaryComponentDecoder.swift in Sources */, C006F1AA2406B1F000C3F783 /* DictionaryDataDecodingStrategy.swift in Sources */, + 34C8CEDCE88D06F5994D6149 /* DictionaryDecimalDecodingStrategy.swift in Sources */, C006F1AD2406B1F000C3F783 /* DictionaryNonConformingFloatDecodingStrategy.swift in Sources */, C006F1B82406B1FE00C3F783 /* DictionaryAnyKeyedEncodingContainer.swift in Sources */, C006F1AF2406B1F500C3F783 /* DictionaryDecoder.swift in Sources */, @@ -812,17 +826,19 @@ C013EB5423E1E0AF00F12CCB /* DictionaryComponentEncoder.swift in Sources */, C013EB4B23E1E0AF00F12CCB /* DictionaryComponentContainer.swift in Sources */, C013EB4E23E1E0AF00F12CCB /* DictionaryEncodingOptions.swift in Sources */, - C013EB5B23E1E23B00F12CCB /* Optional+Extensions.swift in Sources */, C013EB3C23E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift in Sources */, C013EB5723E1E0AF00F12CCB /* DictionaryComponent.swift in Sources */, C013EB1223E1E0AF00F12CCB /* AnyCodingKey.swift in Sources */, + D0C83D1B21B28BEB577B4B03 /* DictionaryISO8601Style.swift in Sources */, C001ACAA2F027A4B008C37A5 /* MutexStorage.swift in Sources */, C013EB2723E1E0AF00F12CCB /* DictionaryComponentDecoder.swift in Sources */, C013EB1823E1E0AF00F12CCB /* DictionaryKeyedDecodingContainer.swift in Sources */, C013EB2123E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift in Sources */, + 26898CBB383FF7FDFD5F95E8 /* DictionaryDecimalDecodingStrategy.swift in Sources */, C013EB4523E1E0AF00F12CCB /* DictionaryEncoder.swift in Sources */, C013EB1523E1E0AF00F12CCB /* DictionaryDecoder.swift in Sources */, C013EB3923E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift in Sources */, + E3C3287C6B11499A5E5B00C1 /* DictionaryDecimalEncodingStrategy.swift in Sources */, C013EB3023E1E0AF00F12CCB /* DictionaryDecodingOptions.swift in Sources */, C013EB2D23E1E0AF00F12CCB /* DictionaryUnkeyedDecodingContainer.swift in Sources */, C013EB5123E1E0AF00F12CCB /* DictionaryKeyedEncodingContainer.swift in Sources */, @@ -861,17 +877,19 @@ C013EB5523E1E0AF00F12CCB /* DictionaryComponentEncoder.swift in Sources */, C013EB4C23E1E0AF00F12CCB /* DictionaryComponentContainer.swift in Sources */, C013EB4F23E1E0AF00F12CCB /* DictionaryEncodingOptions.swift in Sources */, - C013EB5C23E1E23B00F12CCB /* Optional+Extensions.swift in Sources */, C013EB3D23E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift in Sources */, C013EB5823E1E0AF00F12CCB /* DictionaryComponent.swift in Sources */, C013EB1323E1E0AF00F12CCB /* AnyCodingKey.swift in Sources */, + 1FEE72E38D243B1482E72824 /* DictionaryISO8601Style.swift in Sources */, C001ACA82F027A4B008C37A5 /* MutexStorage.swift in Sources */, C013EB2823E1E0AF00F12CCB /* DictionaryComponentDecoder.swift in Sources */, C013EB1923E1E0AF00F12CCB /* DictionaryKeyedDecodingContainer.swift in Sources */, C013EB2223E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift in Sources */, + 51AE1E45DC1E17FA460E14A9 /* DictionaryDecimalDecodingStrategy.swift in Sources */, C013EB4623E1E0AF00F12CCB /* DictionaryEncoder.swift in Sources */, C013EB1623E1E0AF00F12CCB /* DictionaryDecoder.swift in Sources */, C013EB3A23E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift in Sources */, + 9345D64F759EDFE41E32C67E /* DictionaryDecimalEncodingStrategy.swift in Sources */, C013EB3123E1E0AF00F12CCB /* DictionaryDecodingOptions.swift in Sources */, C013EB2E23E1E0AF00F12CCB /* DictionaryUnkeyedDecodingContainer.swift in Sources */, C013EB5223E1E0AF00F12CCB /* DictionaryKeyedEncodingContainer.swift in Sources */, @@ -910,17 +928,19 @@ C013EB5623E1E0AF00F12CCB /* DictionaryComponentEncoder.swift in Sources */, C013EB4D23E1E0AF00F12CCB /* DictionaryComponentContainer.swift in Sources */, C013EB5023E1E0AF00F12CCB /* DictionaryEncodingOptions.swift in Sources */, - C013EB5D23E1E23B00F12CCB /* Optional+Extensions.swift in Sources */, C013EB3E23E1E0AF00F12CCB /* DictionaryNonConformingFloatEncodingStrategy.swift in Sources */, C013EB5923E1E0AF00F12CCB /* DictionaryComponent.swift in Sources */, C013EB1423E1E0AF00F12CCB /* AnyCodingKey.swift in Sources */, + 6D33E77C3C18283A7BADD948 /* DictionaryISO8601Style.swift in Sources */, C001ACA92F027A4B008C37A5 /* MutexStorage.swift in Sources */, C013EB2923E1E0AF00F12CCB /* DictionaryComponentDecoder.swift in Sources */, C013EB1A23E1E0AF00F12CCB /* DictionaryKeyedDecodingContainer.swift in Sources */, C013EB2323E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift in Sources */, + 6766F9B09526EA43F23ABC51 /* DictionaryDecimalDecodingStrategy.swift in Sources */, C013EB4723E1E0AF00F12CCB /* DictionaryEncoder.swift in Sources */, C013EB1723E1E0AF00F12CCB /* DictionaryDecoder.swift in Sources */, C013EB3B23E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift in Sources */, + 80CA8A45A144A2E368CEF354 /* DictionaryDecimalEncodingStrategy.swift in Sources */, C013EB3223E1E0AF00F12CCB /* DictionaryDecodingOptions.swift in Sources */, C013EB2F23E1E0AF00F12CCB /* DictionaryUnkeyedDecodingContainer.swift in Sources */, C013EB5323E1E0AF00F12CCB /* DictionaryKeyedEncodingContainer.swift in Sources */, diff --git a/Sources/Decoder/DictionaryComponentDecoder.swift b/Sources/Decoder/DictionaryComponentDecoder.swift index be18390..1ea6ca7 100644 --- a/Sources/Decoder/DictionaryComponentDecoder.swift +++ b/Sources/Decoder/DictionaryComponentDecoder.swift @@ -10,22 +10,139 @@ extension DictionaryComponentDecoder { // MARK: - Instance Methods + @inline(__always) private func decodePrimitiveValue( of type: T.Type = T.self, from component: Any?, at codingPath: [CodingKey] ) throws -> T { - guard let value = component as? T else { + guard let component else { + return try decodeConvertedNumber(from: component, at: codingPath) + } + + let componentType = Swift.type(of: component) + + if componentType == T.self, let value = component as? T { + return value + } + + if let value = bridgeFoundationComponent(component, of: componentType, to: type) { + return value + } + + if let value = component as? T { + return value + } + + return try decodeConvertedNumber(from: component, at: codingPath) + } + + // Bridging a Foundation object through `as? T` looks the bridging up on every call, + // while bridging one known to be of the class that `T` bridges from calls the same conversion directly. + @inline(__always) + private func bridgeFoundationComponent(_ component: Any, of componentType: Any.Type, to type: T.Type) -> T? { + if T.self == String.self { + guard componentType is NSString.Type, let string = component as? NSString else { + return nil + } + + return string as? T + } + + if T.self == Data.self { + guard componentType is NSData.Type, let data = component as? NSData else { + return nil + } + + return data as? T + } + + guard componentType is NSNumber.Type, let number = component as? NSNumber else { + return nil + } + + return number as? T + } + + // Numbers of other types are converted the same way as `NSNumber`, + // so a dictionary decodes equally whether it holds Swift numbers or `NSNumber` instances. + // Unlike `NSNumber`, booleans are not converted to or from numbers here, as in `JSONDecoder`. + @inline(never) + private func decodeConvertedNumber( + from component: Any?, + at codingPath: [CodingKey] + ) throws -> T { + let number = component as? NSNumber + + guard let number, !isBoolean(number), !(T.self is Bool.Type), let value = number as? T else { throw DecodingError.invalidComponent(component, of: T.self, at: codingPath) } return value } - private func decodeNonPrimitiveValue( + // Booleans are bridged to `NSNumber` too, so they are told apart by their Core Foundation type. + private func isBoolean(_ number: NSNumber) -> Bool { + CFGetTypeID(number) == CFBooleanGetTypeID() + } + + private func decodeDecimal(from component: Any?, at codingPath: [CodingKey]) throws -> Decimal { + let strategy = options.decimalDecodingStrategy + + if strategy.contains(.number) { + if let decimal = component as? Decimal { + return decimal + } + + if let number = component as? NSNumber, !isBoolean(number) { + return number.decimalValue + } + } + + guard strategy.contains(.deferredToDecimal) else { + throw DecodingError.invalidComponent(component, of: Decimal.self, at: codingPath) + } + + return try decodeNonPrimitiveValue(from: component, at: codingPath) + } + +#if compiler(>=6.0) + // `NSNumber` does not bridge 128-bit integers, so other integers are converted exactly. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + private func decodeWideInteger( of type: T.Type = T.self, from component: Any?, at codingPath: [CodingKey] + ) throws -> T { + if let value = component as? T { + return value + } + + let value: T? + + switch component { + case let integer as any BinaryInteger: + value = T(exactly: integer) + + case let number as NSNumber where !isBoolean(number): + value = (number as? Int64).flatMap(T.init(exactly:)) ?? (number as? UInt64).flatMap(T.init(exactly:)) + + default: + value = nil + } + + guard let value else { + throw DecodingError.invalidComponent(component, of: T.self, at: codingPath) + } + + return value + } +#endif + + private func decodeNonPrimitiveValue( + of type: T.Type = T.self, + from component: consuming Any?, + at codingPath: consuming [CodingKey] ) throws -> T { let decoder = DictionarySingleValueDecodingContainer( component: component, @@ -39,8 +156,8 @@ extension DictionaryComponentDecoder { private func decodeCustomizedValue( of type: T.Type = T.self, - from component: Any?, - at codingPath: [CodingKey], + from component: consuming Any?, + at codingPath: consuming [CodingKey], closure: (_ decoder: Decoder) throws -> T ) throws -> T { let decoder = DictionarySingleValueDecodingContainer( @@ -57,8 +174,8 @@ extension DictionaryComponentDecoder { from component: Any?, at codingPath: [CodingKey] ) throws -> T { - switch component { - case let string as String: + // The strategy is checked first, so that numbers are not cast to `String` with the default strategy. + if case .convertFromString = options.nonConformingFloatDecodingStrategy, let string = component as? String { switch options.nonConformingFloatDecodingStrategy { case let .convertFromString(positiveInfinity, _, _) where string == positiveInfinity: return T.infinity @@ -72,7 +189,9 @@ extension DictionaryComponentDecoder { case .convertFromString, .throw: break } + } + switch component { case let number as T where number.isFinite: return number @@ -88,10 +207,22 @@ extension DictionaryComponentDecoder { break } - throw DecodingError.invalidComponent(component, of: T.self, at: codingPath) + // Numbers of other types are converted, which strings do not survive. + let number: T = try decodeConvertedNumber(from: component, at: codingPath) + + guard number.isFinite else { + let errorContext = DecodingError.Context( + codingPath: codingPath, + debugDescription: "Parsed dictionary number \(number) does not fit in \(T.self)." + ) + + throw DecodingError.dataCorrupted(errorContext) + } + + return number } - private func decodeDate(from component: Any?, at codingPath: [CodingKey]) throws -> Date { + private func decodeDate(from component: consuming Any?, at codingPath: consuming [CodingKey]) throws -> Date { switch options.dateDecodingStrategy { case .deferredToDate: return try decodeNonPrimitiveValue(from: component, at: codingPath) @@ -102,14 +233,10 @@ extension DictionaryComponentDecoder { case .millisecondsSince1970: return Date(timeIntervalSince1970: try decodePrimitiveValue(from: component, at: codingPath) / 1000.0) - case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") - } - + case let .iso8601(style): let formattedDate = try decodePrimitiveValue(of: String.self, from: component, at: codingPath) - guard let date = ISO8601DateFormatter().date(from: formattedDate) else { + guard let date = style.date(from: formattedDate) else { let errorContext = DecodingError.Context( codingPath: codingPath, debugDescription: "Expected date string to be ISO8601-formatted." @@ -139,7 +266,7 @@ extension DictionaryComponentDecoder { } } - private func decodeData(from component: Any?, at codingPath: [CodingKey]) throws -> Data { + private func decodeData(from component: consuming Any?, at codingPath: consuming [CodingKey]) throws -> Data { switch options.dataDecodingStrategy { case .deferredToData: return try decodeNonPrimitiveValue(from: component, at: codingPath) @@ -185,81 +312,121 @@ extension DictionaryComponentDecoder { // MARK: - + @inline(__always) internal func decodeNilComponent(from component: Any?) -> Bool { - component.isNil || component is NSNull + guard let component else { + return true + } + + // Unlike `is NSNull`, checking the type does not bridge Swift values to Objective-C objects. + return type(of: component) is NSNull.Type } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Bool { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int8 { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int16 { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int32 { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int64 { try decodePrimitiveValue(from: component, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inline(__always) + internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int128 { + try decodeWideInteger(from: component, at: codingPath) + } +#endif + + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt8 { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt16 { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt32 { try decodePrimitiveValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt64 { try decodePrimitiveValue(from: component, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inline(__always) + internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt128 { + try decodeWideInteger(from: component, at: codingPath) + } +#endif + + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Double { try decodeFloatingPointValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Float { try decodeFloatingPointValue(from: component, at: codingPath) } + @inline(__always) internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> String { try decodePrimitiveValue(from: component, at: codingPath) } internal func decodeComponentValue( of type: T.Type, - from component: Any?, - at codingPath: [CodingKey] + from component: consuming Any?, + at codingPath: consuming [CodingKey] ) throws -> T { - switch T.self { - case is Date.Type: + // The type is compared rather than cast, as a cast costs much more and is made for every value. + switch ObjectIdentifier(T.self) { + case ObjectIdentifier(Date.self): return try decodeDate(from: component, at: codingPath) as! T - case is Data.Type: + case ObjectIdentifier(Data.self): return try decodeData(from: component, at: codingPath) as! T - case is URL.Type: + case ObjectIdentifier(URL.self): return try decodeURL(from: component, at: codingPath) as! T + case ObjectIdentifier(Decimal.self): + return try decodeDecimal(from: component, at: codingPath) as! T + default: return try decodeNonPrimitiveValue(from: component, at: codingPath) } diff --git a/Sources/Decoder/DictionaryDecoder.swift b/Sources/Decoder/DictionaryDecoder.swift index f7e4173..6967c2c 100644 --- a/Sources/Decoder/DictionaryDecoder.swift +++ b/Sources/Decoder/DictionaryDecoder.swift @@ -17,6 +17,11 @@ public final class DictionaryDecoder: Sendable { set { optionsMutex.withLock { $0.dataDecodingStrategy = newValue } } } + public var decimalDecodingStrategy: DictionaryDecimalDecodingStrategy { + get { optionsMutex.withLock { $0.decimalDecodingStrategy } } + set { optionsMutex.withLock { $0.decimalDecodingStrategy = newValue } } + } + public var nonConformingFloatDecodingStrategy: DictionaryNonConformingFloatDecodingStrategy { get { optionsMutex.withLock { $0.nonConformingFloatDecodingStrategy } } set { optionsMutex.withLock { $0.nonConformingFloatDecodingStrategy = newValue } } @@ -37,6 +42,7 @@ public final class DictionaryDecoder: Sendable { public init( dateDecodingStrategy: DictionaryDateDecodingStrategy = .deferredToDate, dataDecodingStrategy: DictionaryDataDecodingStrategy = .base64, + decimalDecodingStrategy: DictionaryDecimalDecodingStrategy = .deferredToDecimal, nonConformingFloatDecodingStrategy: DictionaryNonConformingFloatDecodingStrategy = .throw, keyDecodingStrategy: DictionaryKeyDecodingStrategy = .useDefaultKeys, userInfo: [CodingUserInfoKey: Sendable] = [:] @@ -44,6 +50,7 @@ public final class DictionaryDecoder: Sendable { let options = DictionaryDecodingOptions( dateDecodingStrategy: dateDecodingStrategy, dataDecodingStrategy: dataDecodingStrategy, + decimalDecodingStrategy: decimalDecodingStrategy, nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, keyDecodingStrategy: keyDecodingStrategy ) @@ -54,41 +61,33 @@ public final class DictionaryDecoder: Sendable { // MARK: - Instance Methods - public func decode( - _ type: T.Type = T.self, - from dictionary: [String: Any] - ) throws -> T { + private func rootDecoder(for dictionary: [String: Any]) -> DictionarySingleValueDecodingContainer { let options = optionsMutex.withLock { $0 } - let decoder = DictionarySingleValueDecodingContainer( + return DictionarySingleValueDecodingContainer( component: dictionary, options: options, userInfo: userInfo, codingPath: [] ) + } - return try T(from: decoder) + public func decode( + _ type: T.Type = T.self, + from dictionary: [String: Any] + ) throws -> T { + try T(from: rootDecoder(for: dictionary)) } public func decode(from dictionary: [String: Any]) throws -> T { try decode(T.self, from: dictionary) } - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public func decode( _ type: T.Type = T.self, from dictionary: [String: Any], configuration: T.DecodingConfiguration ) throws -> T { - let options = optionsMutex.withLock { $0 } - - let decoder = DictionarySingleValueDecodingContainer( - component: dictionary, - options: options, - userInfo: userInfo, - codingPath: [] - ) - - return try T(from: decoder, configuration: configuration) + try T(from: rootDecoder(for: dictionary), configuration: configuration) } } diff --git a/Sources/Decoder/DictionaryKeyedDecodingContainer.swift b/Sources/Decoder/DictionaryKeyedDecodingContainer.swift index 68b89bf..daa0478 100644 --- a/Sources/Decoder/DictionaryKeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryKeyedDecodingContainer.swift @@ -1,6 +1,4 @@ -import Foundation - -internal class DictionaryKeyedDecodingContainer: +internal final class DictionaryKeyedDecodingContainer: KeyedDecodingContainerProtocol, DictionaryComponentDecoder { @@ -27,12 +25,12 @@ internal class DictionaryKeyedDecodingContainer: case .useDefaultKeys: self.components = components - case let.custom(closure): - let componentKeysAndValues = components.map { key, value in - (closure(codingPath.appending(AnyCodingKey(key))).stringValue, value) - } + case let .custom(closure): + let componentKeysAndValues = components + .sorted { $0.key < $1.key } + .map { key, value in (closure(codingPath.appending(AnyCodingKey(key))).stringValue, value) } - self.components = Dictionary(componentKeysAndValues) { $1 } + self.components = Dictionary(componentKeysAndValues) { first, _ in first } } self.options = options @@ -42,6 +40,7 @@ internal class DictionaryKeyedDecodingContainer: // MARK: - Instance Methods + @inline(__always) private func component(of type: T.Type = T.self, forKey key: Key) throws -> T { let anyComponent = components[key.stringValue] @@ -57,6 +56,18 @@ internal class DictionaryKeyedDecodingContainer: return component } + // Unlike the default implementation of `decodeIfPresent`, looks the key up once rather than in `contains`, + // `decodeNil` and `decode`. + @inline(__always) + private func decodeIfPresent(forKey key: Key, _ decode: (_ component: Any) throws -> T) rethrows -> T? { + guard let component = components[key.stringValue], !decodeNilComponent(from: component) else { + return nil + } + + return try decode(component) + } + + @inline(__always) private func superDecoder(forAnyKey key: CodingKey) throws -> Decoder { DictionarySingleValueDecodingContainer( component: components[key.stringValue], @@ -69,7 +80,7 @@ internal class DictionaryKeyedDecodingContainer: // MARK: - KeyedDecodingContainerProtocol internal func contains(_ key: Key) -> Bool { - components.contains { $0.key == key.stringValue } + components.keys.contains(key.stringValue) } internal func decodeNil(forKey key: Key) throws -> Bool { @@ -100,6 +111,13 @@ internal class DictionaryKeyedDecodingContainer: try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key)) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decode(_ type: Int128.Type, forKey key: Key) throws -> Int128 { + try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key)) + } +#endif + internal func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key)) } @@ -120,6 +138,13 @@ internal class DictionaryKeyedDecodingContainer: try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key)) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decode(_ type: UInt128.Type, forKey key: Key) throws -> UInt128 { + try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key)) + } +#endif + internal func decode(_ type: Double.Type, forKey key: Key) throws -> Double { try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key)) } @@ -136,6 +161,114 @@ internal class DictionaryKeyedDecodingContainer: try decodeComponentValue(of: type, from: try component(forKey: key), at: codingPath.appending(key)) } + internal func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: Int8.Type, forKey key: Key) throws -> Int8? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: Int16.Type, forKey key: Key) throws -> Int16? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: Int32.Type, forKey key: Key) throws -> Int32? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: Int64.Type, forKey key: Key) throws -> Int64? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decodeIfPresent(_ type: Int128.Type, forKey key: Key) throws -> Int128? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } +#endif + + internal func decodeIfPresent(_ type: UInt.Type, forKey key: Key) throws -> UInt? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: UInt8.Type, forKey key: Key) throws -> UInt8? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: UInt16.Type, forKey key: Key) throws -> UInt16? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: UInt32.Type, forKey key: Key) throws -> UInt32? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: UInt64.Type, forKey key: Key) throws -> UInt64? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decodeIfPresent(_ type: UInt128.Type, forKey key: Key) throws -> UInt128? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } +#endif + + internal func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(from: component, at: codingPath.appending(key)) + } + } + + internal func decodeIfPresent(_ type: T.Type, forKey key: Key) throws -> T? { + try decodeIfPresent(forKey: key) { component in + try decodeComponentValue(of: type, from: component, at: codingPath.appending(key)) + } + } + internal func nestedContainer( keyedBy keyType: NestedKey.Type, forKey key: Key diff --git a/Sources/Decoder/DictionarySingleValueDecodingContainer.swift b/Sources/Decoder/DictionarySingleValueDecodingContainer.swift index 6a4f08b..b1b825f 100644 --- a/Sources/Decoder/DictionarySingleValueDecodingContainer.swift +++ b/Sources/Decoder/DictionarySingleValueDecodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionarySingleValueDecodingContainer: Decoder, SingleValueDecodingContainer, @@ -56,6 +54,13 @@ internal final class DictionarySingleValueDecodingContainer: try decodeComponentValue(from: component, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decode(_ type: Int128.Type) throws -> Int128 { + try decodeComponentValue(from: component, at: codingPath) + } +#endif + internal func decode(_ type: UInt.Type) throws -> UInt { try decodeComponentValue(from: component, at: codingPath) } @@ -76,6 +81,13 @@ internal final class DictionarySingleValueDecodingContainer: try decodeComponentValue(from: component, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decode(_ type: UInt128.Type) throws -> UInt128 { + try decodeComponentValue(from: component, at: codingPath) + } +#endif + internal func decode(_ type: Double.Type) throws -> Double { try decodeComponentValue(from: component, at: codingPath) } diff --git a/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift b/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift index f7abb78..4ff1731 100644 --- a/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryUnkeyedDecodingContainer: UnkeyedDecodingContainer, DictionaryComponentDecoder { @@ -13,6 +11,7 @@ internal final class DictionaryUnkeyedDecodingContainer: internal private(set) var currentIndex = 0 + @inline(__always) internal var currentCodingPath: [CodingKey] { codingPath.appending(AnyCodingKey(currentIndex)) } @@ -41,7 +40,8 @@ internal final class DictionaryUnkeyedDecodingContainer: // MARK: - Instance Methods - private func popNextComponent() throws -> Any? { + @inline(__always) + private func peekNextComponent() throws -> Any? { guard currentIndex < components.count else { let errorContext = DecodingError.Context( codingPath: currentCodingPath, @@ -51,95 +51,127 @@ internal final class DictionaryUnkeyedDecodingContainer: throw DecodingError.valueNotFound(Any.self, errorContext) } - defer { - currentIndex += 1 - } - return components[currentIndex] } + @inline(__always) + private func decodeNextComponent( + _ decodeComponent: (_ component: consuming Any?, _ codingPath: consuming [CodingKey]) throws -> T + ) throws -> T { + let value = try decodeComponent(try peekNextComponent(), currentCodingPath) + + currentIndex += 1 + + return value + } + + @inline(__always) + private func superDecoder(for component: consuming Any?, at codingPath: consuming [CodingKey]) -> Decoder { + DictionarySingleValueDecodingContainer( + component: component, + options: options, + userInfo: userInfo, + codingPath: codingPath + ) + } + // MARK: - UnkeyedDecodingContainer internal func decodeNil() throws -> Bool { - decodeNilComponent(from: try popNextComponent()) + guard decodeNilComponent(from: try peekNextComponent()) else { + return false + } + + currentIndex += 1 + + return true } internal func decode(_ type: Bool.Type) throws -> Bool { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int.Type) throws -> Int { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int8.Type) throws -> Int8 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int16.Type) throws -> Int16 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int32.Type) throws -> Int32 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int64.Type) throws -> Int64 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } + } + +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decode(_ type: Int128.Type) throws -> Int128 { + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } +#endif internal func decode(_ type: UInt.Type) throws -> UInt { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt8.Type) throws -> UInt8 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt16.Type) throws -> UInt16 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt32.Type) throws -> UInt32 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt64.Type) throws -> UInt64 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func decode(_ type: UInt128.Type) throws -> UInt128 { + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } + } +#endif + internal func decode(_ type: Double.Type) throws -> Double { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Float.Type) throws -> Float { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: String.Type) throws -> String { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: T.Type) throws -> T { - try decodeComponentValue(of: type, from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(of: type, from: $0, at: $1) } } internal func nestedContainer( keyedBy keyType: NestedKey.Type ) throws -> KeyedDecodingContainer { - try superDecoder().container(keyedBy: keyType) + try decodeNextComponent { try superDecoder(for: $0, at: $1).container(keyedBy: keyType) } } internal func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { - try superDecoder().unkeyedContainer() + try decodeNextComponent { try superDecoder(for: $0, at: $1).unkeyedContainer() } } internal func superDecoder() throws -> Decoder { - DictionarySingleValueDecodingContainer( - component: try popNextComponent(), - options: options, - userInfo: userInfo, - codingPath: currentCodingPath - ) + try decodeNextComponent { superDecoder(for: $0, at: $1) } } } diff --git a/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift index 11b36fd..655dc8a 100644 --- a/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift @@ -14,13 +14,19 @@ public enum DictionaryDateDecodingStrategy: Sendable { /// The strategy that decodes dates in terms of milliseconds since midnight UTC on January 1st, 1970. case millisecondsSince1970 - /// The strategy that formats dates according to the ISO 8601 standard. - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - case iso8601 + /// The strategy that formats dates according to the ISO 8601 standard in the given style. + case iso8601(style: DictionaryISO8601Style) /// The strategy that defers formatting settings to a supplied date formatter. case formatted(DateFormatter) /// The strategy that formats custom dates by calling a user-defined function. case custom(@Sendable (_ decoder: Decoder) throws -> Date) + + // MARK: - Type Properties + + /// The strategy that formats dates according to the ISO 8601 standard in the style of `ISO8601DateFormatter`. + public static var iso8601: Self { + .iso8601(style: .dateFormatter) + } } diff --git a/Sources/Decoder/Options/DictionaryDecimalDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryDecimalDecodingStrategy.swift new file mode 100644 index 0000000..c41bb70 --- /dev/null +++ b/Sources/Decoder/Options/DictionaryDecimalDecodingStrategy.swift @@ -0,0 +1,21 @@ +/// The strategies for decoding decimals, which can be combined to accept both representations. +public struct DictionaryDecimalDecodingStrategy: OptionSet, Sendable { + + // MARK: - Type Properties + + /// The strategy that decodes decimals from the keyed representation of the decimal type itself. + public static let deferredToDecimal = Self(rawValue: 1 << 0) + + /// The strategy that decodes decimals from numbers, such as `Decimal`, `NSDecimalNumber` or `Double`. + public static let number = Self(rawValue: 1 << 1) + + // MARK: - Instance Properties + + public let rawValue: Int + + // MARK: - Initializers + + public init(rawValue: Int) { + self.rawValue = rawValue + } +} diff --git a/Sources/Decoder/Options/DictionaryDecodingOptions.swift b/Sources/Decoder/Options/DictionaryDecodingOptions.swift index fa2a51f..747fa35 100644 --- a/Sources/Decoder/Options/DictionaryDecodingOptions.swift +++ b/Sources/Decoder/Options/DictionaryDecodingOptions.swift @@ -1,11 +1,10 @@ -import Foundation - internal struct DictionaryDecodingOptions { // MARK: - Instance Properties internal var dateDecodingStrategy: DictionaryDateDecodingStrategy internal var dataDecodingStrategy: DictionaryDataDecodingStrategy + internal var decimalDecodingStrategy: DictionaryDecimalDecodingStrategy internal var nonConformingFloatDecodingStrategy: DictionaryNonConformingFloatDecodingStrategy internal var keyDecodingStrategy: DictionaryKeyDecodingStrategy } diff --git a/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift index b036eef..f2e1dc1 100644 --- a/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - /// The values that determine how to decode a type’s coding keys from Dictionary keys. public enum DictionaryKeyDecodingStrategy: Sendable { @@ -9,5 +7,8 @@ public enum DictionaryKeyDecodingStrategy: Sendable { case useDefaultKeys /// A key decoding strategy defined by the closure you supply. + /// + /// If several keys are converted to the same key, + /// the value of the lexicographically smallest original key is used. case custom(@Sendable (_ codingPath: [CodingKey]) -> CodingKey) } diff --git a/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift index a3dd669..3c2a59c 100644 --- a/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - /// The strategies for encoding nonconforming floating-point numbers, /// also known as IEEE 754 exceptional values. public enum DictionaryNonConformingFloatDecodingStrategy: Sendable { diff --git a/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift b/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift index 9a4410f..8e613b2 100644 --- a/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift +++ b/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryAnyKeyedEncodingContainer: DictionaryComponentContainer { // MARK: - Instance Properties @@ -24,6 +22,7 @@ internal final class DictionaryAnyKeyedEncodingContainer: DictionaryComponentCon // MARK: - Instance Methods + @inline(__always) private func encodeKey(_ key: Key) -> String { switch options.keyEncodingStrategy { case .useDefaultKeys: @@ -36,7 +35,8 @@ internal final class DictionaryAnyKeyedEncodingContainer: DictionaryComponentCon // MARK: - - internal func collectComponent(_ component: DictionaryComponent, forKey key: Key) { + @inline(__always) + internal func collectComponent(_ component: consuming DictionaryComponent, forKey key: Key) { components[encodeKey(key)] = component } diff --git a/Sources/Encoder/DictionaryComponent.swift b/Sources/Encoder/DictionaryComponent.swift index 138d310..5420272 100644 --- a/Sources/Encoder/DictionaryComponent.swift +++ b/Sources/Encoder/DictionaryComponent.swift @@ -1,6 +1,4 @@ -import Foundation - -internal indirect enum DictionaryComponent { +internal enum DictionaryComponent { // MARK: - Enumeration Cases diff --git a/Sources/Encoder/DictionaryComponentContainer.swift b/Sources/Encoder/DictionaryComponentContainer.swift index 5fea329..c373861 100644 --- a/Sources/Encoder/DictionaryComponentContainer.swift +++ b/Sources/Encoder/DictionaryComponentContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal protocol DictionaryComponentContainer { // MARK: - Instance Methods diff --git a/Sources/Encoder/DictionaryComponentEncoder.swift b/Sources/Encoder/DictionaryComponentEncoder.swift index a6f4627..e2b0592 100644 --- a/Sources/Encoder/DictionaryComponentEncoder.swift +++ b/Sources/Encoder/DictionaryComponentEncoder.swift @@ -12,8 +12,9 @@ extension DictionaryComponentEncoder { // MARK: - Instance Methods + @inline(__always) private func encodePrimitiveValue( - _ value: Any?, + _ value: consuming Any?, at codingPath: [CodingKey] ) -> DictionaryComponent { .value(value) @@ -21,7 +22,7 @@ extension DictionaryComponentEncoder { private func encodeNonPrimitiveValue( _ value: T, - at codingPath: [CodingKey] + at codingPath: consuming [CodingKey] ) throws -> DictionaryComponent { let encoder = DictionarySingleValueEncodingContainer( options: options, @@ -36,7 +37,7 @@ extension DictionaryComponentEncoder { private func encodeCustomizedValue( _ value: T, - at codingPath: [CodingKey], + at codingPath: consuming [CodingKey], closure: (_ value: T, _ encoder: Encoder) throws -> Void ) throws -> DictionaryComponent { let encoder = DictionarySingleValueEncodingContainer( @@ -60,7 +61,7 @@ extension DictionaryComponentEncoder { } } - private func encodeDate(_ date: Date, at codingPath: [CodingKey]) throws -> DictionaryComponent { + private func encodeDate(_ date: Date, at codingPath: consuming [CodingKey]) throws -> DictionaryComponent { switch options.dateEncodingStrategy { case .deferredToDate: return try encodeNonPrimitiveValue(date, at: codingPath) @@ -71,18 +72,8 @@ extension DictionaryComponentEncoder { case .secondsSince1970: return encodePrimitiveValue(date.timeIntervalSince1970, at: codingPath) - case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") - } - - let formattedDate = ISO8601DateFormatter.string( - from: date, - timeZone: .iso8601TimeZone, - formatOptions: .withInternetDateTime - ) - - return encodePrimitiveValue(formattedDate, at: codingPath) + case let .iso8601(style): + return encodePrimitiveValue(style.string(from: date), at: codingPath) case let .formatted(dateFormatter): return encodePrimitiveValue(dateFormatter.string(from: date), at: codingPath) @@ -92,7 +83,7 @@ extension DictionaryComponentEncoder { } } - private func encodeData(_ data: Data, at codingPath: [CodingKey]) throws -> DictionaryComponent { + private func encodeData(_ data: Data, at codingPath: consuming [CodingKey]) throws -> DictionaryComponent { switch options.dataEncodingStrategy { case .deferredToData: return try encodeNonPrimitiveValue(data, at: codingPath) @@ -108,6 +99,16 @@ extension DictionaryComponentEncoder { } } + private func encodeDecimal(_ decimal: Decimal, at codingPath: [CodingKey]) throws -> DictionaryComponent { + switch options.decimalEncodingStrategy { + case .deferredToDecimal: + return try encodeNonPrimitiveValue(decimal, at: codingPath) + + case .number: + return encodePrimitiveValue(decimal, at: codingPath) + } + } + private func encodeFloatingPoint( _ value: T, at codingPath: [CodingKey] @@ -137,79 +138,114 @@ extension DictionaryComponentEncoder { // MARK: - + @inline(__always) internal func encodeNilComponent(at codingPath: [CodingKey]) -> DictionaryComponent { encodeNil(at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: Bool, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: Int, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: Int8, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: Int16, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: Int32, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: Int64, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inline(__always) + internal func encodeComponentValue(_ value: Int128, at codingPath: [CodingKey]) -> DictionaryComponent { + encodePrimitiveValue(value, at: codingPath) + } +#endif + + @inline(__always) internal func encodeComponentValue(_ value: UInt, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: UInt8, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: UInt16, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: UInt32, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: UInt64, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + @inline(__always) + internal func encodeComponentValue(_ value: UInt128, at codingPath: [CodingKey]) -> DictionaryComponent { + encodePrimitiveValue(value, at: codingPath) + } +#endif + + @inline(__always) internal func encodeComponentValue(_ value: Double, at codingPath: [CodingKey]) throws -> DictionaryComponent { try encodeFloatingPoint(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: Float, at codingPath: [CodingKey]) throws -> DictionaryComponent { try encodeFloatingPoint(value, at: codingPath) } + @inline(__always) internal func encodeComponentValue(_ value: String, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } internal func encodeComponentValue( _ value: T, - at codingPath: [CodingKey] + at codingPath: consuming [CodingKey] ) throws -> DictionaryComponent { - switch value { - case let date as Date: - return try encodeDate(date, at: codingPath) + // The type is compared rather than the value cast, as a cast costs much more and is made for every value. + switch ObjectIdentifier(T.self) { + case ObjectIdentifier(Date.self): + return try encodeDate(value as! Date, at: codingPath) - case let data as Data: - return try encodeData(data, at: codingPath) + case ObjectIdentifier(Data.self): + return try encodeData(value as! Data, at: codingPath) - case let url as URL: - return try encodeURL(url, at: codingPath) + case ObjectIdentifier(URL.self): + return try encodeURL(value as! URL, at: codingPath) + + case ObjectIdentifier(Decimal.self): + return try encodeDecimal(value as! Decimal, at: codingPath) default: return try encodeNonPrimitiveValue(value, at: codingPath) @@ -217,13 +253,6 @@ extension DictionaryComponentEncoder { } } -extension TimeZone { - - // MARK: - Type Properties - - fileprivate static let iso8601TimeZone = TimeZone(secondsFromGMT: 0)! -} - extension EncodingError { // MARK: - Type Methods @@ -250,6 +279,6 @@ extension EncodingError { Use DictionaryNonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded. """ - return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription)) + return .invalidValue(value, Context(codingPath: codingPath, debugDescription: debugDescription)) } } diff --git a/Sources/Encoder/DictionaryEncoder.swift b/Sources/Encoder/DictionaryEncoder.swift index d424b99..6ad9668 100644 --- a/Sources/Encoder/DictionaryEncoder.swift +++ b/Sources/Encoder/DictionaryEncoder.swift @@ -17,6 +17,11 @@ public final class DictionaryEncoder: Sendable { set { optionsMutex.withLock { $0.dataEncodingStrategy = newValue } } } + public var decimalEncodingStrategy: DictionaryDecimalEncodingStrategy { + get { optionsMutex.withLock { $0.decimalEncodingStrategy } } + set { optionsMutex.withLock { $0.decimalEncodingStrategy = newValue } } + } + public var nonConformingFloatEncodingStrategy: DictionaryNonConformingFloatEncodingStrategy { get { optionsMutex.withLock { $0.nonConformingFloatEncodingStrategy } } set { optionsMutex.withLock { $0.nonConformingFloatEncodingStrategy = newValue } } @@ -42,6 +47,7 @@ public final class DictionaryEncoder: Sendable { public init( dateEncodingStrategy: DictionaryDateEncodingStrategy = .deferredToDate, dataEncodingStrategy: DictionaryDataEncodingStrategy = .base64, + decimalEncodingStrategy: DictionaryDecimalEncodingStrategy = .deferredToDecimal, nonConformingFloatEncodingStrategy: DictionaryNonConformingFloatEncodingStrategy = .throw, nilEncodingStrategy: DictionaryNilEncodingStrategy = .useNil, keyEncodingStrategy: DictionaryKeyEncodingStrategy = .useDefaultKeys, @@ -50,6 +56,7 @@ public final class DictionaryEncoder: Sendable { let options = DictionaryEncodingOptions( dateEncodingStrategy: dateEncodingStrategy, dataEncodingStrategy: dataEncodingStrategy, + decimalEncodingStrategy: decimalEncodingStrategy, nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy, nilEncodingStrategy: nilEncodingStrategy, keyEncodingStrategy: keyEncodingStrategy @@ -61,7 +68,10 @@ public final class DictionaryEncoder: Sendable { // MARK: - Instance Methods - public func encode(_ value: T) throws -> [String: Sendable] { + private func encodeRootValue( + _ value: T, + encoding: (_ encoder: Encoder) throws -> Void + ) throws -> [String: Sendable] { let options = optionsMutex.withLock { $0 } let encoder = DictionarySingleValueEncodingContainer( @@ -70,7 +80,7 @@ public final class DictionaryEncoder: Sendable { codingPath: [] ) - try value.encode(to: encoder) + try encoding(encoder) guard let dictionary = encoder.resolveValue() as? [String: Sendable] else { let errorContext = EncodingError.Context( @@ -84,30 +94,18 @@ public final class DictionaryEncoder: Sendable { return dictionary } - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) + public func encode(_ value: T) throws -> [String: Sendable] { + try encodeRootValue(value) { encoder in + try value.encode(to: encoder) + } + } + public func encode( _ value: T, configuration: T.EncodingConfiguration ) throws -> [String: Sendable] { - let options = optionsMutex.withLock { $0 } - - let encoder = DictionarySingleValueEncodingContainer( - options: options, - userInfo: userInfo, - codingPath: [] - ) - - try value.encode(to: encoder, configuration: configuration) - - guard let dictionary = encoder.resolveValue() as? [String: Sendable] else { - let errorContext = EncodingError.Context( - codingPath: [], - debugDescription: "Root component cannot be encoded in Dictionary" - ) - - throw EncodingError.invalidValue(value, errorContext) + try encodeRootValue(value) { encoder in + try value.encode(to: encoder, configuration: configuration) } - - return dictionary } } diff --git a/Sources/Encoder/DictionaryKeyedEncodingContainer.swift b/Sources/Encoder/DictionaryKeyedEncodingContainer.swift index df841d6..7e0dbe1 100644 --- a/Sources/Encoder/DictionaryKeyedEncodingContainer.swift +++ b/Sources/Encoder/DictionaryKeyedEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryKeyedEncodingContainer: KeyedEncodingContainerProtocol, DictionaryComponentEncoder { @@ -56,6 +54,13 @@ internal final class DictionaryKeyedEncodingContainer: container.collectComponent(encodeComponentValue(value, at: codingPath.appending(key)), forKey: key) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encode(_ value: Int128, forKey key: Key) throws { + container.collectComponent(encodeComponentValue(value, at: codingPath.appending(key)), forKey: key) + } +#endif + internal func encode(_ value: UInt, forKey key: Key) throws { container.collectComponent(encodeComponentValue(value, at: codingPath.appending(key)), forKey: key) } @@ -76,6 +81,13 @@ internal final class DictionaryKeyedEncodingContainer: container.collectComponent(encodeComponentValue(value, at: codingPath.appending(key)), forKey: key) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encode(_ value: UInt128, forKey key: Key) throws { + container.collectComponent(encodeComponentValue(value, at: codingPath.appending(key)), forKey: key) + } +#endif + internal func encode(_ value: Double, forKey key: Key) throws { container.collectComponent(try encodeComponentValue(value, at: codingPath.appending(key)), forKey: key) } diff --git a/Sources/Encoder/DictionarySingleValueEncodingContainer.swift b/Sources/Encoder/DictionarySingleValueEncodingContainer.swift index d9d6741..1500770 100644 --- a/Sources/Encoder/DictionarySingleValueEncodingContainer.swift +++ b/Sources/Encoder/DictionarySingleValueEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionarySingleValueEncodingContainer: Encoder, SingleValueEncodingContainer, @@ -28,7 +26,8 @@ internal final class DictionarySingleValueEncodingContainer: // MARK: - Instance Methods - private func collectComponent(_ component: DictionaryComponent, for value: Any?) throws { + @inline(__always) + private func collectComponent(_ component: consuming DictionaryComponent, for value: Any?) throws { guard self.component == nil else { let errorContext = EncodingError.Context( codingPath: codingPath, @@ -71,6 +70,13 @@ internal final class DictionarySingleValueEncodingContainer: try collectComponent(encodeComponentValue(value, at: codingPath), for: value) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encode(_ value: Int128) throws { + try collectComponent(encodeComponentValue(value, at: codingPath), for: value) + } +#endif + internal func encode(_ value: UInt) throws { try collectComponent(encodeComponentValue(value, at: codingPath), for: value) } @@ -91,6 +97,13 @@ internal final class DictionarySingleValueEncodingContainer: try collectComponent(encodeComponentValue(value, at: codingPath), for: value) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encode(_ value: UInt128) throws { + try collectComponent(encodeComponentValue(value, at: codingPath), for: value) + } +#endif + internal func encode(_ value: Double) throws { try collectComponent(try encodeComponentValue(value, at: codingPath), for: value) } diff --git a/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift b/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift index 842b89e..76bfb5e 100644 --- a/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift +++ b/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryUnkeyedEncodingContainer: UnkeyedEncodingContainer, DictionaryComponentContainer, @@ -13,6 +11,7 @@ internal final class DictionaryUnkeyedEncodingContainer: internal let userInfo: [CodingUserInfoKey: Any] internal let codingPath: [CodingKey] + @inline(__always) internal var currentCodingPath: [CodingKey] { codingPath.appending(AnyCodingKey(count)) } @@ -35,7 +34,15 @@ internal final class DictionaryUnkeyedEncodingContainer: // MARK: - Instance Methods - private func collectComponent(_ component: DictionaryComponent) { + @inline(__always) + private func collectComponent(_ component: consuming DictionaryComponent) { + // Most unkeyed containers of compact encodings hold a couple of elements, so room for two is reserved + // up front. It saves a reallocation for every container of two and more elements, which grow as usual, + // and costs memory only for containers of one element. + if components.isEmpty { + components.reserveCapacity(2) + } + components.append(component) } @@ -69,6 +76,13 @@ internal final class DictionaryUnkeyedEncodingContainer: collectComponent(encodeComponentValue(value, at: currentCodingPath)) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encode(_ value: Int128) throws { + collectComponent(encodeComponentValue(value, at: currentCodingPath)) + } +#endif + internal func encode(_ value: UInt) throws { collectComponent(encodeComponentValue(value, at: currentCodingPath)) } @@ -89,6 +103,13 @@ internal final class DictionaryUnkeyedEncodingContainer: collectComponent(encodeComponentValue(value, at: currentCodingPath)) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encode(_ value: UInt128) throws { + collectComponent(encodeComponentValue(value, at: currentCodingPath)) + } +#endif + internal func encode(_ value: Double) throws { collectComponent(try encodeComponentValue(value, at: currentCodingPath)) } @@ -148,11 +169,10 @@ internal final class DictionaryUnkeyedEncodingContainer: // MARK: - DictionaryComponentContainer internal func resolveValue() -> Any? { - let values = components - .lazy - .map { $0.resolveValue() } - .compactMap { $0 ?? $0 as Any } + components.map { component in + let value = component.resolveValue() - return Array(values) + return value ?? value as Any + } } } diff --git a/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift index 92c06cf..a092791 100644 --- a/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift @@ -9,9 +9,14 @@ public enum DictionaryDateEncodingStrategy: Sendable { case millisecondsSince1970 case secondsSince1970 - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - case iso8601 + case iso8601(style: DictionaryISO8601Style) case formatted(DateFormatter) case custom(@Sendable (_ date: Date, _ encoder: Encoder) throws -> Void) + + // MARK: - Type Properties + + public static var iso8601: Self { + .iso8601(style: .dateFormatter) + } } diff --git a/Sources/Encoder/Options/DictionaryDecimalEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryDecimalEncodingStrategy.swift new file mode 100644 index 0000000..8c6883c --- /dev/null +++ b/Sources/Encoder/Options/DictionaryDecimalEncodingStrategy.swift @@ -0,0 +1,7 @@ +public enum DictionaryDecimalEncodingStrategy: Sendable { + + // MARK: - Enumeration Cases + + case deferredToDecimal + case number +} diff --git a/Sources/Encoder/Options/DictionaryEncodingOptions.swift b/Sources/Encoder/Options/DictionaryEncodingOptions.swift index 6878ada..3351d84 100644 --- a/Sources/Encoder/Options/DictionaryEncodingOptions.swift +++ b/Sources/Encoder/Options/DictionaryEncodingOptions.swift @@ -1,11 +1,10 @@ -import Foundation - internal struct DictionaryEncodingOptions { // MARK: - Instance Properties internal var dateEncodingStrategy: DictionaryDateEncodingStrategy internal var dataEncodingStrategy: DictionaryDataEncodingStrategy + internal var decimalEncodingStrategy: DictionaryDecimalEncodingStrategy internal var nonConformingFloatEncodingStrategy: DictionaryNonConformingFloatEncodingStrategy internal var nilEncodingStrategy: DictionaryNilEncodingStrategy internal var keyEncodingStrategy: DictionaryKeyEncodingStrategy diff --git a/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift index da26126..deb3215 100644 --- a/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - public enum DictionaryKeyEncodingStrategy: Sendable { // MARK: - Enumeration Cases diff --git a/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift index 37abb2a..b321d17 100644 --- a/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - public enum DictionaryNilEncodingStrategy: Sendable { // MARK: - Enumeration Cases diff --git a/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift index 63cffcd..bff87ba 100644 --- a/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - public enum DictionaryNonConformingFloatEncodingStrategy: Sendable { // MARK: - Enumeration Cases diff --git a/Sources/Tools/AnyCodingKey.swift b/Sources/Tools/AnyCodingKey.swift index b883a4e..1aa1f95 100644 --- a/Sources/Tools/AnyCodingKey.swift +++ b/Sources/Tools/AnyCodingKey.swift @@ -1,5 +1,3 @@ -import Foundation - internal struct AnyCodingKey: CodingKey { // MARK: - Type Properties diff --git a/Sources/Tools/DictionaryISO8601Style.swift b/Sources/Tools/DictionaryISO8601Style.swift new file mode 100644 index 0000000..dee232c --- /dev/null +++ b/Sources/Tools/DictionaryISO8601Style.swift @@ -0,0 +1,105 @@ +import Foundation + +/// The styles of dates formatted according to the ISO 8601 standard. +public enum DictionaryISO8601Style: Sendable { + + // MARK: - Enumeration Cases + + /// The style of `ISO8601DateFormatter`, the default one. + case dateFormatter + + /// The style of `Date.ISO8601FormatStyle`, as in `JSONEncoder` and `JSONDecoder`: + /// fractions of a second are dropped when encoding and accepted when decoding. + case formatStyle +} + +extension DictionaryISO8601Style { + + // MARK: - Instance Methods + + internal func string(from date: Date) -> String { + switch self { + case .dateFormatter: + // `ISO8601DateFormatter` rounds dates to milliseconds before dropping fractions of a second. + let milliseconds = (date.timeIntervalSince1970 * 1000 + 0.5).rounded(.down) + let seconds = (milliseconds / 1000).rounded(.down) + + return Date.ISO8601FormatStyle.internetDateTime.format(Date(timeIntervalSince1970: seconds)) + + case .formatStyle: + return Date.ISO8601FormatStyle.internetDateTime.format(date) + } + } + + internal func date(from string: String) -> Date? { + switch self { + case .dateFormatter: + // Both parse dates with whole seconds alike, but the format style is much faster. + if string.isInternetDateTimeWithWholeSeconds, + let date = try? Date.ISO8601FormatStyle.internetDateTime.parse(string) { + return date + } + + return ISO8601DateFormatter().date(from: string) + + case .formatStyle: + return try? Date.ISO8601FormatStyle.internetDateTime.parse(string) + } + } +} + +extension Date.ISO8601FormatStyle { + + // MARK: - Type Properties + + // Creating a style costs nearly as much as formatting a date with it, so a single one is shared. + fileprivate static let internetDateTime = Self() +} + +extension String { + + // MARK: - Type Properties + + // The ranges of the year, month, day, hour, minute, second and time zone offset in hours and minutes. + private static let internetDateTimeFieldRanges = [1...9999, 1...12, 1...31, 0...23, 0...59, 0...59, 0...23, 0...59] + + // MARK: - Instance Properties + + // Whether the string is like `2001-02-03T04:05:06Z` or `2001-02-03T04:05:06+07:00` with fields in their ranges, + // unlike strings with fractions of a second or a leap second, for example, + // which only `Date.ISO8601FormatStyle` accepts. + fileprivate var isInternetDateTimeWithWholeSeconds: Bool { + let template = utf8.count == 20 ? "0000-00-00T00:00:00Z" : "0000-00-00T00:00:00+00:00" + + guard utf8.count == template.utf8.count else { + return false + } + + var field = 0 + var value = 0 + + for (character, templateCharacter) in zip(utf8, template.utf8) { + if templateCharacter == UInt8(ascii: "0") { + guard (UInt8(ascii: "0")...UInt8(ascii: "9")).contains(character) else { + return false + } + + value = value * 10 + Int(character - UInt8(ascii: "0")) + continue + } + + let isSeparator = character == templateCharacter + || templateCharacter == UInt8(ascii: "+") && character == UInt8(ascii: "-") + + guard isSeparator, Self.internetDateTimeFieldRanges[field].contains(value) else { + return false + } + + field += 1 + value = 0 + } + + // A time zone offset ends with its minutes rather than a separator. + return field == 6 || Self.internetDateTimeFieldRanges[field].contains(value) + } +} diff --git a/Sources/Tools/Mutex/Mutex.swift b/Sources/Tools/Mutex/Mutex.swift index 1f3bcb2..9413be2 100644 --- a/Sources/Tools/Mutex/Mutex.swift +++ b/Sources/Tools/Mutex/Mutex.swift @@ -1,5 +1,3 @@ -import Foundation - public struct Mutex: @unchecked Sendable { private let storage: MutexStorage diff --git a/Sources/Tools/Mutex/MutexStorage.swift b/Sources/Tools/Mutex/MutexStorage.swift index a884dad..0602e2d 100644 --- a/Sources/Tools/Mutex/MutexStorage.swift +++ b/Sources/Tools/Mutex/MutexStorage.swift @@ -1,5 +1,3 @@ -import Foundation - import struct os.os_unfair_lock_t import struct os.os_unfair_lock diff --git a/Sources/Tools/Optional+Extensions.swift b/Sources/Tools/Optional+Extensions.swift deleted file mode 100644 index 4344e37..0000000 --- a/Sources/Tools/Optional+Extensions.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -extension Optional { - - // MARK: - Instance Properties - - internal var isNil: Bool { - self == nil - } -} - -extension Optional where Wrapped: Collection { - - // MARK: - Instance Properties - - internal var isEmptyOrNil: Bool { - self?.isEmpty ?? true - } -} diff --git a/Sources/Tools/RangeReplaceableCollection+Extensions.swift b/Sources/Tools/RangeReplaceableCollection+Extensions.swift index a68dfd6..62977db 100644 --- a/Sources/Tools/RangeReplaceableCollection+Extensions.swift +++ b/Sources/Tools/RangeReplaceableCollection+Extensions.swift @@ -1,14 +1,13 @@ -import Foundation - extension RangeReplaceableCollection { // MARK: - Instance Methods - internal func appending(contentsOf collection: T) -> Self where Self.Element == T.Element { - self + collection - } - + @inline(__always) internal func appending(_ element: Element) -> Self { - appending(contentsOf: [element]) + var collection = self + + collection.append(element) + + return collection } } diff --git a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift index fffb9ba..a02affb 100644 --- a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift +++ b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift @@ -48,6 +48,15 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) } + func testThatDecoderSucceedsWhenDecodingCollidingKeysUsingCustomFunctionForKeys() { + decoder.keyDecodingStrategy = .custom { _ in AnyCodingKey("foobar") } + + let letters = "abcdefghijklmnopqrstuvwxyz".map(String.init) + let dictionary = Dictionary(uniqueKeysWithValues: letters.enumerated().map { ($1, $0) }) + + assertDecoderSucceeds(decoding: ["foobar": 0], from: dictionary) + } + // MARK: - func testThatDecoderSucceedsWhenDecodingDate() { @@ -90,7 +99,6 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti assertDecoderSucceeds(decoding: [String: Date].self, from: dictionary) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) func testThatDecoderSucceedsWhenDecodingDateFromISO8601Format() { decoder.dateDecodingStrategy = .iso8601 @@ -99,7 +107,6 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti assertDecoderSucceeds(decoding: [String: Date].self, from: dictionary) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) func testThatDecoderFailsWhenDecodingInvalidDateFromISO8601Format() { decoder.dateDecodingStrategy = .iso8601 @@ -116,6 +123,70 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti } } + func testThatDecoderSucceedsWhenDecodingDateWithTimeZoneFromISO8601Format() { + decoder.dateDecodingStrategy = .iso8601 + + let dictionary = [ + "foo": "2001-01-01T01:02:03+01:00", + "bar": "2001-01-01T01:02:03 +01:00" + ] + + let value = [ + "foo": Date(timeIntervalSinceReferenceDate: 123), + "bar": Date(timeIntervalSinceReferenceDate: 123) + ] + + assertDecoderSucceeds(decoding: value, from: dictionary) + } + + func testThatDecoderFailsWhenDecodingDateWithFractionalSecondsFromISO8601Format() { + decoder.dateDecodingStrategy = .iso8601 + + let dictionary = ["foobar": "2001-01-01T00:02:03.5Z"] + + assertDecoderFails(decoding: [String: Date].self, from: dictionary) { error in + switch error { + case DecodingError.dataCorrupted: + return true + + default: + return false + } + } + } + + func testThatDecoderSucceedsWhenDecodingDateFromISO8601FormatStyle() { + decoder.dateDecodingStrategy = .iso8601(style: .formatStyle) + + let dictionary = [ + "foo": "2001-01-01T00:02:03Z", + "bar": "2001-01-01T00:02:03.5Z" + ] + + let value = [ + "foo": Date(timeIntervalSinceReferenceDate: 123), + "bar": Date(timeIntervalSinceReferenceDate: 123.5) + ] + + assertDecoderSucceeds(decoding: value, from: dictionary) + } + + func testThatDecoderFailsWhenDecodingInvalidDateFromISO8601FormatStyle() { + decoder.dateDecodingStrategy = .iso8601(style: .formatStyle) + + let dictionary = ["foobar": "2001-01-01T00:02:03 +01:00"] + + assertDecoderFails(decoding: [String: Date].self, from: dictionary) { error in + switch error { + case DecodingError.dataCorrupted: + return true + + default: + return false + } + } + } + func testThatDecoderSucceedsWhenDecodingDateUsingFormatter() { let dateFormatter = DateFormatter() @@ -292,6 +363,87 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti // MARK: - + func testThatDecoderSucceedsWhenDecodingDecimal() throws { + let value = [ + "foo": Decimal(string: "1.5")!, + "bar": Decimal(string: "-2.25")! + ] + + let dictionary = try DictionaryEncoder().encode(value) + + assertDecoderSucceeds(decoding: value, from: dictionary) + } + + func testThatDecoderFailsWhenDecodingInvalidDecimal() { + let dictionary = ["foobar": 1.5] + + assertDecoderFails(decoding: [String: Decimal].self, from: dictionary) { error in + switch error { + case DecodingError.typeMismatch: + return true + + default: + return false + } + } + } + + func testThatDecoderSucceedsWhenDecodingDecimalFromNumber() { + decoder.decimalDecodingStrategy = .number + + let dictionary: [String: Any] = [ + "foo": Decimal(string: "1.5")!, + "bar": NSDecimalNumber(string: "-2.25"), + "baz": 0.1, + "qux": 3 + ] + + let value = [ + "foo": Decimal(string: "1.5")!, + "bar": Decimal(string: "-2.25")!, + "baz": Decimal(string: "0.1")!, + "qux": Decimal(3) + ] + + assertDecoderSucceeds(decoding: value, from: dictionary) + } + + func testThatDecoderFailsWhenDecodingInvalidDecimalFromNumber() throws { + decoder.decimalDecodingStrategy = .number + + let dictionaries: [[String: Any]] = [ + ["foobar": true], + try DictionaryEncoder().encode(["foobar": Decimal(string: "1.5")!]) + ] + + for dictionary in dictionaries { + assertDecoderFails(decoding: [String: Decimal].self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, _) where type is Decimal.Type: + return true + + default: + return false + } + } + } + } + + func testThatDecoderSucceedsWhenDecodingDecimalFromNumberOrKeyedRepresentation() throws { + decoder.decimalDecodingStrategy = [.deferredToDecimal, .number] + + var dictionary = try DictionaryEncoder().encode(["foo": Decimal(string: "1.5")!]) + + dictionary["bar"] = 0.25 + + let value = [ + "foo": Decimal(string: "1.5")!, + "bar": Decimal(string: "0.25")! + ] + + assertDecoderSucceeds(decoding: value, from: dictionary) + } + func testThatDecoderFailsWhenDecodingNonConformingFloat() { decoder.nonConformingFloatDecodingStrategy = .throw diff --git a/Tests/Decoder/DictionaryDecoderTesting.swift b/Tests/Decoder/DictionaryDecoderTesting.swift index 0e31868..75723a1 100644 --- a/Tests/Decoder/DictionaryDecoderTesting.swift +++ b/Tests/Decoder/DictionaryDecoderTesting.swift @@ -152,12 +152,21 @@ extension DictionaryDateDecodingStrategy { case .secondsSince1970: return .secondsSince1970 - case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") + case .iso8601(style: .dateFormatter): + return .custom { decoder in + let container = try decoder.singleValueContainer() + + guard let date = ISO8601DateFormatter().date(from: try container.decode(String.self)) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date") + } + + return date } - return .iso8601 + case .iso8601(style: .formatStyle): + return .custom { decoder in + try Date.ISO8601FormatStyle().parse(try decoder.singleValueContainer().decode(String.self)) + } case let .formatted(dateFormatter): return .formatted(dateFormatter) diff --git a/Tests/Decoder/DictionaryDecoderTests.swift b/Tests/Decoder/DictionaryDecoderTests.swift index 5228352..40580a6 100644 --- a/Tests/Decoder/DictionaryDecoderTests.swift +++ b/Tests/Decoder/DictionaryDecoderTests.swift @@ -133,6 +133,50 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting { assertDecoderSucceeds(decoding: [String: Float].self, from: dictionary) } + func testThatDecoderSucceedsWhenDecodingNumbersOfOtherTypes() { + struct DecodableStruct: Decodable, Equatable { + let foo: Double + let bar: Float + let baz: Int64 + let qux: Int + } + + let dictionary: [String: Any] = [ + "foo": 123, + "bar": 1.5, + "baz": 456, + "qux": 789.0 + ] + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + +#if compiler(>=6.0) + func testThatDecoderSucceedsWhenDecodingWideIntegers() throws { + guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else { + throw XCTSkip("Int128 and UInt128 are unavailable") + } + + struct DecodableStruct: Decodable, Equatable { + let foo: Int128 + let bar: UInt128 + let baz: Int128 + let qux: [UInt128] + } + + let dictionary: [String: Any] = [ + "foo": Int128.max, + "bar": UInt128.max, + "baz": -123, + "qux": [UInt64.max, NSNumber(value: 456)] as [Any] + ] + + let value = DecodableStruct(foo: .max, bar: .max, baz: -123, qux: [UInt128(UInt64.max), 456]) + + assertDecoderSucceeds(decoding: value, from: dictionary) + } +#endif + func testThatDecoderSucceedsWhenDecodingStringToStringDictionary() { let dictionary = [ "foo": "qwe", @@ -208,6 +252,87 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting { assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) } + func testThatDecoderSucceedsWhenDecodingOptionalsOfKeyedContainer() { + struct Nested: Decodable, Equatable { + let foo: Int + } + + struct DecodableStruct: Decodable, Equatable { + let int: Int? + let string: String? + let double: Double? + let nested: Nested? + let absent: Int? + let null: String? + let nullNested: Nested? + } + + let dictionary: [String: Any] = [ + "int": 1, + "string": "foo", + "double": 1.5, + "nested": ["foo": 2], + "null": NSNull(), + "nullNested": NSNull() + ] + + let value = DecodableStruct( + int: 1, + string: "foo", + double: 1.5, + nested: Nested(foo: 2), + absent: nil, + null: nil, + nullNested: nil + ) + + assertDecoderSucceeds(decoding: value, from: dictionary) + } + + func testThatDecoderSucceedsWhenDecodingFoundationNumbers() throws { + struct DecodableStruct: Decodable, Equatable { + let bool: Bool + let int: Int + let int8: Int8 + let int16: Int16 + let int32: Int32 + let int64: Int64 + let uint: UInt + let uint8: UInt8 + let uint16: UInt16 + let uint32: UInt32 + let uint64: UInt64 + let double: Double + let float: Float + let ints: [Int] + let doubles: [Double] + } + + let object: [String: Any] = [ + "bool": true, + "int": -1, + "int8": -2, + "int16": -3, + "int32": -4, + "int64": -5, + "uint": 1, + "uint8": 2, + "uint16": 3, + "uint32": 4, + "uint64": 5, + "double": 1.5, + "float": 2.5, + "ints": [1, 2, 3], + "doubles": [1.5, 2] + ] + + // Numbers of `JSONSerialization` output are `NSNumber` instances. + let json = try JSONSerialization.data(withJSONObject: object) + let dictionary = try XCTUnwrap(JSONSerialization.jsonObject(with: json) as? [String: Any]) + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + func testThatDecoderSucceedsWhenDecodingStructWithURL() { struct DecodableStruct: Decodable, Equatable { let foobar: URL? @@ -333,6 +458,88 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting { assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) } + func testThatDecoderSucceedsWhenDecodingStructUsingDecodeIfPresentOfUnkeyedContainer() { + struct DecodableStruct: Decodable, Equatable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: Int? + let baz: Int? + let qux: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + bar = try unkeyedContainer.decodeIfPresent(Int.self) + baz = try unkeyedContainer.decodeIfPresent(Int.self) + qux = try unkeyedContainer.decodeIfPresent(Int.self) + } + } + + let dictionary: [String: Any] = ["foo": [123, NSNull(), 456] as [Any]] + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + + func testThatDecoderSucceedsWhenDecodingElementCodingPathsOfUnkeyedContainer() { + struct Element: Decodable, Equatable { + let index: Int? + + init(from decoder: Decoder) throws { + index = decoder.codingPath.last?.intValue + } + } + + struct DecodableStruct: Decodable, Equatable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: Element + let baz: Element + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + bar = try unkeyedContainer.decode(Element.self) + baz = try Element(from: unkeyedContainer.superDecoder()) + } + } + + let dictionary = ["foo": [123, 456]] + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + + func testThatDecoderSucceedsWhenDecodingStructAfterFailedAttemptsInUnkeyedContainer() { + struct DecodableStruct: Decodable, Equatable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: String + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + _ = try? unkeyedContainer.decode(Int.self) + _ = try? unkeyedContainer.decode([Int].self) + _ = try? unkeyedContainer.nestedContainer(keyedBy: CodingKeys.self) + _ = try? unkeyedContainer.nestedUnkeyedContainer() + + bar = try unkeyedContainer.decode(String.self) + } + } + + let dictionary = ["foo": ["bar"]] + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + func testThatDecoderSucceedsWhenDecodingStructInSeparateKeyedContainersOfUnkeyedContainer() { struct DecodableStruct: Decodable, Equatable { enum CodingKeys: String, CodingKey { @@ -600,6 +807,86 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting { } } + func testThatDecoderFailsWhenDecodingNumberThatDoesNotFit() { + let dictionary = ["foobar": 1.5] + + assertDecoderFails(decoding: [String: Int].self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, _) where type is Int.Type: + return true + + default: + return false + } + } + } + +#if compiler(>=6.0) + func testThatDecoderFailsWhenDecodingWideIntegerThatDoesNotFit() throws { + guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else { + throw XCTSkip("Int128 and UInt128 are unavailable") + } + + let dictionary = ["foobar": 1.5] + + assertDecoderFails(decoding: [String: Int128].self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, _) where type is Int128.Type: + return true + + default: + return false + } + } + } +#endif + + func testThatDecoderFailsWhenDecodingOptionalOfWrongType() { + struct DecodableStruct: Decodable { + let foo: Int? + } + + let dictionary = ["foo": "bar"] + + assertDecoderFails(decoding: DecodableStruct.self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, context) where type is Int.Type: + return context.codingPath.map(\.stringValue) == ["foo"] + + default: + return false + } + } + } + + func testThatDecoderFailsWhenDecodingFoundationNumberThatDoesNotFit() { + let dictionary: [String: Any] = ["foobar": NSNumber(value: 300)] + + assertDecoderFails(decoding: [String: Int8].self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, _) where type is Int8.Type: + return true + + default: + return false + } + } + } + + func testThatDecoderFailsWhenDecodingBoolFromNumber() { + let dictionary = ["foobar": 1] + + assertDecoderFails(decoding: [String: Bool].self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, _) where type is Bool.Type: + return true + + default: + return false + } + } + } + func testThatDecoderFailsWhenDecodingNilForKeyedContainer() { struct DecodableStruct: Decodable { enum CodingKeys: String, CodingKey { @@ -735,6 +1022,37 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting { } } + func testThatDecoderFailsWhenDecodingInvalidElementForUnkeyedContainer() { + struct DecodableStruct: Decodable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: Int + let baz: Int + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + bar = try unkeyedContainer.decode(Int.self) + baz = try unkeyedContainer.decode(Int.self) + } + } + + let dictionary: [String: Any] = ["foo": [123, "456"] as [Any]] + + assertDecoderFails(decoding: DecodableStruct.self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, context) where type is Int.Type: + return context.codingPath.map(\.stringValue) == ["foo", "1"] + + default: + return false + } + } + } + // MARK: - XCTestCase override func setUp() { diff --git a/Tests/Encoder/DictionaryEncoderStrategiesTests.swift b/Tests/Encoder/DictionaryEncoderStrategiesTests.swift index 4ec139d..ced3397 100644 --- a/Tests/Encoder/DictionaryEncoderStrategiesTests.swift +++ b/Tests/Encoder/DictionaryEncoderStrategiesTests.swift @@ -60,7 +60,6 @@ final class DictionaryEncoderStrategiesTests: XCTestCase, DictionaryEncoderTesti assertEncoderSucceeds(encoding: value) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) func testThatEncoderSucceedsWhenEncodingDateToISO8601Format() { encoder.dateEncodingStrategy = .iso8601 @@ -69,7 +68,23 @@ final class DictionaryEncoderStrategiesTests: XCTestCase, DictionaryEncoderTesti assertEncoderSucceeds(encoding: value) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + func testThatEncoderSucceedsWhenEncodingDateWithFractionalSecondsToISO8601Format() { + encoder.dateEncodingStrategy = .iso8601 + + let value = ["foobar": Date(timeIntervalSince1970: 0.9999)] + + // Dates are rounded to milliseconds before fractions of a second are dropped, as in `ISO8601DateFormatter`. + assertEncoderSucceeds(encoding: value, expecting: ["foobar": "1970-01-01T00:00:01Z"]) + } + + func testThatEncoderSucceedsWhenEncodingDateToISO8601FormatStyle() { + encoder.dateEncodingStrategy = .iso8601(style: .formatStyle) + + let value = ["foobar": Date(timeIntervalSince1970: 0.9999)] + + assertEncoderSucceeds(encoding: value, expecting: ["foobar": "1970-01-01T00:00:00Z"]) + } + func testThatEncoderSucceedsWhenEncodingDateUsingFormatter() { let dateFormatter = DateFormatter() @@ -139,6 +154,34 @@ final class DictionaryEncoderStrategiesTests: XCTestCase, DictionaryEncoderTesti // MARK: - + func testThatEncoderSucceedsWhenEncodingDecimal() throws { + struct DeferredDecimal: Encodable { + let value: Decimal + + func encode(to encoder: Encoder) throws { + try value.encode(to: encoder) + } + } + + let decimal = Decimal(string: "1.5")! + let expectedDictionary = try encoder.encode(["foobar": DeferredDecimal(value: decimal)]) + + XCTAssert(expectedDictionary["foobar"] is [String: Any]) + + assertEncoderSucceeds(encoding: ["foobar": decimal], expecting: expectedDictionary) + } + + func testThatEncoderSucceedsWhenEncodingDecimalToNumber() { + encoder.decimalEncodingStrategy = .number + + let value = [ + "foo": Decimal(string: "1.5")!, + "bar": Decimal(string: "-2.25")! + ] + + assertEncoderSucceeds(encoding: value, expecting: value) + } + func testThatEncoderFailsWhenEncodingPositiveInfinityFloat() { encoder.nonConformingFloatEncodingStrategy = .throw diff --git a/Tests/Encoder/DictionaryEncoderTesting.swift b/Tests/Encoder/DictionaryEncoderTesting.swift index ba17b03..b61c6cf 100644 --- a/Tests/Encoder/DictionaryEncoderTesting.swift +++ b/Tests/Encoder/DictionaryEncoderTesting.swift @@ -118,12 +118,25 @@ extension DictionaryDateEncodingStrategy { case .secondsSince1970: return .secondsSince1970 - case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") + case .iso8601(style: .dateFormatter): + return .custom { date, encoder in + var container = encoder.singleValueContainer() + + try container.encode( + ISO8601DateFormatter.string( + from: date, + timeZone: TimeZone(secondsFromGMT: 0)!, + formatOptions: .withInternetDateTime + ) + ) } - return .iso8601 + case .iso8601(style: .formatStyle): + return .custom { date, encoder in + var container = encoder.singleValueContainer() + + try container.encode(Date.ISO8601FormatStyle().format(date)) + } case let .formatted(dateFormatter): return .formatted(dateFormatter) diff --git a/Tests/Encoder/DictionaryEncoderTests.swift b/Tests/Encoder/DictionaryEncoderTests.swift index 17f8083..6b6da2b 100644 --- a/Tests/Encoder/DictionaryEncoderTests.swift +++ b/Tests/Encoder/DictionaryEncoderTests.swift @@ -142,6 +142,26 @@ final class DictionaryEncoderTests: XCTestCase, DictionaryEncoderTesting { assertEncoderSucceeds(encoding: value) } +#if compiler(>=6.0) + func testThatEncoderSucceedsWhenEncodingWideIntegers() throws { + guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else { + throw XCTSkip("Int128 and UInt128 are unavailable") + } + + struct EncodableStruct: Encodable { + let foo = Int128.max + let bar = UInt128.max + let baz: [Int128] = [-1, 2] + } + + let dictionary = try encoder.encode(EncodableStruct()) + + XCTAssertEqual(dictionary["foo"] as? Int128, .max) + XCTAssertEqual(dictionary["bar"] as? UInt128, .max) + XCTAssertEqual(dictionary["baz"] as? [Int128], [-1, 2]) + } +#endif + func testThatEncoderSucceedsWhenEncodingStringToURLDictionary() { let value = [ "foo": URL(string: "https://swift.org")!,