diff --git a/DictionaryCoder.xcodeproj/project.pbxproj b/DictionaryCoder.xcodeproj/project.pbxproj index f70f984..bab8936 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,18 @@ 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 */; }; + 40B78C975BEC5E40F990D9AF /* DictionaryISO8601Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */; }; C006F1C22406B20400C3F783 /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013EB5A23E1E23B00F12CCB /* Optional+Extensions.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 +66,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 +93,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 */; }; @@ -177,11 +189,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 +204,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 = ""; }; @@ -305,6 +320,7 @@ children = ( C001ACA62F027A3B008C37A5 /* Mutex */, C013EAF323E1E0AE00F12CCB /* AnyCodingKey.swift */, + 42D8B55F1A8736A8BFA545F4 /* DictionaryISO8601Style.swift */, C013EB5A23E1E23B00F12CCB /* Optional+Extensions.swift */, C013EAF223E1E0AE00F12CCB /* RangeReplaceableCollection+Extensions.swift */, ); @@ -328,6 +344,7 @@ isa = PBXGroup; children = ( C013EAFA23E1E0AF00F12CCB /* DictionaryDataDecodingStrategy.swift */, + FFD64BF90D40C01B95CBB75B /* DictionaryDecimalDecodingStrategy.swift */, C013EAF923E1E0AE00F12CCB /* DictionaryDateDecodingStrategy.swift */, C013EAFF23E1E0AF00F12CCB /* DictionaryDecodingOptions.swift */, C013EAF823E1E0AE00F12CCB /* DictionaryKeyDecodingStrategy.swift */, @@ -356,6 +373,7 @@ isa = PBXGroup; children = ( C013EB0423E1E0AF00F12CCB /* DictionaryDataEncodingStrategy.swift */, + EFB19ABEC86D0C1E9BEF2848 /* DictionaryDecimalEncodingStrategy.swift */, C013EB0323E1E0AF00F12CCB /* DictionaryDateEncodingStrategy.swift */, C013EB0B23E1E0AF00F12CCB /* DictionaryEncodingOptions.swift */, C013EB0223E1E0AF00F12CCB /* DictionaryKeyEncodingStrategy.swift */, @@ -777,6 +795,7 @@ 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 +804,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 */, @@ -816,13 +837,16 @@ 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 */, @@ -865,13 +889,16 @@ 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 */, @@ -914,13 +941,16 @@ 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..dc74ae0 100644 --- a/Sources/Decoder/DictionaryComponentDecoder.swift +++ b/Sources/Decoder/DictionaryComponentDecoder.swift @@ -16,11 +16,86 @@ extension DictionaryComponentDecoder { at codingPath: [CodingKey] ) throws -> T { guard let value = component as? T else { + return try decodeConvertedNumber(from: component, at: codingPath) + } + + return value + } + + // 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 + } + + // 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, @@ -88,7 +163,19 @@ 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 { @@ -102,14 +189,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." @@ -213,6 +296,13 @@ extension DictionaryComponentDecoder { 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, *) + internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Int128 { + try decodeWideInteger(from: component, at: codingPath) + } +#endif + internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt { try decodePrimitiveValue(from: component, at: codingPath) } @@ -233,6 +323,13 @@ extension DictionaryComponentDecoder { 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, *) + internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> UInt128 { + try decodeWideInteger(from: component, at: codingPath) + } +#endif + internal func decodeComponentValue(from component: Any?, at codingPath: [CodingKey]) throws -> Double { try decodeFloatingPointValue(from: component, at: codingPath) } @@ -250,16 +347,20 @@ extension DictionaryComponentDecoder { from component: Any?, at codingPath: [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..f843ed0 100644 --- a/Sources/Decoder/DictionaryKeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryKeyedDecodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal 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 @@ -100,6 +98,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 +125,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)) } 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..929c88d 100644 --- a/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryUnkeyedDecodingContainer: UnkeyedDecodingContainer, DictionaryComponentDecoder { @@ -41,7 +39,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 +50,126 @@ internal final class DictionaryUnkeyedDecodingContainer: throw DecodingError.valueNotFound(Any.self, errorContext) } - defer { - currentIndex += 1 - } - return components[currentIndex] } + @inline(__always) + private func decodeNextComponent( + _ decodeComponent: (_ component: Any?, _ codingPath: [CodingKey]) throws -> T + ) throws -> T { + let value = try decodeComponent(try peekNextComponent(), currentCodingPath) + + currentIndex += 1 + + return value + } + + private func superDecoder(for component: Any?, at codingPath: [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..cd99768 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 diff --git a/Sources/Encoder/DictionaryComponent.swift b/Sources/Encoder/DictionaryComponent.swift index 138d310..b1f1a41 100644 --- a/Sources/Encoder/DictionaryComponent.swift +++ b/Sources/Encoder/DictionaryComponent.swift @@ -1,5 +1,3 @@ -import Foundation - internal indirect 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..5472cbe 100644 --- a/Sources/Encoder/DictionaryComponentEncoder.swift +++ b/Sources/Encoder/DictionaryComponentEncoder.swift @@ -71,18 +71,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) @@ -108,6 +98,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] @@ -165,6 +165,13 @@ extension DictionaryComponentEncoder { encodePrimitiveValue(value, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encodeComponentValue(_ value: Int128, at codingPath: [CodingKey]) -> DictionaryComponent { + encodePrimitiveValue(value, at: codingPath) + } +#endif + internal func encodeComponentValue(_ value: UInt, at codingPath: [CodingKey]) -> DictionaryComponent { encodePrimitiveValue(value, at: codingPath) } @@ -185,6 +192,13 @@ extension DictionaryComponentEncoder { encodePrimitiveValue(value, at: codingPath) } +#if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func encodeComponentValue(_ value: UInt128, at codingPath: [CodingKey]) -> DictionaryComponent { + encodePrimitiveValue(value, at: codingPath) + } +#endif + internal func encodeComponentValue(_ value: Double, at codingPath: [CodingKey]) throws -> DictionaryComponent { try encodeFloatingPoint(value, at: codingPath) } @@ -201,15 +215,19 @@ extension DictionaryComponentEncoder { _ value: T, at codingPath: [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 ObjectIdentifier(Data.self): + return try encodeData(value as! Data, at: codingPath) - case let data as Data: - return try encodeData(data, at: codingPath) + case ObjectIdentifier(URL.self): + return try encodeURL(value as! URL, at: codingPath) - case let url as URL: - return try encodeURL(url, at: codingPath) + case ObjectIdentifier(Decimal.self): + return try encodeDecimal(value as! Decimal, at: codingPath) default: return try encodeNonPrimitiveValue(value, at: codingPath) @@ -217,13 +235,6 @@ extension DictionaryComponentEncoder { } } -extension TimeZone { - - // MARK: - Type Properties - - fileprivate static let iso8601TimeZone = TimeZone(secondsFromGMT: 0)! -} - extension EncodingError { // MARK: - Type Methods @@ -250,6 +261,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..24da78b 100644 --- a/Sources/Encoder/DictionarySingleValueEncodingContainer.swift +++ b/Sources/Encoder/DictionarySingleValueEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionarySingleValueEncodingContainer: Encoder, SingleValueEncodingContainer, @@ -71,6 +69,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 +96,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..494761e 100644 --- a/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift +++ b/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryUnkeyedEncodingContainer: UnkeyedEncodingContainer, DictionaryComponentContainer, @@ -69,6 +67,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 +94,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)) } 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 index 4344e37..141c9ac 100644 --- a/Sources/Tools/Optional+Extensions.swift +++ b/Sources/Tools/Optional+Extensions.swift @@ -1,5 +1,3 @@ -import Foundation - extension Optional { // MARK: - Instance Properties @@ -8,12 +6,3 @@ extension Optional { 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..06c51f1 100644 --- a/Sources/Tools/RangeReplaceableCollection+Extensions.swift +++ b/Sources/Tools/RangeReplaceableCollection+Extensions.swift @@ -1,5 +1,3 @@ -import Foundation - extension RangeReplaceableCollection { // MARK: - Instance Methods 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..253926b 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", @@ -333,6 +377,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 +726,54 @@ 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 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 +909,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")!,