FunctionsSerializer.swift 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // Copyright 2022 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. extension FunctionsSerializer {
  16. enum Error: Swift.Error {
  17. case unsupportedType(typeName: String)
  18. case failedToParseWrappedNumber(value: String, type: String)
  19. }
  20. }
  21. final class FunctionsSerializer: Sendable {
  22. // MARK: - Internal APIs
  23. // This function only supports the following types and will otherwise throw
  24. // an error.
  25. // - NSNull (note: `nil` collection values from a Swift caller will be treated as NSNull)
  26. // - NSNumber
  27. // - NSString
  28. // - NSDicionary
  29. // - NSArray
  30. func encode(_ object: Any) throws -> Any {
  31. if object is NSNull {
  32. return object
  33. } else if let number = object as? NSNumber {
  34. return wrapNumberIfNeeded(number)
  35. } else if object is NSString {
  36. return object
  37. } else if let dict = object as? NSDictionary {
  38. let encoded = NSMutableDictionary()
  39. try dict.forEach { key, value in
  40. encoded[key] = try encode(value)
  41. }
  42. return encoded
  43. } else if let array = object as? NSArray {
  44. return try array.map { element in
  45. try encode(element)
  46. }
  47. } else {
  48. throw Error.unsupportedType(typeName: typeName(of: object))
  49. }
  50. }
  51. // This function only supports the following types and will otherwise throw
  52. // an error.
  53. // - NSNull (note: `nil` collection values from a Swift caller will be treated as NSNull)
  54. // - NSNumber
  55. // - NSString
  56. // - NSDicionary
  57. // - NSArray
  58. func decode(_ object: Any) throws -> Any {
  59. // Return these types as is. PORTING NOTE: Moved from the bottom of the func for readability.
  60. if let dict = object as? NSDictionary {
  61. if let wrappedNumber = WrappedNumber(from: dict) {
  62. return try unwrapNumber(wrappedNumber)
  63. }
  64. let decoded = NSMutableDictionary()
  65. try dict.forEach { key, value in
  66. decoded[key] = try decode(value)
  67. }
  68. return decoded
  69. } else if let array = object as? NSArray {
  70. let decoded = NSMutableArray(capacity: array.count)
  71. try array.forEach { element in
  72. try decoded.add(decode(element) as Any)
  73. }
  74. return decoded
  75. } else if object is NSNumber || object is NSString || object is NSNull {
  76. return object as AnyObject
  77. }
  78. throw Error.unsupportedType(typeName: typeName(of: object))
  79. }
  80. // MARK: - Private Helpers
  81. private func typeName(of value: Any) -> String {
  82. String(describing: type(of: value))
  83. }
  84. private func wrapNumberIfNeeded(_ number: NSNumber) -> Any {
  85. switch String(cString: number.objCType) {
  86. case "q":
  87. // "long long" might be larger than JS supports, so make it a string:
  88. return WrappedNumber(type: .long, value: "\(number)").encoded
  89. case "Q":
  90. // "unsigned long long" might be larger than JS supports, so make it a string:
  91. return WrappedNumber(type: .unsignedLong, value: "\(number)").encoded
  92. default:
  93. // All other types should fit JS limits, so return the number as is:
  94. return number
  95. }
  96. }
  97. private func unwrapNumber(_ wrapped: WrappedNumber) throws(Error) -> any Numeric {
  98. switch wrapped.type {
  99. case .long:
  100. guard let n = Int(wrapped.value) else {
  101. throw .failedToParseWrappedNumber(
  102. value: wrapped.value,
  103. type: wrapped.type.rawValue
  104. )
  105. }
  106. return n
  107. case .unsignedLong:
  108. guard let n = UInt(wrapped.value) else {
  109. throw .failedToParseWrappedNumber(
  110. value: wrapped.value,
  111. type: wrapped.type.rawValue
  112. )
  113. }
  114. return n
  115. }
  116. }
  117. }
  118. // MARK: - WrappedNumber
  119. extension FunctionsSerializer {
  120. private struct WrappedNumber {
  121. let type: NumberType
  122. let value: String
  123. // When / if objects are encoded / decoded using `Codable`,
  124. // these two `init`s and `encoded` won’t be needed anymore:
  125. init(type: NumberType, value: String) {
  126. self.type = type
  127. self.value = value
  128. }
  129. init?(from dictionary: NSDictionary) {
  130. guard
  131. let typeString = dictionary["@type"] as? String,
  132. let type = NumberType(rawValue: typeString),
  133. let value = dictionary["value"] as? String
  134. else {
  135. return nil
  136. }
  137. self.init(type: type, value: value)
  138. }
  139. var encoded: [String: String] {
  140. ["@type": type.rawValue, "value": value]
  141. }
  142. enum NumberType: String {
  143. case long = "type.googleapis.com/google.protobuf.Int64Value"
  144. case unsignedLong = "type.googleapis.com/google.protobuf.UInt64Value"
  145. }
  146. }
  147. }