From 942b108c1d506539c0c53276ed4ec35eed36634e Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Tue, 3 Sep 2024 17:35:12 +0200 Subject: [PATCH 1/3] fix(cli): replace app-store deprecated method on build (#7637) --- cli/src/ios/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/ios/build.ts b/cli/src/ios/build.ts index ea7357318..d79050ae5 100644 --- a/cli/src/ios/build.ts +++ b/cli/src/ios/build.ts @@ -53,7 +53,7 @@ export async function buildiOS( method -app-store +app-store-connect `; From 410249b6c626e67235f25b466ed4969d52148bd1 Mon Sep 17 00:00:00 2001 From: Steven Sherry Date: Thu, 12 Sep 2024 12:36:49 -0500 Subject: [PATCH 2/3] feat(ios): JSValueEncoder/Decoder feature parity with JSONEncoder/Decoder (#7647) closes #7576 --- .../Capacitor.xcodeproj/project.pbxproj | 16 ++ .../Capacitor/Codable/JSValueDecoder.swift | 196 ++++++++++++++--- .../Capacitor/Codable/JSValueEncoder.swift | 206 ++++++++++++++---- .../CodableTests/DataCodableTests.swift | 156 +++++++++++++ .../CodableTests/DateCodableTests.swift | 158 ++++++++++++++ .../NonconformingFloatCodableTests.swift | 174 +++++++++++++++ .../CodableTests/URLCodableTests.swift | 62 ++++++ 7 files changed, 900 insertions(+), 68 deletions(-) create mode 100644 ios/Capacitor/CodableTests/DataCodableTests.swift create mode 100644 ios/Capacitor/CodableTests/DateCodableTests.swift create mode 100644 ios/Capacitor/CodableTests/NonconformingFloatCodableTests.swift create mode 100644 ios/Capacitor/CodableTests/URLCodableTests.swift diff --git a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj index 5860adc2e..8ba1634cd 100644 --- a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj +++ b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj @@ -90,7 +90,11 @@ A71289EB27F380FD00DADDF3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71289EA27F380FD00DADDF3 /* RouterTests.swift */; }; A7187FD22BD1CB7D00093C45 /* CAPPluginMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7187FD12BD1CB7D00093C45 /* CAPPluginMethod.swift */; }; A76739792B98E09700795F7B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A76739782B98E09700795F7B /* PrivacyInfo.xcprivacy */; }; + A771ADEE2C8B845000AF234D /* DateCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A771ADED2C8B845000AF234D /* DateCodableTests.swift */; }; + A771ADF12C8B909100AF234D /* URLCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A771ADF02C8B909100AF234D /* URLCodableTests.swift */; }; A7BE62CC2B486A5400165ACB /* KeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7BE62CB2B486A5400165ACB /* KeyValueStore.swift */; }; + A7D474D52C8BA8E8005620A8 /* DataCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D474D42C8BA8E8005620A8 /* DataCodableTests.swift */; }; + A7D474D82C8BA8FD005620A8 /* NonconformingFloatCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D474D72C8BA8FD005620A8 /* NonconformingFloatCodableTests.swift */; }; A7D8B3522B238A840003FAD6 /* JSValueEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D8B3512B238A840003FAD6 /* JSValueEncoder.swift */; }; A7D8B3632B263B8D0003FAD6 /* NestedCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D8B3622B263B8D0003FAD6 /* NestedCodableTests.swift */; }; A7D8B3642B263B8D0003FAD6 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50503EDF1FC08594003606DC /* Capacitor.framework */; }; @@ -244,7 +248,11 @@ A71289EA27F380FD00DADDF3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; A7187FD12BD1CB7D00093C45 /* CAPPluginMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CAPPluginMethod.swift; sourceTree = ""; }; A76739782B98E09700795F7B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + A771ADED2C8B845000AF234D /* DateCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateCodableTests.swift; sourceTree = ""; }; + A771ADF02C8B909100AF234D /* URLCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCodableTests.swift; sourceTree = ""; }; A7BE62CB2B486A5400165ACB /* KeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueStore.swift; sourceTree = ""; }; + A7D474D42C8BA8E8005620A8 /* DataCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCodableTests.swift; sourceTree = ""; }; + A7D474D72C8BA8FD005620A8 /* NonconformingFloatCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonconformingFloatCodableTests.swift; sourceTree = ""; }; A7D8B3512B238A840003FAD6 /* JSValueEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSValueEncoder.swift; sourceTree = ""; }; A7D8B3562B23B2110003FAD6 /* CodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableTests.swift; sourceTree = ""; }; A7D8B3602B263B8D0003FAD6 /* CodableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CodableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -475,6 +483,10 @@ A7D8B3622B263B8D0003FAD6 /* NestedCodableTests.swift */, A7D8B3562B23B2110003FAD6 /* CodableTests.swift */, A7D8B36D2B2692300003FAD6 /* SuperCodableTests.swift */, + A771ADED2C8B845000AF234D /* DateCodableTests.swift */, + A771ADF02C8B909100AF234D /* URLCodableTests.swift */, + A7D474D42C8BA8E8005620A8 /* DataCodableTests.swift */, + A7D474D72C8BA8FD005620A8 /* NonconformingFloatCodableTests.swift */, ); path = CodableTests; sourceTree = ""; @@ -779,9 +791,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A771ADEE2C8B845000AF234D /* DateCodableTests.swift in Sources */, A7D8B3632B263B8D0003FAD6 /* NestedCodableTests.swift in Sources */, + A7D474D52C8BA8E8005620A8 /* DataCodableTests.swift in Sources */, A7D8B36A2B263B990003FAD6 /* CodableTests.swift in Sources */, A7D8B36E2B2692300003FAD6 /* SuperCodableTests.swift in Sources */, + A7D474D82C8BA8FD005620A8 /* NonconformingFloatCodableTests.swift in Sources */, + A771ADF12C8B909100AF234D /* URLCodableTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Capacitor/Capacitor/Codable/JSValueDecoder.swift b/ios/Capacitor/Capacitor/Codable/JSValueDecoder.swift index dd03ed5ba..b1f7081db 100644 --- a/ios/Capacitor/Capacitor/Codable/JSValueDecoder.swift +++ b/ios/Capacitor/Capacitor/Codable/JSValueDecoder.swift @@ -11,7 +11,63 @@ import Combine /// A decoder that can decode ``JSValue`` objects into `Decodable` types. public final class JSValueDecoder: TopLevelDecoder { - public init() {} + /// The strategies available for formatting dates when decoding from a ``JSValue`` + public typealias DateDecodingStrategy = JSONDecoder.DateDecodingStrategy + /// The strategies available for decoding raw data. + public typealias DataDecodingStrategy = JSONDecoder.DataDecodingStrategy + + /// The strategies availble for decoding NaN, Infinity, and -Infinity + public enum NonConformingFloatDecodingStrategy { + /// Decodes directly into the floating point type as .infinity, -.infinity, or .nan + case deferred + /// Throw an error when a non-conforming float is encountered + case `throw` + /// Converts from the provided strings into .infinity, -.infinity, or .nan + case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String) + } + + fileprivate struct Options { + var dataStrategy: DataDecodingStrategy + var dateStrategy: DateDecodingStrategy + var nonConformingStrategy: NonConformingFloatDecodingStrategy + } + + private var options: Options + + /// Creates a new JSValueDecoder with the provided decoding and formatting strategies + /// - Parameters: + /// - dateDecodingStrategy: Defaults to `DateDecodingStrategy.deferredToDate` + /// - dataDecodingStrategy: Defaults to `DataDecodingStrategy.deferredToData` + /// - nonConformingFloatDecodingStrategy: Defaults to ``NonConformingFloatDecodingStrategy/deferred`` + public init( + dateDecodingStrategy: DateDecodingStrategy = .deferredToDate, + dataDecodingStrategy: DataDecodingStrategy = .deferredToData, + nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .deferred + ) { + self.options = .init(dataStrategy: dataDecodingStrategy, dateStrategy: dateDecodingStrategy, nonConformingStrategy: nonConformingFloatDecodingStrategy) + } + + fileprivate init(options: Options) { + self.options = options + } + + /// The strategy to use when decoding dates from a ``JSValue`` + public var dateDecodingStrategy: DateDecodingStrategy { + get { options.dateStrategy } + set { options.dateStrategy = newValue } + } + + /// The strategy to use when decoding raw data from a ``JSValue`` + public var dataDecodingStrategy: DataDecodingStrategy { + get { options.dataStrategy } + set { options.dataStrategy = newValue } + } + + /// The strategy used by a decoder when it encounters exceptional floating-point values + public var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy { + get { options.nonConformingStrategy } + set { options.nonConformingStrategy = newValue } + } /// Decodes a ``JSValue`` into the provided `Decodable` type /// - Parameters: @@ -23,20 +79,23 @@ public final class JSValueDecoder: TopLevelDecoder { /// 1. A type mismatch was found. /// 2. A key was not found in the `data` field that is required in the `type` provided. public func decode(_ type: T.Type, from data: JSValue) throws -> T where T: Decodable { - let decoder = _JSValueDecoder(data: data) - return try T(from: decoder) + let decoder = _JSValueDecoder(data: data, options: options) + return try decoder.decodeData(as: T.self) } } typealias CodingUserInfo = [CodingUserInfoKey: Any] +private typealias Options = JSValueDecoder.Options private final class _JSValueDecoder { var codingPath: [CodingKey] = [] var userInfo: CodingUserInfo = [:] + var options: Options fileprivate var data: JSValue - init(data: JSValue) { + init(data: JSValue, options: Options) { self.data = data + self.options = options } } @@ -50,7 +109,8 @@ extension _JSValueDecoder: Decoder { KeyedContainer( data: data, codingPath: codingPath, - userInfo: userInfo + userInfo: userInfo, + options: options ) ) } @@ -60,11 +120,61 @@ extension _JSValueDecoder: Decoder { throw DecodingError.typeMismatch(JSArray.self, on: data, codingPath: codingPath) } - return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo) + return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options) } func singleValueContainer() throws -> SingleValueDecodingContainer { - SingleValueContainer(data: data, codingPath: codingPath, userInfo: userInfo) + SingleValueContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options) + } + + fileprivate func decodeData(as type: T.Type) throws -> T where T: Decodable { + switch type { + case is Date.Type: + switch options.dateStrategy { + case .deferredToDate: + return try T(from: self) + case .secondsSince1970: + guard let value = data as? NSNumber else { throw DecodingError.dataCorrupted(data, target: Double.self, codingPath: codingPath) } + return Date(timeIntervalSince1970: value.doubleValue) as! T + case .millisecondsSince1970: + guard let value = data as? NSNumber else { throw DecodingError.dataCorrupted(data, target: Double.self, codingPath: codingPath) } + return Date(timeIntervalSince1970: value.doubleValue / Double(MSEC_PER_SEC)) as! T + case .iso8601: + guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) } + let formatter = ISO8601DateFormatter() + guard let date = formatter.date(from: value) else { throw DecodingError.dataCorrupted(value, target: Date.self, codingPath: codingPath) } + return date as! T + case .formatted(let formatter): + guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) } + guard let date = formatter.date(from: value) else { throw DecodingError.dataCorrupted(value, target: Date.self, codingPath: codingPath) } + return date as! T + case .custom(let decode): + return try decode(self) as! T + @unknown default: + return try T(from: self) + } + case is URL.Type: + guard let str = data as? String, + let url = URL(string: str) + else { throw DecodingError.dataCorrupted(data, target: URL.self, codingPath: codingPath) } + + return url as! T + case is Data.Type: + switch options.dataStrategy { + case .deferredToData: + return try T(from: self) + case .base64: + guard let value = data as? String else { throw DecodingError.dataCorrupted(data, target: String.self, codingPath: codingPath) } + guard let data = Data(base64Encoded: value) else { throw DecodingError.dataCorrupted(value, target: Data.self, codingPath: codingPath) } + return data as! T + case .custom(let decode): + return try decode(self) as! T + @unknown default: + return try T(from: self) + } + default: + return try T(from: self) + } } } @@ -73,12 +183,14 @@ private final class KeyedContainer where Key: CodingKey { var codingPath: [CodingKey] var userInfo: CodingUserInfo var allKeys: [Key] + var options: Options - init(data: JSObject, codingPath: [CodingKey], userInfo: CodingUserInfo) { + init(data: JSObject, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) { self.data = data self.codingPath = codingPath self.userInfo = userInfo self.allKeys = data.keys.compactMap(Key.init(stringValue:)) + self.options = options } } @@ -98,8 +210,8 @@ extension KeyedContainer: KeyedDecodingContainerProtocol { var newPath = codingPath newPath.append(key) - let decoder = _JSValueDecoder(data: rawValue) - return try T(from: decoder) + let decoder = _JSValueDecoder(data: rawValue, options: options) + return try decoder.decodeData(as: T.self) } func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { @@ -113,7 +225,7 @@ extension KeyedContainer: KeyedDecodingContainerProtocol { ) } - return UnkeyedContainer(data: data, codingPath: newPath, userInfo: userInfo) + return UnkeyedContainer(data: data, codingPath: newPath, userInfo: userInfo, options: options) } func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey: CodingKey { @@ -127,7 +239,7 @@ extension KeyedContainer: KeyedDecodingContainerProtocol { ) } - return KeyedDecodingContainer(KeyedContainer(data: data, codingPath: newPath, userInfo: userInfo)) + return KeyedDecodingContainer(KeyedContainer(data: data, codingPath: newPath, userInfo: userInfo, options: options)) } enum SuperKey: String, CodingKey { case `super` } @@ -139,7 +251,7 @@ extension KeyedContainer: KeyedDecodingContainerProtocol { throw DecodingError.keyNotFound(SuperKey.super, on: data, codingPath: newPath) } - return _JSValueDecoder(data: data) + return _JSValueDecoder(data: data, options: options) } func superDecoder(forKey key: Key) throws -> Decoder { @@ -149,7 +261,7 @@ extension KeyedContainer: KeyedDecodingContainerProtocol { throw DecodingError.keyNotFound(key, on: data, codingPath: newPath) } - return _JSValueDecoder(data: data) + return _JSValueDecoder(data: data, options: options) } } @@ -158,11 +270,13 @@ private final class UnkeyedContainer { var codingPath: [CodingKey] var userInfo: CodingUserInfo private(set) var currentIndex = 0 + var options: Options - init(data: JSArray, codingPath: [CodingKey], userInfo: CodingUserInfo) { + init(data: JSArray, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) { self.data = data self.codingPath = codingPath self.userInfo = userInfo + self.options = options } } @@ -182,8 +296,8 @@ extension UnkeyedContainer: UnkeyedDecodingContainer { func decode(_ type: T.Type) throws -> T where T: Decodable { defer { currentIndex += 1 } - let decoder = _JSValueDecoder(data: data[currentIndex]) - return try T(from: decoder) + let decoder = _JSValueDecoder(data: data[currentIndex], options: options) + return try decoder.decodeData(as: T.self) } func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { @@ -192,7 +306,7 @@ extension UnkeyedContainer: UnkeyedDecodingContainer { throw DecodingError.typeMismatch(JSArray.self, on: data[currentIndex], codingPath: codingPath) } - return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo) + return UnkeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options) } func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey: CodingKey { @@ -201,13 +315,13 @@ extension UnkeyedContainer: UnkeyedDecodingContainer { throw DecodingError.typeMismatch(JSObject.self, on: data[currentIndex], codingPath: codingPath) } - return KeyedDecodingContainer(KeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo)) + return KeyedDecodingContainer(KeyedContainer(data: data, codingPath: codingPath, userInfo: userInfo, options: options)) } func superDecoder() throws -> Decoder { defer { currentIndex += 1 } let data = data[currentIndex] - return _JSValueDecoder(data: data) + return _JSValueDecoder(data: data, options: options) } } @@ -215,11 +329,13 @@ private final class SingleValueContainer { var data: JSValue var codingPath: [CodingKey] var userInfo: CodingUserInfo + var options: Options - init(data: JSValue, codingPath: [CodingKey], userInfo: CodingUserInfo) { + init(data: JSValue, codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) { self.data = data self.codingPath = codingPath self.userInfo = userInfo + self.options = options } } @@ -236,6 +352,28 @@ extension SingleValueContainer: SingleValueDecodingContainer { return data } + private func castFloat(to type: N.Type) throws -> N where N: FloatingPoint { + if let data = data as? String, + case let .convertFromString(positiveInfinity: pos, negativeInfinity: neg, nan: nan) = options.nonConformingStrategy { + switch data { + case pos: + return N.infinity + case neg: + return -N.infinity + case nan: + return N.nan + default: + throw DecodingError.typeMismatch(type, on: data, codingPath: codingPath) + } + } + + let data = try cast(to: N.self) + if !data.isFinite, case .throw = options.nonConformingStrategy { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "\(data) is a non-conforming floating point number")) + } + return data + } + func decode(_ type: Bool.Type) throws -> Bool { try cast(to: type) } @@ -245,11 +383,11 @@ extension SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: Double.Type) throws -> Double { - try cast(to: type) + try castFloat(to: type) } func decode(_ type: Float.Type) throws -> Float { - try cast(to: type) + try castFloat(to: type) } func decode(_ type: Int.Type) throws -> Int { @@ -293,13 +431,13 @@ extension SingleValueContainer: SingleValueDecodingContainer { } func decode(_ type: T.Type) throws -> T where T: Decodable { - let decoder = _JSValueDecoder(data: data) - return try T(from: decoder) + let decoder = _JSValueDecoder(data: data, options: options) + return try decoder.decodeData(as: T.self) } } extension DecodingError { - static func typeMismatch(_ type: Any.Type, on data: JSValue, codingPath: [CodingKey]) -> DecodingError { + static func typeMismatch(_ type: Any.Type, on data: any JSValue, codingPath: [CodingKey]) -> DecodingError { return .typeMismatch( type, .init( @@ -309,7 +447,7 @@ extension DecodingError { ) } - static func keyNotFound(_ key: any CodingKey, on data: JSValue, codingPath: [CodingKey]) -> DecodingError { + static func keyNotFound(_ key: any CodingKey, on data: any JSValue, codingPath: [CodingKey]) -> DecodingError { return .keyNotFound( key, .init( @@ -317,4 +455,8 @@ extension DecodingError { debugDescription: "Key \(key.stringValue) not found in \(data)") ) } + + static func dataCorrupted(_ value: any JSValue, target type: T.Type, codingPath: [CodingKey]) -> DecodingError where T: Decodable { + return .dataCorrupted(.init(codingPath: codingPath, debugDescription: "\(value) was not in the format expected for \(T.self)")) + } } diff --git a/ios/Capacitor/Capacitor/Codable/JSValueEncoder.swift b/ios/Capacitor/Capacitor/Codable/JSValueEncoder.swift index fcfde2143..ce017fc34 100644 --- a/ios/Capacitor/Capacitor/Codable/JSValueEncoder.swift +++ b/ios/Capacitor/Capacitor/Codable/JSValueEncoder.swift @@ -19,22 +19,77 @@ public final class JSValueEncoder: TopLevelEncoder { case undefined } + /// The strategies available for encoding .nan, .infinity, and -.infinity + public enum NonConformingFloatEncodingStrategy: Equatable { + /// Throws an error when encountering an exceptional floating-point value + case `throw` + /// Converts to the provided strings + case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String) + /// Encodes directly into an NSNumber + case deferred + } + + /// The strategy to use when encoding `Date` values + public typealias DateEncodingStrategy = JSONEncoder.DateEncodingStrategy + + /// The strategy to use when encoding `Data` values + public typealias DataEncodingStrategy = JSONEncoder.DataEncodingStrategy + + fileprivate struct Options { + var optionalStrategy: OptionalEncodingStrategy + var dateStrategy: DateEncodingStrategy + var dataStrategy: DataEncodingStrategy + var nonConformingFloatStrategy: NonConformingFloatEncodingStrategy + } + + private var options: Options + /// The strategy to use when encoding `nil` values - public var optionalEncodingStrategy: OptionalEncodingStrategy + public var optionalEncodingStrategy: OptionalEncodingStrategy { + get { options.optionalStrategy } + set { options.optionalStrategy = newValue } + } + + /// The strategy to use when encoding dates + public var dateEncodingStrategy: DateEncodingStrategy { + get { options.dateStrategy } + set { options.dateStrategy = newValue } + } + + /// The encoding strategy to use when encoding raw data + public var dataEncodingStrategy: DataEncodingStrategy { + get { options.dataStrategy } + set { options.dataStrategy = newValue } + } + + /// The encoding strategy to use when the encoder encounters exceptional floating-point values + public var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy { + get { options.nonConformingFloatStrategy } + set { options.nonConformingFloatStrategy = newValue } + } /// Creates a new `JSValueEncoder` - /// - Parameter optionalEncodingStrategy: The strategy to use when encoding `nil` values - public init(optionalEncodingStrategy: OptionalEncodingStrategy = .undefined) { - self.optionalEncodingStrategy = optionalEncodingStrategy + /// - Parameter optionalEncodingStrategy: The strategy to use when encoding `nil` values. Defaults to ``OptionalEncodingStrategy-swift.enum/undefined`` + /// - Parameter dateEncodingStrategy: Defaults to `DateEncodingStrategy.deferredToDate` + /// - Parameter dataEncodingStrategy: Defaults to `DataEncodingStrategy.deferredToData` + /// - Parameter nonConformingFloatEncodingStategy: Defaults to ``NonConformingFloatEncodingStrategy-swift.enum/deferred`` + public init( + optionalEncodingStrategy: OptionalEncodingStrategy = .undefined, + dateEncodingStrategy: DateEncodingStrategy = .deferredToDate, + dataEncodingStrategy: DataEncodingStrategy = .deferredToData, + nonConformingFloatEncodingStategy: NonConformingFloatEncodingStrategy = .deferred + ) { + self.options = .init(optionalStrategy: optionalEncodingStrategy, dateStrategy: dateEncodingStrategy, dataStrategy: dataEncodingStrategy, nonConformingFloatStrategy: nonConformingFloatEncodingStategy) } + /// Encodes an `Encodable` value to a ``JSValue`` /// - Parameter value: The value to encode to ``JSValue`` /// - Returns: The encoded ``JSValue`` /// - Throws: An error if the value could not be encoded as a ``JSValue`` public func encode(_ value: T) throws -> JSValue where T: Encodable { - let encoder = _JSValueEncoder(optionalEncodingStrategy: optionalEncodingStrategy) - try value.encode(to: encoder) + let encoder = _JSValueEncoder(options: options) + try encoder.encodeGeneric(value) guard let value = encoder.data else { throw EncodingError.invalidValue( value, @@ -103,19 +158,21 @@ private enum EncodingContainer: JSValueEncodingContainer { } } +private typealias Options = JSValueEncoder.Options + private final class _JSValueEncoder: JSValueEncodingContainer { var codingPath: [CodingKey] = [] var data: JSValue? { containers.data } - let optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy + var options: Options var userInfo: CodingUserInfo = [:] fileprivate var containers: [EncodingContainer] = [] - init(optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy) { - self.optionalEncodingStrategy = optionalEncodingStrategy + init(options: Options) { + self.options = options } } @@ -171,22 +228,73 @@ extension _JSValueEncoder: Encoder { } func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { - let container = KeyedContainer(codingPath: codingPath, userInfo: userInfo, optionalEncodingStrategy: optionalEncodingStrategy) + let container = KeyedContainer( + codingPath: codingPath, + userInfo: userInfo, + options: options + ) addContainer(.keyed(.init(container))) return KeyedEncodingContainer(container) } func unkeyedContainer() -> UnkeyedEncodingContainer { - let container = UnkeyedContainer(codingPath: codingPath, userInfo: userInfo, optionalEncodingStrategy: optionalEncodingStrategy) + let container = UnkeyedContainer( + codingPath: codingPath, + userInfo: userInfo, + options: options + ) addContainer(.unkeyed(container)) return container } func singleValueContainer() -> SingleValueEncodingContainer { - let container = SingleValueContainer(codingPath: codingPath, userInfo: userInfo, optionalEncodingStrategy: optionalEncodingStrategy) + let container = SingleValueContainer( + codingPath: codingPath, + userInfo: userInfo, + options: options + ) addContainer(.singleValue(container)) return container } + + fileprivate func encodeGeneric(_ value: T) throws where T: Encodable { + switch value { + case let value as Date: + switch options.dateStrategy { + case .deferredToDate: + try value.encode(to: self) + case .millisecondsSince1970: + try (value.timeIntervalSince1970 * Double(MSEC_PER_SEC)).encode(to: self) + case .secondsSince1970: + try value.timeIntervalSince1970.encode(to: self) + case .iso8601: + let formattedDate = ISO8601DateFormatter().string(from: value) + try formattedDate.encode(to: self) + case .formatted(let formatter): + let formattedDate = formatter.string(from: value) + try formattedDate.encode(to: self) + case .custom(let encode): + try encode(value, self) + @unknown default: + try value.encode(to: self) + } + case let value as URL: + try value.absoluteString.encode(to: self) + case let value as Data: + switch options.dataStrategy { + case .deferredToData: + try value.encode(to: self) + case .base64: + try value.base64EncodedString().encode(to: self) + case .custom(let encode): + try encode(value, self) + @unknown default: + try value.encode(to: self) + } + default: + try value.encode(to: self) + } + } } private final class KeyedContainer where Key: CodingKey { @@ -204,13 +312,13 @@ private final class KeyedContainer where Key: CodingKey { var codingPath: [CodingKey] var userInfo: CodingUserInfo - var optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy + var options: Options private var encodedKeyedValue: [String: EncodedValue]? - init(codingPath: [CodingKey], userInfo: CodingUserInfo, optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy) { + init(codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) { self.codingPath = codingPath self.userInfo = userInfo - self.optionalEncodingStrategy = optionalEncodingStrategy + self.options = options } } @@ -232,8 +340,8 @@ extension KeyedContainer: KeyedEncodingContainerProtocol { } func encode(_ value: T, forKey key: Key) throws where T: Encodable { - let encoder = _JSValueEncoder(optionalEncodingStrategy: optionalEncodingStrategy) - try value.encode(to: encoder) + let encoder = _JSValueEncoder(options: options) + try encoder.encodeGeneric(value) insert(.nestedContainer(encoder), for: key) } @@ -241,7 +349,7 @@ extension KeyedContainer: KeyedEncodingContainerProtocol { // protocol requirement. // swiftlint:disable:next identifier_name func _encodeIfPresent(_ value: T?, forKey key: Key) throws where T: Encodable { - switch optionalEncodingStrategy { + switch options.optionalStrategy { case .explicitNulls: if let value = value { try encode(value, forKey: key) @@ -321,7 +429,7 @@ extension KeyedContainer: KeyedEncodingContainerProtocol { let nestedContainer = KeyedContainer( codingPath: newPath, userInfo: userInfo, - optionalEncodingStrategy: optionalEncodingStrategy + options: options ) insert(.nestedContainer(nestedContainer), for: key) @@ -334,7 +442,7 @@ extension KeyedContainer: KeyedEncodingContainerProtocol { let nestedContainer = UnkeyedContainer( codingPath: codingPath, userInfo: userInfo, - optionalEncodingStrategy: optionalEncodingStrategy + options: options ) insert(.nestedContainer(nestedContainer), for: key) return nestedContainer @@ -345,13 +453,13 @@ extension KeyedContainer: KeyedEncodingContainerProtocol { } func superEncoder() -> Encoder { - let encoder = _JSValueEncoder(optionalEncodingStrategy: optionalEncodingStrategy) + let encoder = _JSValueEncoder(options: options) insert(.nestedContainer(encoder), for: SuperKey.super) return encoder } func superEncoder(forKey key: Key) -> Encoder { - let encoder = _JSValueEncoder(optionalEncodingStrategy: optionalEncodingStrategy) + let encoder = _JSValueEncoder(options: options) insert(.nestedContainer(encoder), for: key) return encoder } @@ -385,13 +493,13 @@ private final class UnkeyedContainer { var codingPath: [CodingKey] var userInfo: CodingUserInfo - var optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy + var options: Options private var encodedUnkeyedValue: [EncodedValue]? - init(codingPath: [CodingKey], userInfo: CodingUserInfo, optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy) { + init(codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) { self.codingPath = codingPath self.userInfo = userInfo - self.optionalEncodingStrategy = optionalEncodingStrategy + self.options = options } } @@ -417,8 +525,8 @@ extension UnkeyedContainer: UnkeyedEncodingContainer { } func encode(_ value: T) throws where T: Encodable { - let encoder = _JSValueEncoder(optionalEncodingStrategy: optionalEncodingStrategy) - try value.encode(to: encoder) + let encoder = _JSValueEncoder(options: options) + try encoder.encodeGeneric(value) append(.nestedContainer(encoder)) } @@ -426,7 +534,7 @@ extension UnkeyedContainer: UnkeyedEncodingContainer { let nestedContainer = UnkeyedContainer( codingPath: codingPath, userInfo: userInfo, - optionalEncodingStrategy: optionalEncodingStrategy + options: options ) append(.nestedContainer(nestedContainer)) return nestedContainer @@ -436,14 +544,14 @@ extension UnkeyedContainer: UnkeyedEncodingContainer { let nestedContainer = KeyedContainer( codingPath: codingPath, userInfo: userInfo, - optionalEncodingStrategy: optionalEncodingStrategy + options: options ) append(.nestedContainer(nestedContainer)) return KeyedEncodingContainer(nestedContainer) } func superEncoder() -> Encoder { - let encoder = _JSValueEncoder(optionalEncodingStrategy: optionalEncodingStrategy) + let encoder = _JSValueEncoder(options: options) append(.nestedContainer(encoder)) return encoder } @@ -457,16 +565,12 @@ private final class SingleValueContainer { var data: JSValue? var codingPath: [CodingKey] var userInfo: CodingUserInfo - var optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy + var options: Options - init( - codingPath: [CodingKey], - userInfo: CodingUserInfo, - optionalEncodingStrategy: JSValueEncoder.OptionalEncodingStrategy - ) { + init(codingPath: [CodingKey], userInfo: CodingUserInfo, options: Options) { self.codingPath = codingPath self.userInfo = userInfo - self.optionalEncodingStrategy = optionalEncodingStrategy + self.options = options } } @@ -484,11 +588,31 @@ extension SingleValueContainer: SingleValueEncodingContainer { } func encode(_ value: Double) throws { - data = value as NSNumber + try encodeFloat(value) + } + + private func encodeFloat(_ value: N) throws where N: FloatingPoint { + if value.isFinite { + data = value as! NSNumber + } else { + switch options.nonConformingFloatStrategy { + case .deferred: + data = value as! NSNumber + case let .convertToString(positiveInfinity: pos, negativeInfinity: neg, nan: nan): + if value == .infinity { data = pos } + if value == -.infinity { data = neg } + if value.isNaN { data = nan } + case .throw: + throw EncodingError.invalidValue( + value, + .init(codingPath: codingPath, debugDescription: "Unable to encode \(value) to JSValue") + ) + } + } } func encode(_ value: Float) throws { - data = value as NSNumber + try encodeFloat(value) } func encode(_ value: Int) throws { @@ -532,8 +656,8 @@ extension SingleValueContainer: SingleValueEncodingContainer { } func encode(_ value: T) throws where T: Encodable { - let encoder = _JSValueEncoder(optionalEncodingStrategy: optionalEncodingStrategy) - try value.encode(to: encoder) + let encoder = _JSValueEncoder(options: options) + try encoder.encodeGeneric(value) data = encoder.data } } diff --git a/ios/Capacitor/CodableTests/DataCodableTests.swift b/ios/Capacitor/CodableTests/DataCodableTests.swift new file mode 100644 index 000000000..e5452e766 --- /dev/null +++ b/ios/Capacitor/CodableTests/DataCodableTests.swift @@ -0,0 +1,156 @@ +// +// DataCodableTests.swift +// CodableTests +// +// Created by Steven Sherry on 9/6/24. +// Copyright © 2024 Drifty Co. All rights reserved. +// + +import XCTest +import Capacitor + +private struct Foo: Codable, Equatable { + var data: Data +} + +private let jsonString = #"{ "key": "value" }"# +private let jsonData = jsonString.data(using: .utf8)! +private let jsonByteArray: [NSNumber] = [123, 32, 34, 107, 101, 121, 34, 58, 32, 34, 118, 97, 108, 117, 101, 34, 32, 125] +private let jsonBase64 = "eyAia2V5IjogInZhbHVlIiB9" + +class JSValueDecoderDataTests: XCTestCase { + func testDecode_data__default_root() throws { + let decoder = JSValueDecoder() + let result = try decoder.decode(Data.self, from: jsonByteArray) + XCTAssertEqual(result, jsonData) + } + + func testDecode_data__default_array() throws { + let decoder = JSValueDecoder() + let result = try decoder.decode([Data].self, from: [jsonByteArray, jsonByteArray]) + XCTAssertEqual(result, [jsonData, jsonData]) + } + + func testDecode_data__default_struct() throws { + let decoder = JSValueDecoder() + let result = try decoder.decode(Foo.self, from: ["data": jsonByteArray]) + XCTAssertEqual(result, .init(data: jsonData)) + } + + func testDecode_data__base64_root() throws { + let decoder = JSValueDecoder(dataDecodingStrategy: .base64) + let result = try decoder.decode(Data.self, from: jsonBase64) + XCTAssertEqual(result, jsonData) + } + + func testDecode_data__base64_array() throws { + let decoder = JSValueDecoder(dataDecodingStrategy: .base64) + let result = try decoder.decode([Data].self, from: [jsonBase64, jsonBase64]) + XCTAssertEqual(result, [jsonData, jsonData]) + } + + func testDecode_data__base64_struct() throws { + let decoder = JSValueDecoder(dataDecodingStrategy: .base64) + let result = try decoder.decode(Foo.self, from: ["data": jsonBase64]) + XCTAssertEqual(result, .init(data: jsonData)) + } + + let customStrategy = JSValueDecoder.DataDecodingStrategy.custom { decoder in + var container = try decoder.unkeyedContainer() + var byteArray: [UInt8] = [] + while !container.isAtEnd { + byteArray.append(try container.decode(UInt8.self)) + } + return Data(byteArray) + } + + func testDecode_data__custom_root() throws { + let decoder = JSValueDecoder(dataDecodingStrategy: customStrategy) + let result = try decoder.decode(Data.self, from: jsonByteArray) + XCTAssertEqual(result, jsonData) + } + + func testDecode_data__custom_array() throws { + let decoder = JSValueDecoder(dataDecodingStrategy: customStrategy) + let result = try decoder.decode([Data].self, from: [jsonByteArray, jsonByteArray]) + XCTAssertEqual(result, [jsonData, jsonData]) + } + + func testDecode_data__custom_struct() throws { + let decoder = JSValueDecoder(dataDecodingStrategy: customStrategy) + let result = try decoder.decode(Foo.self, from: ["data": jsonByteArray]) + XCTAssertEqual(result, .init(data: jsonData)) + } +} + +class JSValueEncoderDataTests: XCTestCase { + func testEncode_data__default_root() throws { + let encoder = JSValueEncoder() + let rawResult = try encoder.encode(jsonData) + let result = try XCTUnwrap(rawResult as? [NSNumber]) + XCTAssertEqual(result, jsonByteArray) + } + + func testEncode_data__default_array() throws { + let encoder = JSValueEncoder() + let rawResult = try encoder.encode([jsonData, jsonData]) + let result = try XCTUnwrap(rawResult as? [[NSNumber]]) + XCTAssertEqual(result, [jsonByteArray, jsonByteArray]) + } + + func testEncode_data__default_struct() throws { + let encoder = JSValueEncoder() + let rawResult = try encoder.encode(Foo(data: jsonData)) + let result = try XCTUnwrap(rawResult as? [String: [NSNumber]]) + XCTAssertEqual(result, ["data": jsonByteArray]) + } + + func testEncode_data__base64_root() throws { + let encoder = JSValueEncoder(dataEncodingStrategy: .base64) + let rawResult = try encoder.encode(jsonData) + let result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, jsonBase64) + } + + func testEncode_data__base64_array() throws { + let encoder = JSValueEncoder(dataEncodingStrategy: .base64) + let rawResult = try encoder.encode([jsonData, jsonData]) + let result = try XCTUnwrap(rawResult as? [String]) + XCTAssertEqual(result, [jsonBase64, jsonBase64]) + } + + func testEncode_data__base64_struct() throws { + let encoder = JSValueEncoder(dataEncodingStrategy: .base64) + let rawResult = try encoder.encode(Foo(data: jsonData)) + let result = try XCTUnwrap(rawResult as? [String: String]) + XCTAssertEqual(result, ["data": jsonBase64]) + } + + let customStrategy = JSValueEncoder.DataEncodingStrategy.custom { data, encoder in + let byteArray = data.map { $0 } + var unkeyedContainer = encoder.unkeyedContainer() + try unkeyedContainer.encode(contentsOf: byteArray) + } + + + func testEncode_data__custom_root() throws { + let encoder = JSValueEncoder(dataEncodingStrategy: customStrategy) + let rawResult = try encoder.encode(jsonData) + let result = try XCTUnwrap(rawResult as? [NSNumber]) + XCTAssertEqual(result, jsonByteArray) + } + + func testEncode_data__custom_array() throws { + let encoder = JSValueEncoder(dataEncodingStrategy: customStrategy) + let rawResult = try encoder.encode([jsonData, jsonData]) + let result = try XCTUnwrap(rawResult as? [[NSNumber]]) + XCTAssertEqual(result, [jsonByteArray, jsonByteArray]) + } + + func testEncode_data__custom_struct() throws { + let encoder = JSValueEncoder(dataEncodingStrategy: customStrategy) + let rawResult = try encoder.encode(Foo(data: jsonData)) + let result = try XCTUnwrap(rawResult as? [String: [NSNumber]]) + XCTAssertEqual(result, ["data": jsonByteArray]) + } +} diff --git a/ios/Capacitor/CodableTests/DateCodableTests.swift b/ios/Capacitor/CodableTests/DateCodableTests.swift new file mode 100644 index 000000000..875e8247d --- /dev/null +++ b/ios/Capacitor/CodableTests/DateCodableTests.swift @@ -0,0 +1,158 @@ +// +// DateCodableTests.swift +// CodableTests +// +// Created by Steven Sherry on 9/6/24. +// Copyright © 2024 Drifty Co. All rights reserved. +// + +import XCTest +import Capacitor + +// Fixture data that all refers to the same Date and Time +private let timeIntervalSinceReferenceDate: TimeInterval = 747268580 +private let referenceDate = Date(timeIntervalSinceReferenceDate: timeIntervalSinceReferenceDate) +private let secondsSince1970 = 1725575780 as Double +private let millisecondsSince1970 = 1725575780000 as Double +private let iso8601 = "2024-09-05T22:36:20Z" + +private let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .long + formatter.timeZone = .init(abbreviation: "CDT") + return formatter +}() +private let formatted = "Sep 5, 2024 at 5:36:20 PM CDT" + +private struct Foo: Codable, Equatable { + var date: Date +} + + +final class JSValueDecoderDateTests: XCTestCase { + func testDecode_date__default() throws { + let reference = timeIntervalSinceReferenceDate + let decoder = JSValueDecoder() + let result = try decoder.decode(Date.self, from: reference) + XCTAssertEqual(result, referenceDate) + } + + func testDecode_date__secondsSince1970() throws { + let decoder = JSValueDecoder(dateDecodingStrategy: .secondsSince1970) + let result = try decoder.decode(Date.self, from: secondsSince1970) + XCTAssertEqual(result, referenceDate) + } + + func testDecode_date__millisecondsSince1970() throws { + let decoder = JSValueDecoder(dateDecodingStrategy: .millisecondsSince1970) + let result = try decoder.decode(Date.self, from: millisecondsSince1970) + XCTAssertEqual(result, referenceDate) + } + + func testDecode_date__iso8601() throws { + let decoder = JSValueDecoder(dateDecodingStrategy: .iso8601) + let result = try decoder.decode(Date.self, from: iso8601) + XCTAssertEqual(result, referenceDate) + } + + func testDecode_date__formatted() throws { + let decoder = JSValueDecoder(dateDecodingStrategy: .formatted(formatter)) + let result = try decoder.decode(Date.self, from: formatted) + XCTAssertEqual(result, referenceDate) + } + + func testDecode_date__custom() throws { + let strategy = JSValueDecoder.DateDecodingStrategy.custom { decoder in + let container = try decoder.singleValueContainer() + let referenceDateString = try container.decode(String.self) + guard let referenceDateSeconds = Double(referenceDateString) else { + throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Unable to decode Double from String")) + } + return Date(timeIntervalSinceReferenceDate: referenceDateSeconds) + } + + let referenceString = "\(timeIntervalSinceReferenceDate)" + let decoder = JSValueDecoder(dateDecodingStrategy: strategy) + let result = try decoder.decode(Date.self, from: referenceString) + XCTAssertEqual(result, referenceDate) + } + + func testDecode_date__array() throws { + let dateArray = [iso8601, iso8601] + let decoder = JSValueDecoder(dateDecodingStrategy: .iso8601) + let result = try decoder.decode([Date].self, from: dateArray) + XCTAssertEqual(result, [referenceDate, referenceDate]) + } + + func testDecode_date__struct() throws { + let value = ["date": iso8601] as JSObject + let decoder = JSValueDecoder(dateDecodingStrategy: .iso8601) + let result = try decoder.decode(Foo.self, from: value) + XCTAssertEqual(result, Foo(date: referenceDate)) + } +} + +final class JSValueEncoderDateTests: XCTestCase { + func testEncode_date__default() throws { + let encoder = JSValueEncoder() + let rawResult = try encoder.encode(referenceDate) + let result = try XCTUnwrap(rawResult as? Double) + XCTAssertEqual(result, timeIntervalSinceReferenceDate) + } + + func testEncode_date__secondsSince1970() throws { + let encoder = JSValueEncoder(dateEncodingStrategy: .secondsSince1970) + let rawResult = try encoder.encode(referenceDate) + let result = try XCTUnwrap(rawResult as? Double) + XCTAssertEqual(result, secondsSince1970) + } + + func testEncode_date__millisecondsSince1970() throws { + let encoder = JSValueEncoder(dateEncodingStrategy: .millisecondsSince1970) + let rawResult = try encoder.encode(referenceDate) + let result = try XCTUnwrap(rawResult as? Double) + XCTAssertEqual(result, millisecondsSince1970) + } + + func testEncode_date__iso8601() throws { + let encoder = JSValueEncoder(dateEncodingStrategy: .iso8601) + let rawResult = try encoder.encode(referenceDate) + let result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, iso8601) + } + + func testEncode_date__formatted() throws { + let encoder = JSValueEncoder(dateEncodingStrategy: .formatted(formatter)) + let rawResult = try encoder.encode(referenceDate) + let result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, formatted) + } + + func testEncode_date__custom() throws { + let strategy = JSValueEncoder.DateEncodingStrategy.custom { date, encoder in + var container = encoder.singleValueContainer() + try container.encode("\(date.timeIntervalSinceReferenceDate)") + } + + let encoder = JSValueEncoder(dateEncodingStrategy: strategy) + let rawResult = try encoder.encode(referenceDate) + let result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, "\(timeIntervalSinceReferenceDate)") + } + + func testEncode_date__array() throws { + let encoder = JSValueEncoder(dateEncodingStrategy: .iso8601) + let array = [referenceDate, referenceDate] + let rawResult = try encoder.encode(array) + let result = try XCTUnwrap(rawResult as? [String]) + XCTAssertEqual(result, [iso8601, iso8601]) + } + + func testEncode_date__struct() throws { + let encoder = JSValueEncoder(dateEncodingStrategy: .iso8601) + let rawResult = try encoder.encode(Foo(date: referenceDate)) + let result = try XCTUnwrap(rawResult as? [String: String]) + XCTAssertEqual(result, ["date": iso8601]) + } +} diff --git a/ios/Capacitor/CodableTests/NonconformingFloatCodableTests.swift b/ios/Capacitor/CodableTests/NonconformingFloatCodableTests.swift new file mode 100644 index 000000000..c16996c4f --- /dev/null +++ b/ios/Capacitor/CodableTests/NonconformingFloatCodableTests.swift @@ -0,0 +1,174 @@ +// +// NonconformingFloatCodableTests.swift +// CodableTests +// +// Created by Steven Sherry on 9/6/24. +// Copyright © 2024 Drifty Co. All rights reserved. +// + +import XCTest +import Capacitor + +private struct Foo: Codable, Equatable { + var number: Double +} + +class JSValueEncoderNonConformingFloatTests: XCTestCase { + func testEncode_float__default_root() throws { + let encoder = JSValueEncoder() + let rawResult = try encoder.encode(Double.infinity) + let result = try XCTUnwrap(rawResult as? Double) + XCTAssertEqual(result, .infinity) + } + + func testEncode_float__default_array() throws { + let encoder = JSValueEncoder() + let rawResult = try encoder.encode([Double.infinity, -.infinity, .nan]) + let result = try XCTUnwrap(rawResult as? [Double]) + XCTAssertEqual(result[0...1], [.infinity, -.infinity]) + XCTAssertTrue(result[2].isNaN) + } + + + func testEncode_float__default_struct() throws { + let encoder = JSValueEncoder() + let rawResult = try encoder.encode(Foo.init(number: .infinity)) + let result = try XCTUnwrap(rawResult as? [String: Double]) + XCTAssertEqual(result, ["number": .infinity]) + } + + func testEncode_float__convertToString_root() throws { + let encoder = JSValueEncoder( + nonConformingFloatEncodingStategy: .convertToString( + positiveInfinity: "pos", + negativeInfinity: "neg", + nan: "nan" + ) + ) + + var rawResult = try encoder.encode(Double.infinity) + var result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, "pos") + + rawResult = try encoder.encode(-Double.infinity) + result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, "neg") + + rawResult = try encoder.encode(Double.nan) + result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, "nan") + } + + func testEncode_float__convertToString_array() throws { + let encoder = JSValueEncoder( + nonConformingFloatEncodingStategy: .convertToString( + positiveInfinity: "pos", + negativeInfinity: "neg", + nan: "nan" + ) + ) + + let rawResult = try encoder.encode([Double.infinity, -.infinity, .nan]) + let result = try XCTUnwrap(rawResult as? [String]) + XCTAssertEqual(result, ["pos", "neg", "nan"]) + } + + func testEncode_float__convertToString_struct() throws { + let encoder = JSValueEncoder( + nonConformingFloatEncodingStategy: .convertToString( + positiveInfinity: "pos", + negativeInfinity: "neg", + nan: "nan" + ) + ) + + var rawResult = try encoder.encode(Foo(number: .infinity)) + var result = try XCTUnwrap(rawResult as? [String: String]) + XCTAssertEqual(result, ["number": "pos"]) + + rawResult = try encoder.encode(Foo(number: -.infinity)) + result = try XCTUnwrap(rawResult as? [String: String]) + XCTAssertEqual(result, ["number": "neg"]) + + rawResult = try encoder.encode(Foo(number: .nan)) + result = try XCTUnwrap(rawResult as? [String: String]) + XCTAssertEqual(result, ["number": "nan"]) + } + + func testEncode_float__throw_root() throws { + let encoder = JSValueEncoder(nonConformingFloatEncodingStategy: .throw) + XCTAssertThrowsError(try encoder.encode(Double.infinity)) + } + + func testEncode_float__throw_array() throws { + let encoder = JSValueEncoder(nonConformingFloatEncodingStategy: .throw) + XCTAssertThrowsError(try encoder.encode([Double.infinity, -.infinity, .nan])) + } + + func testEncode_float__throw_struct() throws { + let encoder = JSValueEncoder(nonConformingFloatEncodingStategy: .throw) + XCTAssertThrowsError(try encoder.encode(Foo(number: .infinity))) + } +} + +class JSValueDecoderNonConformingFloatTests: XCTestCase { + func testDecode_float__default_root() throws { + let decoder = JSValueDecoder() + let result = try decoder.decode(Double.self, from: Double.infinity) + XCTAssertEqual(result, .infinity) + } + + func testDecode_float__default_array() throws { + let decoder = JSValueDecoder() + let result = try decoder.decode([Double].self, from: [Double.infinity, Double.infinity]) + XCTAssertEqual(result, [.infinity, .infinity]) + } + + func testDecode_float__default_struct() throws { + let decoder = JSValueDecoder() + let result = try decoder.decode(Foo.self, from: ["number": Double.infinity]) + XCTAssertEqual(result, .init(number: .infinity)) + } + + func testDecode_float__throw_root() throws { + let decoder = JSValueDecoder(nonConformingFloatDecodingStrategy: .throw) + XCTAssertThrowsError(try decoder.decode(Double.self, from: Double.infinity)) + } + + func testDecode_float__throw_array() throws { + let decoder = JSValueDecoder(nonConformingFloatDecodingStrategy: .throw) + XCTAssertThrowsError(try decoder.decode([Double].self, from: [Double.infinity, Double.infinity])) + } + + func testDecode_float__throw_struct() throws { + let decoder = JSValueDecoder(nonConformingFloatDecodingStrategy: .throw) + XCTAssertThrowsError(try decoder.decode(Foo.self, from: ["number": Double.infinity])) + } + + func testDecode_float__convertFromString_root() throws { + let decoder = JSValueDecoder(nonConformingFloatDecodingStrategy: .convertFromString(positiveInfinity: "pos", negativeInfinity: "neg", nan: "nan")) + var result = try decoder.decode(Double.self, from: "pos") + XCTAssertEqual(result, .infinity) + result = try decoder.decode(Double.self, from: "neg") + XCTAssertEqual(result, -.infinity) + result = try decoder.decode(Double.self, from: "nan") + XCTAssertTrue(result.isNaN) + } + + func testDecode_float__convertFromString_array() throws { + let decoder = JSValueDecoder(nonConformingFloatDecodingStrategy: .convertFromString(positiveInfinity: "pos", negativeInfinity: "neg", nan: "nan")) + let result = try decoder.decode([Double].self, from: ["pos", "neg", "nan"]) + XCTAssertEqual(result[0...1], [.infinity, -.infinity]) + XCTAssertTrue(result[2].isNaN) + } + + func testDecode_float__convertFromString_struct() throws { + let decoder = JSValueDecoder(nonConformingFloatDecodingStrategy: .convertFromString(positiveInfinity: "pos", negativeInfinity: "neg", nan: "nan")) + var result = try decoder.decode(Foo.self, from: ["number": "pos"]) + XCTAssertEqual(result, .init(number: .infinity)) + result = try decoder.decode(Foo.self, from: ["number": "neg"]) + XCTAssertEqual(result, .init(number: -.infinity)) + result = try decoder.decode(Foo.self, from: ["number":"nan"]) + XCTAssertTrue(result.number.isNaN) + } +} diff --git a/ios/Capacitor/CodableTests/URLCodableTests.swift b/ios/Capacitor/CodableTests/URLCodableTests.swift new file mode 100644 index 000000000..33a739a31 --- /dev/null +++ b/ios/Capacitor/CodableTests/URLCodableTests.swift @@ -0,0 +1,62 @@ +// +// URLCodableTests.swift +// CodableTests +// +// Created by Steven Sherry on 9/6/24. +// Copyright © 2024 Drifty Co. All rights reserved. +// + +import XCTest +import Capacitor + +private let urlString = "https://capacitorjs.com" +private let url = URL(string: urlString)! + +private struct Website: Codable, Equatable { + var url: URL +} + +class JSValueDecoderURLTests: XCTestCase { + let decoder = JSValueDecoder() + + func testDecode_url__root() throws { + let result = try decoder.decode(URL.self, from: urlString) + XCTAssertEqual(result, url) + } + + func testDecode_url__array() throws { + let result = try decoder.decode([URL].self, from: [urlString, urlString]) + XCTAssertEqual(result, [url, url]) + } + + func testDecode_url__struct() throws { + let result = try decoder.decode(Website.self, from: ["url": urlString]) + XCTAssertEqual(result, .init(url: url)) + } + + func testDecode_url__fails_when_invalid_url_string_is_provided() { + XCTAssertThrowsError(try decoder.decode(URL.self, from: "🐞://🐞.com/🐞")) + } +} + +class JSValueEncoderURLTests: XCTestCase { + let encoder = JSValueEncoder() + + func testEncode_url__root() throws { + let rawResult = try encoder.encode(url) + let result = try XCTUnwrap(rawResult as? String) + XCTAssertEqual(result, urlString) + } + + func testEncode_url__array() throws { + let rawResult = try encoder.encode([url, url]) + let result = try XCTUnwrap(rawResult as? [String]) + XCTAssertEqual(result, [urlString, urlString]) + } + + func testEncode_url__struct() throws { + let rawResult = try encoder.encode(Website(url: url)) + let result = try XCTUnwrap(rawResult as? [String: String]) + XCTAssertEqual(result, ["url": urlString]) + } +} From 912d532cf6c7424d180b185120e7a9ba9c1ae050 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Fri, 13 Sep 2024 09:54:22 +0200 Subject: [PATCH 3/3] feat(cli)!: Remove deprecated bundledWebRuntime (#7655) --- cli/src/config.ts | 14 -------------- cli/src/declarations.ts | 15 --------------- cli/src/definitions.ts | 6 ------ cli/src/index.ts | 8 +------- cli/src/tasks/copy.ts | 3 --- cli/src/web/copy.ts | 30 ------------------------------ 6 files changed, 1 insertion(+), 75 deletions(-) delete mode 100644 cli/src/web/copy.ts diff --git a/cli/src/config.ts b/cli/src/config.ts index d4207ed97..3cd37a0ea 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -523,17 +523,3 @@ const config: CapacitorConfig = ${formatJSObject(extConfig)}; export default config;\n`; } - -export function checkExternalConfig(config: ExtConfigPairs): void { - if (typeof config.extConfig.bundledWebRuntime !== 'undefined') { - let actionMessage = `Can be safely deleted.`; - if (config.extConfig.bundledWebRuntime === true) { - actionMessage = `Please, use a bundler to bundle Capacitor and its plugins.`; - } - logger.warn( - `The ${c.strong( - 'bundledWebRuntime', - )} configuration option has been deprecated. ${actionMessage}`, - ); - } -} diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 3f61213fc..c12a66618 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -29,21 +29,6 @@ export interface CapacitorConfig { */ webDir?: string; - /** - * Whether to copy the Capacitor runtime bundle or not. - * - * If your app is not using a bundler, set this to `true`, then Capacitor - * will create a `capacitor.js` file that you'll need to add as a script in - * your `index.html` file. - * - * It's deprecated and will be removed in Capacitor 6 - * - * @since 1.0.0 - * @deprecated 5.0.0 - * @default false - */ - bundledWebRuntime?: boolean; - /** * The build configuration (as defined by the native app) under which Capacitor * will send statements to the log system. This applies to log statements in diff --git a/cli/src/definitions.ts b/cli/src/definitions.ts index 8ac32b892..a81278120 100644 --- a/cli/src/definitions.ts +++ b/cli/src/definitions.ts @@ -62,12 +62,6 @@ export interface AppConfig { readonly extConfigName: string; readonly extConfigFilePath: string; readonly extConfig: ExternalConfig; - /** - * Whether to use a bundled web runtime instead of relying on a bundler/module - * loader. If you're not using something like rollup or webpack or dynamic ES - * module imports, set this to "true" and import "capacitor.js" manually. - */ - readonly bundledWebRuntime?: boolean; } export interface AndroidConfig extends PlatformConfig { diff --git a/cli/src/index.ts b/cli/src/index.ts index 9a9ca587c..10aa330c1 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -2,7 +2,7 @@ import { Option, program } from 'commander'; import { resolve } from 'path'; import c from './colors'; -import { checkExternalConfig, loadConfig } from './config'; +import { loadConfig } from './config'; import type { Config } from './definitions'; import { fatal, isFatal } from './errors'; import { receive } from './ipc'; @@ -96,7 +96,6 @@ export function runProgram(config: Config): void { .action( wrapAction( telemetryAction(config, async (platform, { deployment, inline }) => { - checkExternalConfig(config.app); const { syncCommand } = await import('./tasks/sync'); await syncCommand(config, platform, deployment, inline); }), @@ -117,7 +116,6 @@ export function runProgram(config: Config): void { .action( wrapAction( telemetryAction(config, async (platform, { deployment }) => { - checkExternalConfig(config.app); const { updateCommand } = await import('./tasks/update'); await updateCommand(config, platform, deployment); }), @@ -135,7 +133,6 @@ export function runProgram(config: Config): void { .action( wrapAction( telemetryAction(config, async (platform, { inline }) => { - checkExternalConfig(config.app); const { copyCommand } = await import('./tasks/copy'); await copyCommand(config, platform, inline); }), @@ -283,7 +280,6 @@ export function runProgram(config: Config): void { .action( wrapAction( telemetryAction(config, async (platform, { packagemanager }) => { - checkExternalConfig(config.app); const { addCommand } = await import('./tasks/add'); const configWritable: Writable = config as Writable; @@ -307,7 +303,6 @@ export function runProgram(config: Config): void { .action( wrapAction( telemetryAction(config, async platform => { - checkExternalConfig(config.app); const { listCommand } = await import('./tasks/list'); await listCommand(config, platform); }), @@ -320,7 +315,6 @@ export function runProgram(config: Config): void { .action( wrapAction( telemetryAction(config, async platform => { - checkExternalConfig(config.app); const { doctorCommand } = await import('./tasks/doctor'); await doctorCommand(config, platform); }), diff --git a/cli/src/tasks/copy.ts b/cli/src/tasks/copy.ts index 5513398d7..a1b7c4835 100644 --- a/cli/src/tasks/copy.ts +++ b/cli/src/tasks/copy.ts @@ -23,7 +23,6 @@ import { logger } from '../log'; import { getPlugins } from '../plugin'; import { generateIOSPackageJSON } from '../util/iosplugin'; import { allSerial } from '../util/promise'; -import { copyWeb } from '../web/copy'; import { inlineSourceMaps } from './sourcemaps'; @@ -180,8 +179,6 @@ export async function copy( logger.info( 'FederatedCapacitor Plugin installed, skipping web bundling...', ); - } else { - await copyWeb(config); } } else { throw `Platform ${platformName} is not valid.`; diff --git a/cli/src/web/copy.ts b/cli/src/web/copy.ts deleted file mode 100644 index b75ebedbd..000000000 --- a/cli/src/web/copy.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { copy } from '@ionic/utils-fs'; -import { join } from 'path'; - -import c from '../colors'; -import { runTask } from '../common'; -import type { Config } from '../definitions'; -import { fatal } from '../errors'; -import { resolveNode } from '../util/node'; - -export async function copyWeb(config: Config): Promise { - if (config.app.bundledWebRuntime) { - const runtimePath = resolveNode( - config.app.rootDir, - '@capacitor/core', - 'dist', - 'capacitor.js', - ); - if (!runtimePath) { - fatal( - `Unable to find ${c.strong( - 'node_modules/@capacitor/core/dist/capacitor.js', - )}.\n` + `Are you sure ${c.strong('@capacitor/core')} is installed?`, - ); - } - - return runTask(`Copying ${c.strong('capacitor.js')} to web dir`, () => { - return copy(runtimePath, join(config.app.webDirAbs, 'capacitor.js')); - }); - } -}