Quellcode durchsuchen

Moved Database.Encoder/Decoder to a new target and rebranded as StructureEncoder/Decoder

Morten Bek Ditlevsen vor 4 Jahren
Ursprung
Commit
36d8d89976

+ 13 - 0
FirebaseDatabaseSwift/Sources/Codable/EncoderDecoder.swift

@@ -0,0 +1,13 @@
+//
+//  File.swift
+//  
+//
+//  Created by Morten Bek Ditlevsen on 23/10/2021.
+//
+
+import FirebaseDatabase
+
+extension Database {
+  public typealias Encoder = StructureEncoder
+  public typealias Decoder = StructureDecoder
+}

+ 1 - 0
FirebaseSharedSwift/CHANGELOG.md

@@ -0,0 +1 @@
+

+ 0 - 0
FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/LICENSE → FirebaseSharedSwift/Sources/third_party/StructureEncoder/LICENSE


+ 0 - 0
FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/METADATA → FirebaseSharedSwift/Sources/third_party/StructureEncoder/METADATA


+ 308 - 313
FirebaseDatabaseSwift/Sources/third_party/RTDBEncoder/RTDBEncoder.swift → FirebaseSharedSwift/Sources/third_party/StructureEncoder/StructureEncoder.swift

@@ -12,7 +12,6 @@
 //
 //===----------------------------------------------------------------------===//
 
-import FirebaseDatabase
 import Foundation
 
 extension DecodingError {
@@ -96,188 +95,186 @@ extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String,
 // used in the new runtime. _TtC10Foundation13__JSONEncoder is the
 // mangled name for Foundation.__JSONEncoder.
 
-extension Database {
-  public class Encoder {
-    // MARK: Options
-
-    /// The strategy to use for encoding `Date` values.
-    public enum DateEncodingStrategy {
-      /// Defer to `Date` for choosing an encoding. This is the default strategy.
-      case deferredToDate
-
-      /// Encode the `Date` as a UNIX timestamp (as a JSON number).
-      case secondsSince1970
-
-      /// Encode the `Date` as UNIX millisecond timestamp (as a JSON number).
-      case millisecondsSince1970
-
-      /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
-      @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
-      case iso8601
-
-      /// Encode the `Date` as a string formatted by the given formatter.
-      case formatted(DateFormatter)
-
-      /// Encode the `Date` as a custom value encoded by the given closure.
-      ///
-      /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
-      case custom((Date, Swift.Encoder) throws -> Void)
-    }
-
-    /// The strategy to use for encoding `Data` values.
-    public enum DataEncodingStrategy {
-      /// Defer to `Data` for choosing an encoding.
-      case deferredToData
-
-      /// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
-      case base64
-
-      /// Encode the `Data` as a custom value encoded by the given closure.
-      ///
-      /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
-      case custom((Data, Swift.Encoder) throws -> Void)
-    }
-
-    /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
-    public enum NonConformingFloatEncodingStrategy {
-      /// Throw upon encountering non-conforming values. This is the default strategy.
-      case `throw`
-
-      /// Encode the values using the given representation strings.
-      case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
-    }
-
-    /// The strategy to use for automatically changing the value of keys before encoding.
-    public enum KeyEncodingStrategy {
-      /// Use the keys specified by each type. This is the default strategy.
-      case useDefaultKeys
-
-      /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload.
-      ///
-      /// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
-      /// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
-      ///
-      /// Converting from camel case to snake case:
-      /// 1. Splits words at the boundary of lower-case to upper-case
-      /// 2. Inserts `_` between words
-      /// 3. Lowercases the entire string
-      /// 4. Preserves starting and ending `_`.
-      ///
-      /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
-      ///
-      /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
-      case convertToSnakeCase
-
-      /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types.
-      /// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding.
-      /// If the result of the conversion is a duplicate key, then only one value will be present in the result.
-      case custom((_ codingPath: [CodingKey]) -> CodingKey)
-
-      fileprivate static func _convertToSnakeCase(_ stringKey: String) -> String {
-        guard !stringKey.isEmpty else { return stringKey }
-
-        var words : [Range<String.Index>] = []
-        // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
-        //
-        // myProperty -> my_property
-        // myURLProperty -> my_url_property
-        //
-        // We assume, per Swift naming conventions, that the first character of the key is lowercase.
-        var wordStart = stringKey.startIndex
-        var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
-
-        // Find next uppercase character
-        while let upperCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
-          let untilUpperCase = wordStart..<upperCaseRange.lowerBound
-          words.append(untilUpperCase)
-
-          // Find next lowercase character
-          searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
-          guard let lowerCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
-            // There are no more lower case letters. Just end here.
-            wordStart = searchRange.lowerBound
-            break
-          }
-
-          // Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase letters that we should treat as its own word
-          let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
-          if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
-            // The next character after capital is a lower case character and therefore not a word boundary.
-            // Continue searching for the next upper case for the boundary.
-            wordStart = upperCaseRange.lowerBound
-          } else {
-            // There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
-            let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
-            words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
-
-            // Next word starts at the capital before the lowercase we just found
-            wordStart = beforeLowerIndex
-          }
-          searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
+public class StructureEncoder {
+  // MARK: Options
+
+  /// The strategy to use for encoding `Date` values.
+  public enum DateEncodingStrategy {
+    /// Defer to `Date` for choosing an encoding. This is the default strategy.
+    case deferredToDate
+
+    /// Encode the `Date` as a UNIX timestamp (as a JSON number).
+    case secondsSince1970
+
+    /// Encode the `Date` as UNIX millisecond timestamp (as a JSON number).
+    case millisecondsSince1970
+
+    /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
+    @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
+    case iso8601
+
+    /// Encode the `Date` as a string formatted by the given formatter.
+    case formatted(DateFormatter)
+
+    /// Encode the `Date` as a custom value encoded by the given closure.
+    ///
+    /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
+    case custom((Date, Swift.Encoder) throws -> Void)
+  }
+
+  /// The strategy to use for encoding `Data` values.
+  public enum DataEncodingStrategy {
+    /// Defer to `Data` for choosing an encoding.
+    case deferredToData
+
+    /// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
+    case base64
+
+    /// Encode the `Data` as a custom value encoded by the given closure.
+    ///
+    /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
+    case custom((Data, Swift.Encoder) throws -> Void)
+  }
+
+  /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
+  public enum NonConformingFloatEncodingStrategy {
+    /// Throw upon encountering non-conforming values. This is the default strategy.
+    case `throw`
+
+    /// Encode the values using the given representation strings.
+    case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
+  }
+
+  /// The strategy to use for automatically changing the value of keys before encoding.
+  public enum KeyEncodingStrategy {
+    /// Use the keys specified by each type. This is the default strategy.
+    case useDefaultKeys
+
+    /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload.
+    ///
+    /// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
+    /// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
+    ///
+    /// Converting from camel case to snake case:
+    /// 1. Splits words at the boundary of lower-case to upper-case
+    /// 2. Inserts `_` between words
+    /// 3. Lowercases the entire string
+    /// 4. Preserves starting and ending `_`.
+    ///
+    /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
+    ///
+    /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
+    case convertToSnakeCase
+
+    /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types.
+    /// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding.
+    /// If the result of the conversion is a duplicate key, then only one value will be present in the result.
+    case custom((_ codingPath: [CodingKey]) -> CodingKey)
+
+    fileprivate static func _convertToSnakeCase(_ stringKey: String) -> String {
+      guard !stringKey.isEmpty else { return stringKey }
+
+      var words : [Range<String.Index>] = []
+      // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
+      //
+      // myProperty -> my_property
+      // myURLProperty -> my_url_property
+      //
+      // We assume, per Swift naming conventions, that the first character of the key is lowercase.
+      var wordStart = stringKey.startIndex
+      var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
+
+      // Find next uppercase character
+      while let upperCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
+        let untilUpperCase = wordStart..<upperCaseRange.lowerBound
+        words.append(untilUpperCase)
+
+        // Find next lowercase character
+        searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
+        guard let lowerCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
+          // There are no more lower case letters. Just end here.
+          wordStart = searchRange.lowerBound
+          break
+        }
+
+        // Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase letters that we should treat as its own word
+        let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
+        if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
+          // The next character after capital is a lower case character and therefore not a word boundary.
+          // Continue searching for the next upper case for the boundary.
+          wordStart = upperCaseRange.lowerBound
+        } else {
+          // There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
+          let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
+          words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
+
+          // Next word starts at the capital before the lowercase we just found
+          wordStart = beforeLowerIndex
         }
-        words.append(wordStart..<searchRange.upperBound)
-        let result = words.map({ (range) in
-          return stringKey[range].lowercased()
-        }).joined(separator: "_")
-        return result
+        searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
       }
+      words.append(wordStart..<searchRange.upperBound)
+      let result = words.map({ (range) in
+        return stringKey[range].lowercased()
+      }).joined(separator: "_")
+      return result
     }
+  }
 
-    /// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
-    open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
+  /// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
+  open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
 
-    /// The strategy to use in encoding binary data. Defaults to `.base64`.
-    open var dataEncodingStrategy: DataEncodingStrategy = .base64
+  /// The strategy to use in encoding binary data. Defaults to `.base64`.
+  open var dataEncodingStrategy: DataEncodingStrategy = .base64
 
-    /// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`.
-    open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw
+  /// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`.
+  open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw
 
-    /// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
-    open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
+  /// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
+  open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
 
-    /// Contextual user-provided information for use during encoding.
-    open var userInfo: [CodingUserInfoKey : Any] = [:]
+  /// Contextual user-provided information for use during encoding.
+  open var userInfo: [CodingUserInfoKey : Any] = [:]
 
-    /// Options set on the top-level encoder to pass down the encoding hierarchy.
-    fileprivate struct _Options {
-      let dateEncodingStrategy: DateEncodingStrategy
-      let dataEncodingStrategy: DataEncodingStrategy
-      let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
-      let keyEncodingStrategy: KeyEncodingStrategy
-      let userInfo: [CodingUserInfoKey : Any]
-    }
+  /// Options set on the top-level encoder to pass down the encoding hierarchy.
+  fileprivate struct _Options {
+    let dateEncodingStrategy: DateEncodingStrategy
+    let dataEncodingStrategy: DataEncodingStrategy
+    let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
+    let keyEncodingStrategy: KeyEncodingStrategy
+    let userInfo: [CodingUserInfoKey : Any]
+  }
 
-    /// The options set on the top-level encoder.
-    fileprivate var options: _Options {
-      return _Options(dateEncodingStrategy: dateEncodingStrategy,
-                      dataEncodingStrategy: dataEncodingStrategy,
-                      nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy,
-                      keyEncodingStrategy: keyEncodingStrategy,
-                      userInfo: userInfo)
-    }
+  /// The options set on the top-level encoder.
+  fileprivate var options: _Options {
+    return _Options(dateEncodingStrategy: dateEncodingStrategy,
+                    dataEncodingStrategy: dataEncodingStrategy,
+                    nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy,
+                    keyEncodingStrategy: keyEncodingStrategy,
+                    userInfo: userInfo)
+  }
 
-    // MARK: - Constructing a JSON Encoder
+  // MARK: - Constructing a JSON Encoder
 
-    /// Initializes `self` with default strategies.
-    public init() {}
+  /// Initializes `self` with default strategies.
+  public init() {}
 
-    // MARK: - Encoding Values
+  // MARK: - Encoding Values
 
-    /// Encodes the given top-level value and returns its JSON representation.
-    ///
-    /// - parameter value: The value to encode.
-    /// - returns: A new `Data` value containing the encoded JSON data.
-    /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
-    /// - throws: An error if any value throws an error during encoding.
-    open func encode<T : Encodable>(_ value: T) throws -> Any {
-      let encoder = __JSONEncoder(options: self.options)
-
-      guard let topLevel = try encoder.box_(value) else {
-        throw Swift.EncodingError.invalidValue(value,
-                                               Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
-      }
-      return topLevel
+  /// Encodes the given top-level value and returns its JSON representation.
+  ///
+  /// - parameter value: The value to encode.
+  /// - returns: A new `Data` value containing the encoded JSON data.
+  /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
+  /// - throws: An error if any value throws an error during encoding.
+  open func encode<T : Encodable>(_ value: T) throws -> Any {
+    let encoder = __JSONEncoder(options: self.options)
+
+    guard let topLevel = try encoder.box_(value) else {
+      throw Swift.EncodingError.invalidValue(value,
+                                             Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
     }
+    return topLevel
   }
 }
 
@@ -293,7 +290,7 @@ fileprivate class __JSONEncoder : Encoder {
   fileprivate var storage: _JSONEncodingStorage
 
   /// Options set on the top-level encoder.
-  fileprivate let options: Database.Encoder._Options
+  fileprivate let options: StructureEncoder._Options
 
   /// The path to the current point in encoding.
   public var codingPath: [CodingKey]
@@ -306,7 +303,7 @@ fileprivate class __JSONEncoder : Encoder {
   // MARK: - Initialization
 
   /// Initializes `self` with the given top-level encoder options.
-  fileprivate init(options: Database.Encoder._Options, codingPath: [CodingKey] = []) {
+  fileprivate init(options: StructureEncoder._Options, codingPath: [CodingKey] = []) {
     self.options = options
     self.storage = _JSONEncodingStorage()
     self.codingPath = codingPath
@@ -440,7 +437,7 @@ fileprivate struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingCon
     case .useDefaultKeys:
       return key
     case .convertToSnakeCase:
-      let newKeyString = Database.Encoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue)
+      let newKeyString = StructureEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue)
       return _JSONKey(stringValue: newKeyString, intValue: key.intValue)
     case .custom(let converter):
       return converter(codingPath + [key])
@@ -1042,177 +1039,175 @@ fileprivate class __JSONReferencingEncoder : __JSONEncoder {
 // The two must coexist, so it was renamed. The old name must not be
 // used in the new runtime. _TtC10Foundation13__JSONDecoder is the
 // mangled name for Foundation.__JSONDecoder.
-extension Database {
-  public class Decoder {
-    // MARK: Options
+public class StructureDecoder {
+  // MARK: Options
 
-    /// The strategy to use for decoding `Date` values.
-    public enum DateDecodingStrategy {
-      /// Defer to `Date` for decoding. This is the default strategy.
-      case deferredToDate
+  /// The strategy to use for decoding `Date` values.
+  public enum DateDecodingStrategy {
+    /// Defer to `Date` for decoding. This is the default strategy.
+    case deferredToDate
 
-      /// Decode the `Date` as a UNIX timestamp from a JSON number.
-      case secondsSince1970
+    /// Decode the `Date` as a UNIX timestamp from a JSON number.
+    case secondsSince1970
 
-      /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
-      case millisecondsSince1970
+    /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
+    case millisecondsSince1970
 
-      /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
-      @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
-      case iso8601
+    /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
+    @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
+    case iso8601
 
-      /// Decode the `Date` as a string parsed by the given formatter.
-      case formatted(DateFormatter)
+    /// Decode the `Date` as a string parsed by the given formatter.
+    case formatted(DateFormatter)
 
-      /// Decode the `Date` as a custom value decoded by the given closure.
-      case custom((_ decoder: Swift.Decoder) throws -> Date)
-    }
+    /// Decode the `Date` as a custom value decoded by the given closure.
+    case custom((_ decoder: Swift.Decoder) throws -> Date)
+  }
 
-    /// The strategy to use for decoding `Data` values.
-    public enum DataDecodingStrategy {
-      /// Defer to `Data` for decoding.
-      case deferredToData
+  /// The strategy to use for decoding `Data` values.
+  public enum DataDecodingStrategy {
+    /// Defer to `Data` for decoding.
+    case deferredToData
 
-      /// Decode the `Data` from a Base64-encoded string. This is the default strategy.
-      case base64
+    /// Decode the `Data` from a Base64-encoded string. This is the default strategy.
+    case base64
 
-      /// Decode the `Data` as a custom value decoded by the given closure.
-      case custom((_ decoder: Swift.Decoder) throws -> Data)
-    }
+    /// Decode the `Data` as a custom value decoded by the given closure.
+    case custom((_ decoder: Swift.Decoder) throws -> Data)
+  }
 
-    /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
-    public enum NonConformingFloatDecodingStrategy {
-      /// Throw upon encountering non-conforming values. This is the default strategy.
-      case `throw`
+  /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
+  public enum NonConformingFloatDecodingStrategy {
+    /// Throw upon encountering non-conforming values. This is the default strategy.
+    case `throw`
 
-      /// Decode the values from the given representation strings.
-      case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
-    }
+    /// Decode the values from the given representation strings.
+    case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
+  }
 
-    /// The strategy to use for automatically changing the value of keys before decoding.
-    public enum KeyDecodingStrategy {
-      /// Use the keys specified by each type. This is the default strategy.
-      case useDefaultKeys
+  /// The strategy to use for automatically changing the value of keys before decoding.
+  public enum KeyDecodingStrategy {
+    /// Use the keys specified by each type. This is the default strategy.
+    case useDefaultKeys
 
-      /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type.
-      ///
-      /// The conversion to upper case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
-      ///
-      /// Converting from snake case to camel case:
-      /// 1. Capitalizes the word starting after each `_`
-      /// 2. Removes all `_`
-      /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
-      /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
-      ///
-      /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
-      case convertFromSnakeCase
+    /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type.
+    ///
+    /// The conversion to upper case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
+    ///
+    /// Converting from snake case to camel case:
+    /// 1. Capitalizes the word starting after each `_`
+    /// 2. Removes all `_`
+    /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
+    /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
+    ///
+    /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
+    case convertFromSnakeCase
 
-      /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
-      /// The full path to the current decoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before decoding.
-      /// If the result of the conversion is a duplicate key, then only one value will be present in the container for the type to decode from.
-      case custom((_ codingPath: [CodingKey]) -> CodingKey)
+    /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
+    /// The full path to the current decoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before decoding.
+    /// If the result of the conversion is a duplicate key, then only one value will be present in the container for the type to decode from.
+    case custom((_ codingPath: [CodingKey]) -> CodingKey)
 
-      fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String {
-        guard !stringKey.isEmpty else { return stringKey }
+    fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String {
+      guard !stringKey.isEmpty else { return stringKey }
 
-        // Find the first non-underscore character
-        guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else {
-          // Reached the end without finding an _
-          return stringKey
-        }
+      // Find the first non-underscore character
+      guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else {
+        // Reached the end without finding an _
+        return stringKey
+      }
 
-        // Find the last non-underscore character
-        var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
-        while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
-          stringKey.formIndex(before: &lastNonUnderscore)
-        }
+      // Find the last non-underscore character
+      var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
+      while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
+        stringKey.formIndex(before: &lastNonUnderscore)
+      }
 
-        let keyRange = firstNonUnderscore...lastNonUnderscore
-        let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore
-        let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex
+      let keyRange = firstNonUnderscore...lastNonUnderscore
+      let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore
+      let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex
 
-        let components = stringKey[keyRange].split(separator: "_")
-        let joinedString : String
-        if components.count == 1 {
-          // No underscores in key, leave the word as is - maybe already camel cased
-          joinedString = String(stringKey[keyRange])
-        } else {
-          joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined()
-        }
+      let components = stringKey[keyRange].split(separator: "_")
+      let joinedString : String
+      if components.count == 1 {
+        // No underscores in key, leave the word as is - maybe already camel cased
+        joinedString = String(stringKey[keyRange])
+      } else {
+        joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined()
+      }
 
-        // Do a cheap isEmpty check before creating and appending potentially empty strings
-        let result : String
-        if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) {
-          result = joinedString
-        } else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) {
-          // Both leading and trailing underscores
-          result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange])
-        } else if (!leadingUnderscoreRange.isEmpty) {
-          // Just leading
-          result = String(stringKey[leadingUnderscoreRange]) + joinedString
-        } else {
-          // Just trailing
-          result = joinedString + String(stringKey[trailingUnderscoreRange])
-        }
-        return result
+      // Do a cheap isEmpty check before creating and appending potentially empty strings
+      let result : String
+      if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) {
+        result = joinedString
+      } else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) {
+        // Both leading and trailing underscores
+        result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange])
+      } else if (!leadingUnderscoreRange.isEmpty) {
+        // Just leading
+        result = String(stringKey[leadingUnderscoreRange]) + joinedString
+      } else {
+        // Just trailing
+        result = joinedString + String(stringKey[trailingUnderscoreRange])
       }
+      return result
     }
+  }
 
-    /// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
-    open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate
-
-    /// The strategy to use in decoding binary data. Defaults to `.base64`.
-    open var dataDecodingStrategy: DataDecodingStrategy = .base64
+  /// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
+  open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate
 
-    /// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`.
-    open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw
+  /// The strategy to use in decoding binary data. Defaults to `.base64`.
+  open var dataDecodingStrategy: DataDecodingStrategy = .base64
 
-    /// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
-    open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
+  /// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`.
+  open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw
 
-    /// Contextual user-provided information for use during decoding.
-    open var userInfo: [CodingUserInfoKey : Any] = [:]
+  /// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
+  open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
 
-    /// Options set on the top-level encoder to pass down the decoding hierarchy.
-    fileprivate struct _Options {
-      let dateDecodingStrategy: DateDecodingStrategy
-      let dataDecodingStrategy: DataDecodingStrategy
-      let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
-      let keyDecodingStrategy: KeyDecodingStrategy
-      let userInfo: [CodingUserInfoKey : Any]
-    }
+  /// Contextual user-provided information for use during decoding.
+  open var userInfo: [CodingUserInfoKey : Any] = [:]
 
-    /// The options set on the top-level decoder.
-    fileprivate var options: _Options {
-      return _Options(dateDecodingStrategy: dateDecodingStrategy,
-                      dataDecodingStrategy: dataDecodingStrategy,
-                      nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
-                      keyDecodingStrategy: keyDecodingStrategy,
-                      userInfo: userInfo)
-    }
+  /// Options set on the top-level encoder to pass down the decoding hierarchy.
+  fileprivate struct _Options {
+    let dateDecodingStrategy: DateDecodingStrategy
+    let dataDecodingStrategy: DataDecodingStrategy
+    let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
+    let keyDecodingStrategy: KeyDecodingStrategy
+    let userInfo: [CodingUserInfoKey : Any]
+  }
 
-    // MARK: - Constructing a JSON Decoder
+  /// The options set on the top-level decoder.
+  fileprivate var options: _Options {
+    return _Options(dateDecodingStrategy: dateDecodingStrategy,
+                    dataDecodingStrategy: dataDecodingStrategy,
+                    nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
+                    keyDecodingStrategy: keyDecodingStrategy,
+                    userInfo: userInfo)
+  }
 
-    /// Initializes `self` with default strategies.
-    public init() {}
+  // MARK: - Constructing a JSON Decoder
 
-    // MARK: - Decoding Values
+  /// Initializes `self` with default strategies.
+  public init() {}
 
-    /// Decodes a top-level value of the given type from the given JSON representation.
-    ///
-    /// - parameter type: The type of the value to decode.
-    /// - parameter data: The data to decode from.
-    /// - returns: A value of the requested type.
-    /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
-    /// - throws: An error if any value throws an error during decoding.
-    open func decode<T : Decodable>(_ type: T.Type, from structure: Any) throws -> T {
-      let decoder = __JSONDecoder(referencing: structure, options: self.options)
-      guard let value = try decoder.unbox(structure, as: type) else {
-        throw Swift.DecodingError.valueNotFound(type, Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
-      }
+  // MARK: - Decoding Values
 
-      return value
+  /// Decodes a top-level value of the given type from the given JSON representation.
+  ///
+  /// - parameter type: The type of the value to decode.
+  /// - parameter data: The data to decode from.
+  /// - returns: A value of the requested type.
+  /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
+  /// - throws: An error if any value throws an error during decoding.
+  open func decode<T : Decodable>(_ type: T.Type, from structure: Any) throws -> T {
+    let decoder = __JSONDecoder(referencing: structure, options: self.options)
+    guard let value = try decoder.unbox(structure, as: type) else {
+      throw Swift.DecodingError.valueNotFound(type, Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
     }
+
+    return value
   }
 }
 
@@ -1228,7 +1223,7 @@ fileprivate class __JSONDecoder : Decoder {
   fileprivate var storage: _JSONDecodingStorage
 
   /// Options set on the top-level decoder.
-  fileprivate let options: Database.Decoder._Options
+  fileprivate let options: StructureDecoder._Options
 
   /// The path to the current point in encoding.
   fileprivate(set) public var codingPath: [CodingKey]
@@ -1241,7 +1236,7 @@ fileprivate class __JSONDecoder : Decoder {
   // MARK: - Initialization
 
   /// Initializes `self` with the given top-level container and options.
-  fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: Database.Decoder._Options) {
+  fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: StructureDecoder._Options) {
     self.storage = _JSONDecodingStorage()
     self.storage.push(container: container)
     self.codingPath = codingPath
@@ -1347,7 +1342,7 @@ fileprivate struct _JSONKeyedDecodingContainer<K : CodingKey> : KeyedDecodingCon
       // Convert the snake case keys in the container to camel case.
       // If we hit a duplicate key after conversion, then we'll use the first one we saw. Effectively an undefined behavior with JSON dictionaries.
       self.container = Dictionary(container.map {
-        key, value in (Database.Decoder.KeyDecodingStrategy._convertFromSnakeCase(key), value)
+        key, value in (StructureDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value)
       }, uniquingKeysWith: { (first, _) in first })
     case .custom(let converter):
       self.container = Dictionary(container.map {
@@ -1372,8 +1367,8 @@ fileprivate struct _JSONKeyedDecodingContainer<K : CodingKey> : KeyedDecodingCon
     case .convertFromSnakeCase:
       // In this case we can attempt to recover the original value by reversing the transform
       let original = key.stringValue
-      let converted = Database.Encoder.KeyEncodingStrategy._convertToSnakeCase(original)
-      let roundtrip = Database.Decoder.KeyDecodingStrategy._convertFromSnakeCase(converted)
+      let converted = StructureEncoder.KeyEncodingStrategy._convertToSnakeCase(original)
+      let roundtrip = StructureDecoder.KeyDecodingStrategy._convertFromSnakeCase(converted)
       if converted == original {
         return "\(key) (\"\(original)\")"
       } else if roundtrip == original {

+ 501 - 0
FirebaseSharedSwift/Tests/Codable/StructureEncoderTests.swift

@@ -0,0 +1,501 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+import FirebaseSharedSwift
+import XCTest
+
+class FirebaseStructureEncoderTests: XCTestCase {
+  func testInt() {
+    struct Model: Codable, Equatable {
+      let x: Int
+    }
+    let model = Model(x: 42)
+    let dict = ["x": 42]
+    assertThat(model).roundTrips(to: dict)
+  }
+
+  func testNullDecodesAsNil() throws {
+    let decoder = StructureDecoder()
+    let opt = try decoder.decode(Int?.self, from: NSNull())
+    XCTAssertNil(opt)
+  }
+
+  func testEmpty() {
+    struct Model: Codable, Equatable {}
+    assertThat(Model()).roundTrips(to: [String: Any]())
+  }
+
+  func testString() throws {
+    struct Model: Codable, Equatable {
+      let s: String
+    }
+    assertThat(Model(s: "abc")).roundTrips(to: ["s": "abc"])
+  }
+
+  func testCaseConversion() throws {
+    struct Model: Codable, Equatable {
+      let snakeCase: Int
+    }
+    let model = Model(snakeCase: 42)
+    let dict = ["snake_case": 42]
+    let encoder = StructureEncoder()
+    encoder.keyEncodingStrategy = .convertToSnakeCase
+    let decoder = StructureDecoder()
+    decoder.keyDecodingStrategy = .convertFromSnakeCase
+    assertThat(model).roundTrips(to: dict, using: encoder, decoder: decoder)
+  }
+
+  func testOptional() {
+    struct Model: Codable, Equatable {
+      let x: Int
+      let opt: Int?
+    }
+    assertThat(Model(x: 42, opt: nil)).roundTrips(to: ["x": 42])
+    assertThat(Model(x: 42, opt: 7)).roundTrips(to: ["x": 42, "opt": 7])
+    assertThat(["x": 42, "opt": 5]).decodes(to: Model(x: 42, opt: 5))
+    assertThat(["x": 42, "opt": true]).failsDecoding(to: Model.self)
+    assertThat(["x": 42, "opt": "abc"]).failsDecoding(to: Model.self)
+    assertThat(["x": 45.55, "opt": 5]).failsDecoding(to: Model.self)
+    assertThat(["opt": 5]).failsDecoding(to: Model.self)
+
+    // TODO: - handle encoding keys with nil values
+    // See https://stackoverflow.com/questions/47266862/encode-nil-value-as-null-with-jsonencoder
+    // and https://bugs.swift.org/browse/SR-9232
+    //     XCTAssertTrue(encodedDict.keys.contains("x"))
+  }
+
+  func testEnum() {
+    enum MyEnum: Codable, Equatable {
+      case num(number: Int)
+      case text(String)
+
+      private enum CodingKeys: String, CodingKey {
+        case num
+        case text
+      }
+
+      private enum DecodingError: Error {
+        case decoding(String)
+      }
+
+      init(from decoder: Decoder) throws {
+        let values = try decoder.container(keyedBy: CodingKeys.self)
+        if let value = try? values.decode(Int.self, forKey: .num) {
+          self = .num(number: value)
+          return
+        }
+        if let value = try? values.decode(String.self, forKey: .text) {
+          self = .text(value)
+          return
+        }
+        throw DecodingError.decoding("Decoding error: \(dump(values))")
+      }
+
+      func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        switch self {
+        case let .num(number):
+          try container.encode(number, forKey: .num)
+        case let .text(value):
+          try container.encode(value, forKey: .text)
+        }
+      }
+    }
+    struct Model: Codable, Equatable {
+      let x: Int
+      let e: MyEnum
+    }
+
+    assertThat(Model(x: 42, e: MyEnum.num(number: 4)))
+      .roundTrips(to: ["x": 42, "e": ["num": 4]])
+
+    assertThat(Model(x: 43, e: MyEnum.text("abc")))
+      .roundTrips(to: ["x": 43, "e": ["text": "abc"]])
+  }
+
+  func testBadValue() {
+    struct Model: Codable, Equatable {
+      let x: Int
+    }
+    assertThat(["x": "abc"]).failsDecoding(to: Model.self) // Wrong type
+  }
+
+  func testValueTooBig() {
+    struct Model: Codable, Equatable {
+      let x: CChar
+    }
+    assertThat(Model(x: 42)).roundTrips(to: ["x": 42])
+    assertThat(["x": 12345]).failsDecoding(to: Model.self) // Overflow
+  }
+
+  // Inspired by https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java
+  func testBeans() {
+    struct Model: Codable, Equatable {
+      let s: String
+      let d: Double
+      let f: Float
+      let l: CLongLong
+      let i: Int
+      let b: Bool
+      let sh: CShort
+      let byte: CChar
+      let uchar: CUnsignedChar
+      let ai: [Int]
+      let si: [String]
+      let caseSensitive: String
+      let casESensitive: String
+      let casESensitivE: String
+    }
+    let model = Model(
+      s: "abc",
+      d: 123,
+      f: -4,
+      l: 1_234_567_890_123,
+      i: -4444,
+      b: false,
+      sh: 123,
+      byte: 45,
+      uchar: 44,
+      ai: [1, 2, 3, 4],
+      si: ["abc", "def"],
+      caseSensitive: "aaa",
+      casESensitive: "bbb",
+      casESensitivE: "ccc"
+    )
+    let dict = [
+      "s": "abc",
+      "d": 123,
+      "f": -4,
+      "l": Int64(1_234_567_890_123),
+      "i": -4444,
+      "b": false,
+      "sh": 123,
+      "byte": 45,
+      "uchar": 44,
+      "ai": [1, 2, 3, 4],
+      "si": ["abc", "def"],
+      "caseSensitive": "aaa",
+      "casESensitive": "bbb",
+      "casESensitivE": "ccc",
+    ] as [String: Any]
+
+    assertThat(model).roundTrips(to: dict)
+  }
+
+  func testCodingKeysCanCustomizeEncodingAndDecoding() throws {
+    struct Model: Codable, Equatable {
+      var s: String
+      var ms: String = "filler"
+      var d: Double
+      var md: Double = 42.42
+
+      // Use CodingKeys to only encode part of the struct.
+      enum CodingKeys: String, CodingKey {
+        case s
+        case d
+      }
+    }
+
+    assertThat(Model(s: "abc", ms: "dummy", d: 123.3, md: 0))
+      .encodes(to: ["s": "abc", "d": 123.3])
+      .decodes(to: Model(s: "abc", ms: "filler", d: 123.3, md: 42.42))
+  }
+
+  func testNestedObjects() {
+    struct SecondLevelNestedModel: Codable, Equatable {
+      var age: Int8
+      var weight: Double
+    }
+    struct NestedModel: Codable, Equatable {
+      var group: String
+      var groupList: [SecondLevelNestedModel]
+      var groupMap: [String: SecondLevelNestedModel]
+    }
+    struct Model: Codable, Equatable {
+      var id: Int64
+      var group: NestedModel
+    }
+
+    let model = Model(
+      id: 123,
+      group: NestedModel(
+        group: "g1",
+        groupList: [
+          SecondLevelNestedModel(age: 20, weight: 80.1),
+          SecondLevelNestedModel(age: 25, weight: 85.1),
+        ],
+        groupMap: [
+          "name1": SecondLevelNestedModel(age: 30, weight: 64.2),
+          "name2": SecondLevelNestedModel(age: 35, weight: 79.2),
+        ]
+      )
+    )
+
+    let dict = [
+      "group": [
+        "group": "g1",
+        "groupList": [
+          [
+            "age": 20,
+            "weight": 80.1,
+          ],
+          [
+            "age": 25,
+            "weight": 85.1,
+          ],
+        ],
+        "groupMap": [
+          "name1": [
+            "age": 30,
+            "weight": 64.2,
+          ],
+          "name2": [
+            "age": 35,
+            "weight": 79.2,
+          ],
+        ],
+      ],
+      "id": 123,
+    ] as [String: Any]
+
+    assertThat(model).roundTrips(to: dict)
+  }
+
+  func testCollapsingNestedObjects() {
+    // The model is flat but the document has a nested Map.
+    struct Model: Codable, Equatable {
+      var id: Int64
+      var name: String
+
+      init(id: Int64, name: String) {
+        self.id = id
+        self.name = name
+      }
+
+      private enum CodingKeys: String, CodingKey {
+        case id
+        case nested
+      }
+
+      private enum NestedCodingKeys: String, CodingKey {
+        case name
+      }
+
+      init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        try id = container.decode(Int64.self, forKey: .id)
+
+        let nestedContainer = try container
+          .nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
+        try name = nestedContainer.decode(String.self, forKey: .name)
+      }
+
+      func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(id, forKey: .id)
+        var nestedContainer = container
+          .nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
+        try nestedContainer.encode(name, forKey: .name)
+      }
+    }
+
+    assertThat(Model(id: 12345, name: "ModelName"))
+      .roundTrips(to: [
+        "id": 12345,
+        "nested": ["name": "ModelName"],
+      ])
+  }
+
+  class SuperModel: Codable, Equatable {
+    var superPower: Double? = 100.0
+    var superName: String? = "superName"
+
+    init(power: Double, name: String) {
+      superPower = power
+      superName = name
+    }
+
+    static func == (lhs: SuperModel, rhs: SuperModel) -> Bool {
+      return (lhs.superName == rhs.superName) && (lhs.superPower == rhs.superPower)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+      case superPower
+      case superName
+    }
+
+    required init(from decoder: Decoder) throws {
+      let container = try decoder.container(keyedBy: CodingKeys.self)
+      superPower = try container.decode(Double.self, forKey: .superPower)
+      superName = try container.decode(String.self, forKey: .superName)
+    }
+
+    func encode(to encoder: Encoder) throws {
+      var container = encoder.container(keyedBy: CodingKeys.self)
+      try container.encode(superPower, forKey: .superPower)
+      try container.encode(superName, forKey: .superName)
+    }
+  }
+
+  class SubModel: SuperModel {
+    var timestamp: Double? = 123_456_789.123
+
+    init(power: Double, name: String, seconds: Double) {
+      super.init(power: power, name: name)
+      timestamp = seconds
+    }
+
+    static func == (lhs: SubModel, rhs: SubModel) -> Bool {
+      return ((lhs as SuperModel) == (rhs as SuperModel)) && (lhs.timestamp == rhs.timestamp)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+      case timestamp
+    }
+
+    required init(from decoder: Decoder) throws {
+      let container = try decoder.container(keyedBy: CodingKeys.self)
+      timestamp = try container.decode(Double.self, forKey: .timestamp)
+      try super.init(from: container.superDecoder())
+    }
+
+    override func encode(to encoder: Encoder) throws {
+      var container = encoder.container(keyedBy: CodingKeys.self)
+      try container.encode(timestamp, forKey: .timestamp)
+      try super.encode(to: container.superEncoder())
+    }
+  }
+
+  func testClassHierarchy() {
+    assertThat(SubModel(power: 100, name: "name", seconds: 123_456_789.123))
+      .roundTrips(to: [
+        "super": ["superPower": 100, "superName": "name"],
+        "timestamp": 123_456_789.123,
+      ])
+  }
+}
+
+private func assertThat(_ dictionary: [String: Any],
+                        file: StaticString = #file,
+                        line: UInt = #line) -> DictionarySubject {
+  return DictionarySubject(dictionary, file: file, line: line)
+}
+
+func assertThat<X: Equatable & Codable>(_ model: X, file: StaticString = #file,
+                                        line: UInt = #line) -> CodableSubject<X> {
+  return CodableSubject(model, file: file, line: line)
+}
+
+func assertThat<X: Equatable & Encodable>(_ model: X, file: StaticString = #file,
+                                          line: UInt = #line) -> EncodableSubject<X> {
+  return EncodableSubject(model, file: file, line: line)
+}
+
+class EncodableSubject<X: Equatable & Encodable> {
+  var subject: X
+  var file: StaticString
+  var line: UInt
+
+  init(_ subject: X, file: StaticString, line: UInt) {
+    self.subject = subject
+    self.file = file
+    self.line = line
+  }
+
+  @discardableResult
+  func encodes(to expected: [String: Any],
+               using encoder: StructureEncoder = .init()) -> DictionarySubject {
+    let encoded = assertEncodes(to: expected, using: encoder)
+    return DictionarySubject(encoded, file: file, line: line)
+  }
+
+  func failsToEncode() {
+    do {
+      let encoder = StructureEncoder()
+      encoder.keyEncodingStrategy = .convertToSnakeCase
+      _ = try encoder.encode(subject)
+    } catch {
+      return
+    }
+    XCTFail("Failed to throw")
+  }
+
+  func failsEncodingAtTopLevel() {
+    do {
+      let encoder = StructureEncoder()
+      encoder.keyEncodingStrategy = .convertToSnakeCase
+      _ = try encoder.encode(subject)
+      XCTFail("Failed to throw", file: file, line: line)
+    } catch EncodingError.invalidValue(_, _) {
+      return
+    } catch {
+      XCTFail("Unrecognized error: \(error)", file: file, line: line)
+    }
+  }
+
+  private func assertEncodes(to expected: [String: Any],
+                             using encoder: StructureEncoder = .init()) -> [String: Any] {
+    do {
+      let enc = try encoder.encode(subject)
+      XCTAssertEqual(enc as? NSDictionary, expected as NSDictionary, file: file, line: line)
+      return (enc as! NSDictionary) as! [String: Any]
+    } catch {
+      XCTFail("Failed to encode \(X.self): error: \(error)")
+      return ["": -1]
+    }
+  }
+}
+
+class CodableSubject<X: Equatable & Codable>: EncodableSubject<X> {
+  func roundTrips(to expected: [String: Any],
+                  using encoder: StructureEncoder = .init(),
+                  decoder: StructureDecoder = .init()) {
+    let reverseSubject = encodes(to: expected, using: encoder)
+    reverseSubject.decodes(to: subject, using: decoder)
+  }
+}
+
+class DictionarySubject {
+  var subject: [String: Any]
+  var file: StaticString
+  var line: UInt
+
+  init(_ subject: [String: Any], file: StaticString, line: UInt) {
+    self.subject = subject
+    self.file = file
+    self.line = line
+  }
+
+  func decodes<X: Equatable & Codable>(to expected: X,
+                                       using decoder: StructureDecoder = .init()) -> Void {
+    do {
+      let decoded = try decoder.decode(X.self, from: subject)
+      XCTAssertEqual(decoded, expected)
+    } catch {
+      XCTFail("Failed to decode \(X.self): \(error)", file: file, line: line)
+    }
+  }
+
+  func failsDecoding<X: Equatable & Codable>(to _: X.Type,
+                                             using decoder: StructureDecoder = .init()) -> Void {
+    XCTAssertThrowsError(
+      try decoder.decode(X.self, from: subject),
+      file: file,
+      line: line
+    )
+  }
+}

+ 57 - 58
FirebaseDatabaseSwift/Tests/third_party/EncoderTests.swift → FirebaseSharedSwift/Tests/third_party/EncoderTests.swift

@@ -8,8 +8,7 @@
 //
 //===----------------------------------------------------------------------===//
 
-import FirebaseDatabase
-import FirebaseDatabaseSwift
+import FirebaseSharedSwift
 import Swift
 import Foundation
 
@@ -17,7 +16,7 @@ import Foundation
 
 import XCTest
 
-class TestDatabaseEncoder: XCTestCase {
+class TestStructureEncoder: XCTestCase {
   // MARK: - Encoding Top-Level Empty Types
 
   func testEncodingTopLevelEmptyStruct() {
@@ -216,14 +215,14 @@ class TestDatabaseEncoder: XCTestCase {
     func localTestRoundTrip<T: Codable & Equatable>(of value: T) {
       var payload: Any! = nil
       do {
-        let encoder = Database.Encoder()
+        let encoder = StructureEncoder()
         payload = try encoder.encode(value)
       } catch {
         XCTFail("Failed to encode \(T.self): \(error)")
       }
 
       do {
-        let decoder = Database.Decoder()
+        let decoder = StructureDecoder()
         let decoded = try decoder.decode(T.self, from: payload!)
 
         /// `snprintf`'s `%g`, which `JSONSerialization` uses internally for double values, does not respect
@@ -475,12 +474,12 @@ class TestDatabaseEncoder: XCTestCase {
   }
 
   func testEncodingNonConformingFloatStrings() {
-    let encodingStrategy: Database.Encoder.NonConformingFloatEncodingStrategy = .convertToString(
+    let encodingStrategy: StructureEncoder.NonConformingFloatEncodingStrategy = .convertToString(
       positiveInfinity: "INF",
       negativeInfinity: "-INF",
       nan: "NaN"
     )
-    let decodingStrategy: Database.Decoder.NonConformingFloatDecodingStrategy = .convertFromString(
+    let decodingStrategy: StructureDecoder.NonConformingFloatDecodingStrategy = .convertFromString(
       positiveInfinity: "INF",
       negativeInfinity: "-INF",
       nan: "NaN"
@@ -582,7 +581,7 @@ class TestDatabaseEncoder: XCTestCase {
       let expected = ["\(test.1)": "test"]
       let encoded = EncodeMe(keyName: test.0)
 
-      let encoder = Database.Encoder()
+      let encoder = StructureEncoder()
       encoder.keyEncodingStrategy = .convertToSnakeCase
       let result = try! encoder.encode(encoded)
 
@@ -594,7 +593,7 @@ class TestDatabaseEncoder: XCTestCase {
     let expected = ["QQQhello": "test"]
     let encoded = EncodeMe(keyName: "hello")
 
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in
       let key = _TestKey(stringValue: "QQQ" + path.last!.stringValue)!
       return key
@@ -608,7 +607,7 @@ class TestDatabaseEncoder: XCTestCase {
   func testEncodingDictionaryStringKeyConversionUntouched() {
     let toEncode = ["leaveMeAlone": "test"]
 
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.keyEncodingStrategy = .convertToSnakeCase
     let result = try! encoder.encode(toEncode)
 
@@ -626,7 +625,7 @@ class TestDatabaseEncoder: XCTestCase {
   func testEncodingDictionaryFailureKeyPath() {
     let toEncode: [String: EncodeFailure] = ["key": EncodeFailure(someValue: Double.nan)]
 
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.keyEncodingStrategy = .convertToSnakeCase
     do {
       _ = try encoder.encode(toEncode)
@@ -643,7 +642,7 @@ class TestDatabaseEncoder: XCTestCase {
     let toEncode: [String: [String: EncodeFailureNested]] =
       ["key": ["sub_key": EncodeFailureNested(nestedValue: EncodeFailure(someValue: Double.nan))]]
 
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.keyEncodingStrategy = .convertToSnakeCase
     do {
       _ = try encoder.encode(toEncode)
@@ -673,7 +672,7 @@ class TestDatabaseEncoder: XCTestCase {
     let encoded =
       EncodeNestedNested(outerValue: EncodeNested(nestedValue: EncodeMe(keyName: "helloWorld")))
 
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     var callCount = 0
 
     let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in
@@ -758,7 +757,7 @@ class TestDatabaseEncoder: XCTestCase {
       // This structure contains the camel case key that the test object should decode with, then it uses the snake case key (test.0) as the actual key for the boolean value.
       let input = ["camelCaseKey": "\(test.1)", "\(test.0)": true] as [String: Any]
 
-      let decoder = Database.Decoder()
+      let decoder = StructureDecoder()
       decoder.keyDecodingStrategy = .convertFromSnakeCase
 
       let result = try! decoder.decode(DecodeMe.self, from: input)
@@ -771,7 +770,7 @@ class TestDatabaseEncoder: XCTestCase {
 
   func testDecodingKeyStrategyCustom() {
     let input = ["----hello": "test"]
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     let customKeyConversion = { (_ path: [CodingKey]) -> CodingKey in
       // This converter removes the first 4 characters from the start of all string keys, if it has more than 4 characters
       let string = path.last!.stringValue
@@ -787,7 +786,7 @@ class TestDatabaseEncoder: XCTestCase {
 
   func testDecodingDictionaryStringKeyConversionUntouched() {
     let input = ["leave_me_alone": "test"]
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.keyDecodingStrategy = .convertFromSnakeCase
     let result = try! decoder.decode([String: String].self, from: input)
 
@@ -796,7 +795,7 @@ class TestDatabaseEncoder: XCTestCase {
 
   func testDecodingDictionaryFailureKeyPath() {
     let input = ["leave_me_alone": "test"]
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.keyDecodingStrategy = .convertFromSnakeCase
     do {
       _ = try decoder.decode([String: Int].self, from: input)
@@ -818,7 +817,7 @@ class TestDatabaseEncoder: XCTestCase {
 
   func testDecodingDictionaryFailureKeyPathNested() {
     let input = ["top_level": ["sub_level": ["nested_value": ["int_value": "not_an_int"]]]]
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.keyDecodingStrategy = .convertFromSnakeCase
     do {
       _ = try decoder.decode([String: [String: DecodeFailureNested]].self, from: input)
@@ -840,7 +839,7 @@ class TestDatabaseEncoder: XCTestCase {
   func testEncodingKeyStrategySnakeGenerated() {
     // Test that this works with a struct that has automatically generated keys
     let input = ["this_is_camel_case": "test"]
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.keyDecodingStrategy = .convertFromSnakeCase
     let result = try! decoder.decode(DecodeMe3.self, from: input)
 
@@ -849,7 +848,7 @@ class TestDatabaseEncoder: XCTestCase {
 
   func testDecodingKeyStrategyCamelGenerated() {
     let encoded = DecodeMe3(thisIsCamelCase: "test")
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.keyEncodingStrategy = .convertToSnakeCase
     let result = try! encoder.encode(encoded)
     XCTAssertEqual(["this_is_camel_case": "test"], result as? [String: String])
@@ -868,7 +867,7 @@ class TestDatabaseEncoder: XCTestCase {
 
     // Decoding
     let input = ["foo_bar": "test", "this_is_camel_case_too": "test2"]
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.keyDecodingStrategy = .convertFromSnakeCase
     let decodingResult = try! decoder.decode(DecodeMe4.self, from: input)
 
@@ -877,7 +876,7 @@ class TestDatabaseEncoder: XCTestCase {
 
     // Encoding
     let encoded = DecodeMe4(thisIsCamelCase: "test", thisIsCamelCaseToo: "test2")
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.keyEncodingStrategy = .convertToSnakeCase
     let encodingResult = try! encoder.encode(encoded)
     XCTAssertEqual(
@@ -923,7 +922,7 @@ class TestDatabaseEncoder: XCTestCase {
     // Decoding
     // This input has a dictionary with two keys, but only one will end up in the container
     let input = ["unused key 1": "test1", "unused key 2": "test2"]
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.keyDecodingStrategy = .custom(customKeyConversion)
 
     let decodingResult = try! decoder.decode(DecodeMe5.self, from: input)
@@ -932,7 +931,7 @@ class TestDatabaseEncoder: XCTestCase {
 
     // Encoding
     let encoded = DecodeMe5()
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.keyEncodingStrategy = .custom(customKeyConversion)
     let decodingResult2 = try! encoder.encode(encoded)
 
@@ -943,7 +942,7 @@ class TestDatabaseEncoder: XCTestCase {
   // MARK: - Encoder Features
 
   func testNestedContainerCodingPaths() {
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     do {
       _ = try encoder.encode(NestedContainersTestType())
     } catch let error as NSError {
@@ -952,7 +951,7 @@ class TestDatabaseEncoder: XCTestCase {
   }
 
   func testSuperEncoderCodingPaths() {
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     do {
       _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true))
     } catch let error as NSError {
@@ -976,7 +975,7 @@ class TestDatabaseEncoder: XCTestCase {
   }
 
   func testInterceptURL() {
-    // Want to make sure Database.Encoder writes out single-value URLs, not the keyed encoding.
+    // Want to make sure StructureEncoder writes out single-value URLs, not the keyed encoding.
     let expected = "http://swift.org"
     let url = URL(string: "http://swift.org")!
     _testRoundTrip(of: url, expected: expected)
@@ -1015,13 +1014,13 @@ class TestDatabaseEncoder: XCTestCase {
   }
 
   func testDecodingConcreteTypeParameter() {
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     guard let value = try? encoder.encode(Employee.testValue) else {
       XCTFail("Unable to encode Employee.")
       return
     }
 
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     guard let decoded = try? decoder.decode(Employee.self as Person.Type, from: value) else {
       XCTFail("Failed to decode Employee as Person.")
       return
@@ -1062,7 +1061,7 @@ class TestDatabaseEncoder: XCTestCase {
     //
     // The issue at hand reproduces when you have a referencing encoder (superEncoder() creates one) that has a container on the stack (unkeyedContainer() adds one) that encodes a value going through box_() (Array does that) that encodes something which throws (Float.infinity does that).
     // When reproducing, this will cause a test failure via fatalError().
-    _ = try? Database.Encoder().encode(ReferencingEncoderWrapper([Float.infinity]))
+    _ = try? StructureEncoder().encode(ReferencingEncoderWrapper([Float.infinity]))
   }
 
   func testEncoderStateThrowOnEncodeCustomDate() {
@@ -1079,7 +1078,7 @@ class TestDatabaseEncoder: XCTestCase {
     }
 
     // The closure needs to push a container before throwing an error to trigger.
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.dateEncodingStrategy = .custom { _, encoder in
       _ = encoder.unkeyedContainer()
       enum CustomError: Error { case foo }
@@ -1103,7 +1102,7 @@ class TestDatabaseEncoder: XCTestCase {
     }
 
     // The closure needs to push a container before throwing an error to trigger.
-    let encoder = Database.Encoder()
+    let encoder = StructureEncoder()
     encoder.dataEncodingStrategy = .custom { _, encoder in
       _ = encoder.unkeyedContainer()
       enum CustomError: Error { case foo }
@@ -1121,12 +1120,12 @@ class TestDatabaseEncoder: XCTestCase {
     // Once Array decoding begins, 1 is pushed onto the container stack ([[1,2,3], 1]), and 1 is attempted to be decoded as String. This throws a .typeMismatch, but the container is not popped off the stack.
     // When attempting to decode [Int], the container stack is still ([[1,2,3], 1]), and 1 fails to decode as [Int].
     let input = [1, 2, 3]
-    _ = try! Database.Decoder().decode(EitherDecodable<[String], [Int]>.self, from: input)
+    _ = try! StructureDecoder().decode(EitherDecodable<[String], [Int]>.self, from: input)
   }
 
   func testDecoderStateThrowOnDecodeCustomDate() {
     // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch.
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.dateDecodingStrategy = .custom { decoder in
       enum CustomError: Error { case foo }
       throw CustomError.foo
@@ -1138,7 +1137,7 @@ class TestDatabaseEncoder: XCTestCase {
 
   func testDecoderStateThrowOnDecodeCustomData() {
     // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch.
-    let decoder = Database.Decoder()
+    let decoder = StructureDecoder()
     decoder.dataDecodingStrategy = .custom { decoder in
       enum CustomError: Error { case foo }
       throw CustomError.foo
@@ -1156,34 +1155,34 @@ class TestDatabaseEncoder: XCTestCase {
 
   private func _testEncodeFailure<T: Encodable>(of value: T) {
     do {
-      _ = try Database.Encoder().encode(value)
+      _ = try StructureEncoder().encode(value)
       XCTFail("Encode of top-level \(T.self) was expected to fail.")
     } catch {}
   }
 
   private func _testRoundTrip<T, U>(of value: T,
                                     expected: U,
-                                    dateEncodingStrategy: Database.Encoder
+                                    dateEncodingStrategy: StructureEncoder
                                       .DateEncodingStrategy = .deferredToDate,
-                                    dateDecodingStrategy: Database.Decoder
+                                    dateDecodingStrategy: StructureDecoder
                                       .DateDecodingStrategy = .deferredToDate,
-                                    dataEncodingStrategy: Database.Encoder
+                                    dataEncodingStrategy: StructureEncoder
                                       .DataEncodingStrategy = .base64,
-                                    dataDecodingStrategy: Database.Decoder
+                                    dataDecodingStrategy: StructureDecoder
                                       .DataDecodingStrategy = .base64,
-                                    keyEncodingStrategy: Database.Encoder
+                                    keyEncodingStrategy: StructureEncoder
                                       .KeyEncodingStrategy = .useDefaultKeys,
-                                    keyDecodingStrategy: Database.Decoder
+                                    keyDecodingStrategy: StructureDecoder
                                       .KeyDecodingStrategy = .useDefaultKeys,
-                                    nonConformingFloatEncodingStrategy: Database.Encoder
+                                    nonConformingFloatEncodingStrategy: StructureEncoder
                                       .NonConformingFloatEncodingStrategy = .throw,
-                                    nonConformingFloatDecodingStrategy: Database.Decoder
+                                    nonConformingFloatDecodingStrategy: StructureDecoder
                                       .NonConformingFloatDecodingStrategy = .throw)
     where T: Codable,
     T: Equatable, U: Equatable {
     var payload: Any! = nil
     do {
-      let encoder = Database.Encoder()
+      let encoder = StructureEncoder()
       encoder.dateEncodingStrategy = dateEncodingStrategy
       encoder.dataEncodingStrategy = dataEncodingStrategy
       encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy
@@ -1200,7 +1199,7 @@ class TestDatabaseEncoder: XCTestCase {
     )
 
     do {
-      let decoder = Database.Decoder()
+      let decoder = StructureDecoder()
       decoder.dateDecodingStrategy = dateDecodingStrategy
       decoder.dataDecodingStrategy = dataDecodingStrategy
       decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy
@@ -1213,26 +1212,26 @@ class TestDatabaseEncoder: XCTestCase {
   }
 
   private func _testRoundTrip<T>(of value: T,
-                                 dateEncodingStrategy: Database.Encoder
+                                 dateEncodingStrategy: StructureEncoder
                                    .DateEncodingStrategy = .deferredToDate,
-                                 dateDecodingStrategy: Database.Decoder
+                                 dateDecodingStrategy: StructureDecoder
                                    .DateDecodingStrategy = .deferredToDate,
-                                 dataEncodingStrategy: Database.Encoder
+                                 dataEncodingStrategy: StructureEncoder
                                    .DataEncodingStrategy = .base64,
-                                 dataDecodingStrategy: Database.Decoder
+                                 dataDecodingStrategy: StructureDecoder
                                    .DataDecodingStrategy = .base64,
-                                 keyEncodingStrategy: Database.Encoder
+                                 keyEncodingStrategy: StructureEncoder
                                    .KeyEncodingStrategy = .useDefaultKeys,
-                                 keyDecodingStrategy: Database.Decoder
+                                 keyDecodingStrategy: StructureDecoder
                                    .KeyDecodingStrategy = .useDefaultKeys,
-                                 nonConformingFloatEncodingStrategy: Database.Encoder
+                                 nonConformingFloatEncodingStrategy: StructureEncoder
                                    .NonConformingFloatEncodingStrategy = .throw,
-                                 nonConformingFloatDecodingStrategy: Database.Decoder
+                                 nonConformingFloatDecodingStrategy: StructureDecoder
                                    .NonConformingFloatDecodingStrategy = .throw) where T: Codable,
     T: Equatable {
     var payload: Any! = nil
     do {
-      let encoder = Database.Encoder()
+      let encoder = StructureEncoder()
       encoder.dateEncodingStrategy = dateEncodingStrategy
       encoder.dataEncodingStrategy = dataEncodingStrategy
       encoder.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy
@@ -1243,7 +1242,7 @@ class TestDatabaseEncoder: XCTestCase {
     }
 
     do {
-      let decoder = Database.Decoder()
+      let decoder = StructureDecoder()
       decoder.dateDecodingStrategy = dateDecodingStrategy
       decoder.dataDecodingStrategy = dataDecodingStrategy
       decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy
@@ -1258,8 +1257,8 @@ class TestDatabaseEncoder: XCTestCase {
   private func _testRoundTripTypeCoercionFailure<T, U>(of value: T, as type: U.Type)
     where T: Codable, U: Codable {
     do {
-      let data = try Database.Encoder().encode(value)
-      _ = try Database.Decoder().decode(U.self, from: data)
+      let data = try StructureEncoder().encode(value)
+      _ = try StructureDecoder().decode(U.self, from: data)
       XCTFail("Coercion from \(T.self) to \(U.self) was expected to fail.")
     } catch {}
   }

+ 15 - 6
Package.swift

@@ -516,18 +516,27 @@ let package = Package(
     ),
     .target(
       name: "FirebaseDatabaseSwift",
-      dependencies: ["FirebaseDatabase"],
-      path: "FirebaseDatabaseSwift/Sources",
-      exclude: [
-        "third_party/RTDBEncoder/LICENSE",
-        "third_party/RTDBEncoder/METADATA",
-      ]
+      dependencies: ["FirebaseDatabase", "FirebaseSharedSwift"],
+      path: "FirebaseDatabaseSwift/Sources"
     ),
     .testTarget(
       name: "FirebaseDatabaseSwiftTests",
       dependencies: ["FirebaseDatabase", "FirebaseDatabaseSwift"],
       path: "FirebaseDatabaseSwift/Tests/"
     ),
+    .target(
+      name: "FirebaseSharedSwift",
+      path: "FirebaseSharedSwift/Sources",
+      exclude: [
+        "third_party/StructureEncoder/LICENSE",
+        "third_party/StructureEncoder/METADATA",
+      ]
+    ),
+    .testTarget(
+      name: "FirebaseSharedSwiftTests",
+      dependencies: ["FirebaseSharedSwift"],
+      path: "FirebaseSharedSwift/Tests/"
+    ),
     .target(
       name: "FirebaseDynamicLinksTarget",
       dependencies: [.target(name: "FirebaseDynamicLinks",