Forráskód Böngészése

RC Swift pod and codable (#9084)

Paul Beusterien 4 éve
szülő
commit
2246d8ff8c
36 módosított fájl, 1039 hozzáadás és 277 törlés
  1. 8 6
      .github/workflows/remoteconfig.yml
  2. 1 3
      .gitignore
  3. 5 36
      FirebaseRemoteConfig.podspec
  4. 0 0
      FirebaseRemoteConfig/Tests/Sample/GoogleService-Info.plist
  5. 4 4
      FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj
  6. 0 63
      FirebaseRemoteConfig/Tests/SwiftAPI/APITestBase.swift
  7. 85 0
      FirebaseRemoteConfigSwift.podspec
  8. 5 0
      FirebaseRemoteConfigSwift/CHANGELOG.md
  9. 67 0
      FirebaseRemoteConfigSwift/Sources/Codable.swift
  10. 50 0
      FirebaseRemoteConfigSwift/Sources/FirebaseRemoteConfigValueDecoderHelper.swift
  11. 40 0
      FirebaseRemoteConfigSwift/Sources/Value.swift
  12. 0 0
      FirebaseRemoteConfigSwift/Tests/AccessToken.json
  13. 0 0
      FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift
  14. 4 0
      FirebaseRemoteConfigSwift/Tests/FakeUtils/FakeConsole.swift
  15. 4 0
      FirebaseRemoteConfigSwift/Tests/FakeUtils/URLSessionPartialMock.swift
  16. 1 1
      FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h
  17. 0 0
      FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h
  18. 1 1
      FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.m
  19. 16 10
      FirebaseRemoteConfigSwift/Tests/README.md
  20. 102 0
      FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITestBase.swift
  21. 0 33
      FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift
  22. 6 33
      FirebaseRemoteConfigSwift/Tests/SwiftAPI/AsyncAwaitTests.swift
  23. 174 0
      FirebaseRemoteConfigSwift/Tests/SwiftAPI/Codable.swift
  24. 46 0
      FirebaseRemoteConfigSwift/Tests/SwiftAPI/Constants.swift
  25. 0 0
      FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift
  26. 197 0
      FirebaseRemoteConfigSwift/Tests/SwiftAPI/Value.swift
  27. 6 3
      FirebaseSharedSwift.podspec
  28. 24 0
      FirebaseSharedSwift/Sources/FirebaseRemoteConfigValueDecoding.swift
  29. 54 49
      FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift
  30. 40 0
      Package.swift
  31. 2 1
      ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift
  32. 3 2
      SharedTestUtilities/FIROptionsMock.m
  33. 4 4
      SwiftDashboard.md
  34. 12 27
      scripts/build.sh
  35. 1 1
      scripts/health_metrics/file_patterns.json
  36. 77 0
      scripts/spm_test_schemes/RemoteConfigFakeConsole.xcscheme

+ 8 - 6
.github/workflows/remoteconfig.yml

@@ -32,18 +32,17 @@ jobs:
       run: scripts/setup_bundler.sh
     - name: Install Secret GoogleService-Info.plist
       run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/RemoteConfigSwiftAPI/GoogleService-Info.plist.gpg \
-          FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist "$plist_secret"
+          FirebaseRemoteConfigSwift/Tests/SwiftAPI/GoogleService-Info.plist "$plist_secret"
     - name: Generate Access Token for RemoteConfigConsoleAPI in IntegrationTests
       if: matrix.target == 'iOS'
       run: ([ -z $plist_secret ] || scripts/generate_access_token.sh "$plist_secret" scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg
-          FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json)
-    - name: BuildAndUnitTest # can be replaced with pod lib lint with CocoaPods 1.10
-      run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig ${{ matrix.target }} unit
+          FirebaseRemoteConfigSwift/Tests/AccessToken.json)
     - name: Fake Console API Tests
       run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig iOS fakeconsole
     - name: IntegrationTest
       if: matrix.target == 'iOS'
-      run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig iOS integration)
+      # No retry to avoid exhausting AccessToken quota.
+      run: ([ -z $plist_secret ] || scripts/build.sh RemoteConfig iOS integration)
 
   pod-lib-lint:
     # Don't run on private repo unless it is a PR.
@@ -53,13 +52,14 @@ jobs:
     strategy:
       matrix:
         target: [ios, tvos, macos, watchos]
+        podspec: [FirebaseRemoteConfig.podspec, FirebaseRemoteConfigSwift.podspec --skip-tests]
     steps:
     - uses: actions/checkout@v2
     - name: Setup Bundler
       run: scripts/setup_bundler.sh
     - name: Build and test
       run: |
-       scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --skip-tests --platforms=${{ matrix.target }}
+       scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb ${{ matrix.podspec }} --platforms=${{ matrix.target }}
 
   spm:
     # Don't run on private repo unless it is a PR.
@@ -74,6 +74,8 @@ jobs:
       run: scripts/setup_spm_tests.sh
     - name: iOS Unit Tests
       run: scripts/third_party/travis/retry.sh ./scripts/build.sh RemoteConfigUnit iOS spm
+    - name: Fake Console tests
+      run: scripts/third_party/travis/retry.sh ./scripts/build.sh RemoteConfigFakeConsole iOS spm
 
   spm-cron:
     # Don't run on private repo.

+ 1 - 3
.gitignore

@@ -8,9 +8,7 @@ FirebaseAuth/Tests/Sample/SwiftApiTests/Credentials.swift
 
 FirebaseDatabase/Tests/Resources/GoogleService-Info.plist
 
-FirebaseRemoteConfig/Tests/SwiftAPI/Resources/GoogleService-Info.plist
-FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/GoogleService-Info.plist
-FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/SecondApp-GoogleService-Info.plist
+FirebaseRemoteConfig/Tests/Sample/GoogleService-Info.plist
 
 # FirebaseStorage integration tests GoogleService-Info.plist
 FirebaseStorage/Tests/Integration/Resources/GoogleService-Info.plist

+ 5 - 36
FirebaseRemoteConfig.podspec

@@ -52,6 +52,11 @@ app update.
 
   s.test_spec 'unit' do |unit_tests|
     unit_tests.scheme = { :code_coverage => true }
+    unit_tests.platforms = {
+      :ios => ios_deployment_target,
+      :osx => osx_deployment_target,
+      :tvos => tvos_deployment_target
+    }
     # TODO(dmandar) - Update or delete the commented files.
     unit_tests.source_files =
         'FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m',
@@ -79,40 +84,4 @@ app update.
     unit_tests.requires_arc = true
   end
 
-  # Run Swift API tests on a real backend.
-  s.test_spec 'swift-api-tests' do |swift_api|
-    swift_api.scheme = { :code_coverage => true }
-    swift_api.platforms = {
-      :ios => ios_deployment_target,
-      :osx => osx_deployment_target,
-      :tvos => tvos_deployment_target
-    }
-    swift_api.source_files = 'FirebaseRemoteConfig/Tests/SwiftAPI/*.swift',
-                             'FirebaseRemoteConfig/Tests/FakeUtils/*.[hm]',
-                             'FirebaseRemoteConfig/Tests/FakeUtils/*.swift'
-    swift_api.requires_app_host = true
-    swift_api.pod_target_xcconfig = {
-      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h'
-    }
-    swift_api.dependency 'OCMock'
-  end
-
-  # Run Swift API tests and tests requiring console changes on a Fake Console.
-  s.test_spec 'fake-console-tests' do |fake_console|
-    fake_console.scheme = { :code_coverage => true }
-    fake_console.platforms = {
-      :ios => ios_deployment_target,
-      :osx => osx_deployment_target,
-      :tvos => tvos_deployment_target
-    }
-    fake_console.source_files = 'FirebaseRemoteConfig/Tests/SwiftAPI/*.swift',
-                                      'FirebaseRemoteConfig/Tests/FakeUtils/*.[hm]',
-                                      'FirebaseRemoteConfig/Tests/FakeUtils/*.swift',
-                                      'FirebaseRemoteConfig/Tests/FakeConsole/*.swift'
-    fake_console.requires_app_host = true
-    fake_console.pod_target_xcconfig = {
-      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h'
-    }
-    fake_console.dependency 'OCMock'
-  end
 end

+ 0 - 0
FirebaseRemoteConfig/Tests/FakeUtils/GoogleService-Info.plist → FirebaseRemoteConfig/Tests/Sample/GoogleService-Info.plist


+ 4 - 4
FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj

@@ -17,7 +17,7 @@
 		5BE818FB23271579004DE6BA /* RemoteConfigSampleAppUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BE818FA23271579004DE6BA /* RemoteConfigSampleAppUITests.m */; };
 		D5C72761A16C64F0DB522FE9 /* Pods_RemoteConfigSampleAppUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1244CD4C99B6BF70752BE1B6 /* Pods_RemoteConfigSampleAppUITests.framework */; };
 		DE71200824C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE71200724C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist */; };
-		DE71200A24C9C1F100C28FE2 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE71200924C9C1F000C28FE2 /* GoogleService-Info.plist */; };
+		DEB1E8792785018F0054A548 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DEB1E8782785018F0054A548 /* GoogleService-Info.plist */; };
 		F149334D7027F03ADF9FF9FC /* Pods_RemoteConfigSampleApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D431F3C235B420372A7847C /* Pods_RemoteConfigSampleApp.framework */; };
 /* End PBXBuildFile section */
 
@@ -53,7 +53,7 @@
 		5CC24231A56D124F27682F1C /* Pods-RemoteConfigSampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleApp.debug.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp.debug.xcconfig"; sourceTree = "<group>"; };
 		DA0A0BDC31A76C0D70249412 /* Pods-RemoteConfigSampleApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleApp.release.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp.release.xcconfig"; sourceTree = "<group>"; };
 		DE71200724C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "SecondApp-GoogleService-Info.plist"; path = "../../Unit/SecondApp-GoogleService-Info.plist"; sourceTree = "<group>"; };
-		DE71200924C9C1F000C28FE2 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../FakeUtils/GoogleService-Info.plist"; sourceTree = "<group>"; };
+		DEB1E8782785018F0054A548 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; };
 		DEB267EAC843EF4BD455C9EC /* Pods-RemoteConfigSampleAppUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleAppUITests.debug.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleAppUITests/Pods-RemoteConfigSampleAppUITests.debug.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -120,7 +120,7 @@
 		5BE818E023271577004DE6BA /* RemoteConfigSampleApp */ = {
 			isa = PBXGroup;
 			children = (
-				DE71200924C9C1F000C28FE2 /* GoogleService-Info.plist */,
+				DEB1E8782785018F0054A548 /* GoogleService-Info.plist */,
 				DE71200724C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist */,
 				5B1A9B12232846F3001809E9 /* FRCLog.h */,
 				5B1A9B13232846F3001809E9 /* FRCLog.m */,
@@ -231,7 +231,7 @@
 			files = (
 				5BE818EB23271579004DE6BA /* Assets.xcassets in Resources */,
 				DE71200824C92CA800C28FE2 /* SecondApp-GoogleService-Info.plist in Resources */,
-				DE71200A24C9C1F100C28FE2 /* GoogleService-Info.plist in Resources */,
+				DEB1E8792785018F0054A548 /* GoogleService-Info.plist in Resources */,
 				5B1A9B1723284735001809E9 /* LaunchScreen.xib in Resources */,
 				5BE818E923271577004DE6BA /* Main.storyboard in Resources */,
 			);

+ 0 - 63
FirebaseRemoteConfig/Tests/SwiftAPI/APITestBase.swift

@@ -1,63 +0,0 @@
-// 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 FirebaseCore
-@testable import FirebaseRemoteConfig
-
-import XCTest
-
-class APITestBase: XCTestCase {
-  static var useFakeConfig: Bool!
-  static var mockedFetch: Bool!
-  var app: FirebaseApp!
-  var config: RemoteConfig!
-  var fakeConsole: FakeConsole!
-
-  override class func setUp() {
-    if FirebaseApp.app() == nil {
-      FirebaseApp.configure()
-      APITests.mockedFetch = false
-    }
-    useFakeConfig = FirebaseApp.app()!.options.projectID == "FakeProject"
-  }
-
-  override func setUp() {
-    super.setUp()
-    app = FirebaseApp.app()
-    config = RemoteConfig.remoteConfig(app: app!)
-    let settings = RemoteConfigSettings()
-    settings.minimumFetchInterval = 0
-    config.configSettings = settings
-    if APITests.useFakeConfig {
-      if !APITests.mockedFetch {
-        APITests.mockedFetch = true
-        config.configFetch = FetchMocks.mockFetch(config.configFetch)
-      }
-      fakeConsole = FakeConsole()
-      config.configFetch.fetchSession = URLSessionMock(with: fakeConsole)
-    }
-
-    // Uncomment for verbose debug logging.
-    // FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel.debug)
-  }
-
-  override func tearDown() {
-    if APITests.useFakeConfig {
-      fakeConsole.empty()
-    }
-    app = nil
-    config = nil
-    super.tearDown()
-  }
-}

+ 85 - 0
FirebaseRemoteConfigSwift.podspec

@@ -0,0 +1,85 @@
+Pod::Spec.new do |s|
+  s.name                    = 'FirebaseRemoteConfigSwift'
+  s.version                 = '8.11.0-beta'
+  s.summary                 = 'Swift Extensions for Firebase Remote Config'
+
+  s.description      = <<-DESC
+Firebase Remote Config is a cloud service that lets you change the
+appearance and behavior of your app without requiring users to download an
+app update.
+                       DESC
+
+
+  s.homepage                = 'https://developers.google.com/'
+  s.license                 = { :type => 'Apache', :file => 'LICENSE' }
+  s.authors                 = 'Google, Inc.'
+
+  s.source                  = {
+    :git => 'https://github.com/Firebase/firebase-ios-sdk.git',
+    :tag => 'CocoaPods-' + s.version.to_s
+  }
+
+  s.swift_version           = '5.0'
+
+  ios_deployment_target = '10.0'
+  osx_deployment_target = '10.12'
+  tvos_deployment_target = '10.0'
+  watchos_deployment_target = '6.0'
+
+  s.ios.deployment_target = ios_deployment_target
+  s.osx.deployment_target = osx_deployment_target
+  s.tvos.deployment_target = tvos_deployment_target
+  s.watchos.deployment_target = watchos_deployment_target
+
+  s.cocoapods_version       = '>= 1.4.0'
+  s.prefix_header_file      = false
+
+  s.source_files = [
+    'FirebaseRemoteConfigSwift/Sources/*.swift',
+  ]
+
+  s.dependency 'FirebaseRemoteConfig', '~> 8.11'
+  s.dependency 'FirebaseSharedSwift', '~> 8.11-beta'
+
+  # Run Swift API tests on a real backend.
+  s.test_spec 'swift-api-tests' do |swift_api|
+    swift_api.scheme = { :code_coverage => true }
+    swift_api.platforms = {
+      :ios => ios_deployment_target,
+      :osx => osx_deployment_target,
+      :tvos => tvos_deployment_target
+    }
+    swift_api.source_files = ['FirebaseRemoteConfigSwift/Tests/SwiftAPI/*.swift',
+                              'FirebaseRemoteConfigSwift/Tests/FakeUtils/*.swift',
+                              'FirebaseRemoteConfigSwift/Tests/ObjC/*.[hm]',
+                             ]
+    swift_api.requires_app_host = true
+    swift_api.pod_target_xcconfig = {
+      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h',
+      'OTHER_SWIFT_FLAGS' => '$(inherited) -D USE_REAL_CONSOLE',
+      'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"'
+    }
+    swift_api.dependency 'OCMock'
+  end
+
+  # Run Swift API tests and tests requiring console changes on a Fake Console.
+  s.test_spec 'fake-console-tests' do |fake_console|
+    fake_console.scheme = { :code_coverage => true }
+    fake_console.platforms = {
+      :ios => ios_deployment_target,
+      :osx => osx_deployment_target,
+      :tvos => tvos_deployment_target
+    }
+    fake_console.source_files = ['FirebaseRemoteConfigSwift/Tests/SwiftAPI/*.swift',
+                                 'FirebaseRemoteConfigSwift/Tests/FakeUtils/*.swift',
+                                 'FirebaseRemoteConfigSwift/Tests/FakeConsole/*.swift',
+                                 'FirebaseRemoteConfigSwift/Tests/ObjC/*.[hm]',
+                                ]
+    fake_console.requires_app_host = true
+    fake_console.pod_target_xcconfig = {
+      'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h',
+      'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"'
+    }
+    fake_console.dependency 'OCMock'
+  end
+end

+ 5 - 0
FirebaseRemoteConfigSwift/CHANGELOG.md

@@ -0,0 +1,5 @@
+# 8.12.0-beta
+- Initial public beta release with Codable support. See example usage in
+  https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/Codable.swift
+  and
+  https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/Value.swift. (#6883)

+ 67 - 0
FirebaseRemoteConfigSwift/Sources/Codable.swift

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 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 FirebaseRemoteConfig
+import FirebaseSharedSwift
+
+public enum RemoteConfigValueCodableError: Error {
+  case unsupportedType(String)
+}
+
+public extension RemoteConfigValue {
+  /// Extracts a RemoteConfigValue JSON-encoded object and decodes it to the requested type.
+  ///
+  /// - Parameter asType: The type to decode the JSON-object to
+  func decoded<Value: Decodable>(asType: Value.Type = Value.self) throws -> Value {
+    if asType == Date.self {
+      throw RemoteConfigValueCodableError
+        .unsupportedType("Date type is not currently supported for " +
+          " Remote Config Value decoding. Please file a feature request")
+    }
+    return try FirebaseDataDecoder()
+      .decode(Value.self, from: FirebaseRemoteConfigValueDecoderHelper(value: self))
+  }
+}
+
+public enum RemoteConfigCodableError: Error {
+  case invalidSetDefaultsInput(String)
+}
+
+public extension RemoteConfig {
+  /// Decodes a struct from the respective Remote Config values.
+  ///
+  /// - Parameter asType: The type to decode to.
+  func decoded<Value: Decodable>(asType: Value.Type = Value.self) throws -> Value {
+    let keys = allKeys(from: RemoteConfigSource.default) + allKeys(from: RemoteConfigSource.remote)
+    let config = keys.reduce(into: [String: FirebaseRemoteConfigValueDecoderHelper]()) {
+      $0[$1] = FirebaseRemoteConfigValueDecoderHelper(value: configValue(forKey: $1))
+    }
+    return try FirebaseDataDecoder().decode(Value.self, from: config)
+  }
+
+  /// Sets config defaults from an encodable struct.
+  ///
+  /// - Parameter value: The object to use to set the defaults.
+  func setDefaults<Value: Encodable>(from value: Value) throws {
+    guard let encoded = try FirebaseDataEncoder().encode(value) as? [String: NSObject] else {
+      throw RemoteConfigCodableError.invalidSetDefaultsInput(
+        "The setDefaults input: \(value), must be a Struct that encodes to a Dictionary"
+      )
+    }
+    setDefaults(encoded)
+  }
+}

+ 50 - 0
FirebaseRemoteConfigSwift/Sources/FirebaseRemoteConfigValueDecoderHelper.swift

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 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 FirebaseRemoteConfig
+import FirebaseSharedSwift
+
+/// Implement the FirebaseRemoteConfigValueDecoding protocol for the shared Firebase decoder to
+/// decode Remote Config Values. It returns the four different kinds of values from
+/// a RemoteConfigValue object.
+struct FirebaseRemoteConfigValueDecoderHelper: FirebaseRemoteConfigValueDecoding {
+  let value: RemoteConfigValue
+
+  func numberValue() -> NSNumber {
+    return value.numberValue
+  }
+
+  func boolValue() -> Bool {
+    return value.boolValue
+  }
+
+  func stringValue() -> String {
+    return value.stringValue ?? ""
+  }
+
+  func dataValue() -> Data {
+    return value.dataValue
+  }
+
+  func jsonValue() -> [String: AnyHashable]? {
+    guard let value = value.jsonValue as? [String: AnyHashable] else {
+      // nil is the historical behavior for failing to extract JSON.
+      return nil
+    }
+    return value
+  }
+}

+ 40 - 0
FirebaseRemoteConfigSwift/Sources/Value.swift

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 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 FirebaseRemoteConfig
+
+/// Implements subscript overloads to enable Remote Config values to be accessed
+/// in a type-safe way directly from the current config.
+public extension RemoteConfig {
+  /// Return a typed RemoteConfigValue for a key.
+  /// - Parameter key: A Remote Config key.
+  /// - Returns: A typed RemoteConfigValue.
+  subscript<T: Decodable>(decodedValue key: String) -> T? {
+    return try? configValue(forKey: key).decoded()
+  }
+
+  /// Return a Dictionary for a RemoteConfig JSON key.
+  /// - Parameter key: A Remote Config key.
+  /// - Returns: A Dictionary representing a RemoteConfig JSON value.
+  subscript(jsonValue key: String) -> [String: AnyHashable]? {
+    guard let value = configValue(forKey: key).jsonValue as? [String: AnyHashable] else {
+      // nil is the historical behavior for failing to extract JSON.
+      return nil
+    }
+    return value
+  }
+}

+ 0 - 0
FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json → FirebaseRemoteConfigSwift/Tests/AccessToken.json


+ 0 - 0
FirebaseRemoteConfig/Tests/FakeConsole/FakeConsoleTests.swift → FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift


+ 4 - 0
FirebaseRemoteConfig/Tests/FakeUtils/FakeConsole.swift → FirebaseRemoteConfigSwift/Tests/FakeUtils/FakeConsole.swift

@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#if SWIFT_PACKAGE
+  import RemoteConfigFakeConsoleObjC
+#endif
+
 class FakeConsole {
   var config = [String: AnyHashable]()
   private var last = [String: AnyHashable]()

+ 4 - 0
FirebaseRemoteConfig/Tests/FakeUtils/URLSessionPartialMock.swift → FirebaseRemoteConfigSwift/Tests/FakeUtils/URLSessionPartialMock.swift

@@ -14,6 +14,10 @@
 
 import Foundation
 
+#if SWIFT_PACKAGE
+  import RemoteConfigFakeConsoleObjC
+#endif
+
 // Create a partial mock by subclassing the URLSessionDataTask.
 class URLSessionDataTaskMock: URLSessionDataTask {
   private let closure: () -> Void

+ 1 - 1
FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h → FirebaseRemoteConfigSwift/Tests/ObjC/Bridging-Header.h

@@ -14,4 +14,4 @@
 
 #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
 #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
-#import "FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h"
+#import "FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h"

+ 0 - 0
FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h → FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h


+ 1 - 1
FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.m → FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.m

@@ -15,7 +15,7 @@
 #import <OCMock/OCMock.h>
 
 #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
-#import "FirebaseRemoteConfig/Tests/FakeUtils/FetchMocks.h"
+#import "FirebaseRemoteConfigSwift/Tests/ObjC/FetchMocks.h"
 
 @interface RCNConfigFetch (ExposedForTest)
 - (void)refreshInstallationsTokenWithCompletionHandler:

+ 16 - 10
FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md → FirebaseRemoteConfigSwift/Tests/README.md

@@ -1,10 +1,16 @@
-# Remote Config Console API
+# Remote Config Swift Tests
 
-[`RemoteConfigConsole.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift)
+Currently the Remote Config tests run in two configurations:
+1. Fake Console - mocks the console to run tests with a dummy GoogleService-Info.plist.
+2. Remote Config Console API - relies on generating an access token to use a real Firebase project.
+
+## Remote Config Console API
+
+[`RemoteConfigConsole.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift)
 provides a simple API for interacting with an app's Remote Config on the
 Firebase console.
 
-## Local Development Setup
+### Local Development Setup
 1. Create a Firebase project on the Firebase Console and download
 the  `GoogleService-Info.plist`.
 2. Navigate to your project's settings. Click on the **Service accounts** tab and
@@ -12,7 +18,7 @@ then download a private key by clicking the blue button that says "Generate new
 Rename it `ServiceAccount.json`.
 3. Within the `firebase-ios-sdk`, run:
 ```bash
-./scripts/generate_access_token.sh local_dev PATH/TO/ServiceAccount.json FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json
+./scripts/generate_access_token.sh local_dev PATH/TO/ServiceAccount.json FirebaseRemoteConfigSwift/Tests/AccessToken.json
 ```
 4. Generate the `FirebaseRemoteConfig` project:
 ```bash
@@ -24,12 +30,12 @@ Xcode project.
 🚀 Everything is ready to go! Run the tests in the `swift-api-tests` target.
 
 
-## How it works
+### How it works
 
 While the `RemoteConfigConsole` API basically just makes simple network calls,
 we need to include an `access token` so our requests do the proper "handshake" with the Firebase console.
 
-### Firebase Service Account Private Key
+#### Firebase Service Account Private Key
 This private key is needed to create an access token with the valid parameters
 that authorizes our requests to programmatically make changes to remote config on the Firebase console.
 
@@ -37,7 +43,7 @@ The private key can be located on the Firebase console and navigate to your proj
 click on the **Service accounts** tab and then generate the private key by clicking
 the blue button that says "Generate new private key".
 
-### Create the Access Token
+#### Create the Access Token
 We use Google's [Auth Library for Swift](https://github.com/googleapis/google-auth-library-swift)
 to generate the access token. There are a few example use cases provided. We use the
 [`TokenSource`](https://github.com/googleapis/google-auth-library-swift/blob/master/Sources/Examples/TokenSource/main.swift)
@@ -48,7 +54,7 @@ Firebase project's service account key is stored. This is set in the
 [`generate_access_token.sh`](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/generate_access_token.sh)
 script.
 
-### Remote Config API Tests
-There is a [section](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift#L210)
-of tests in [`APITests.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift)
+#### Remote Config API Tests
+There is a [section](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift#L210)
+of tests in [`APITests.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift)
 showcasing the  `RemoteConfigConsole` in action.

+ 102 - 0
FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITestBase.swift

@@ -0,0 +1,102 @@
+// 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 FirebaseCore
+import FirebaseRemoteConfig
+
+#if SWIFT_PACKAGE
+  import RemoteConfigFakeConsoleObjC
+#endif
+
+import XCTest
+
+class APITestBase: XCTestCase {
+  static var useFakeConfig: Bool!
+  static var mockedFetch: Bool!
+  var app: FirebaseApp!
+  var config: RemoteConfig!
+  var console: RemoteConfigConsole!
+  var fakeConsole: FakeConsole!
+
+  override class func setUp() {
+    if FirebaseApp.app() == nil {
+      #if USE_REAL_CONSOLE
+        useFakeConfig = false
+        FirebaseApp.configure()
+      #else
+        useFakeConfig = true
+        let options = FirebaseOptions(googleAppID: "1:123:ios:123abc",
+                                      gcmSenderID: "correct_gcm_sender_id")
+        options.apiKey = "A23456789012345678901234567890123456789"
+        options.projectID = "Fake Project"
+        FirebaseApp.configure(options: options)
+        APITests.mockedFetch = false
+      #endif
+    }
+  }
+
+  override func setUpWithError() throws {
+    try super.setUpWithError()
+    app = FirebaseApp.app()
+    config = RemoteConfig.remoteConfig(app: app!)
+    let settings = RemoteConfigSettings()
+    settings.minimumFetchInterval = 0
+    config.configSettings = settings
+
+    let jsonData = try JSONSerialization.data(
+      withJSONObject: Constants.jsonValue
+    )
+    guard let jsonValue = String(data: jsonData, encoding: .ascii) else {
+      fatalError("Failed to make json Value from jsonData")
+    }
+
+    if APITests.useFakeConfig {
+      if !APITests.mockedFetch {
+        APITests.mockedFetch = true
+        config.configFetch = FetchMocks.mockFetch(config.configFetch)
+      }
+      fakeConsole = FakeConsole()
+      config.configFetch.fetchSession = URLSessionMock(with: fakeConsole)
+
+      fakeConsole.config = [Constants.key1: Constants.value1,
+                            Constants.jsonKey: jsonValue,
+                            Constants.nonJsonKey: Constants.nonJsonValue,
+                            Constants.stringKey: Constants.stringValue,
+                            Constants.intKey: String(Constants.intValue),
+                            Constants.floatKey: String(Constants.floatValue),
+                            Constants.decimalKey: "\(Constants.decimalValue)",
+                            Constants.trueKey: String(true),
+                            Constants.falseKey: String(false),
+                            Constants.dataKey: String(decoding: Constants.dataValue, as: UTF8.self)]
+    } else {
+      console = RemoteConfigConsole()
+      console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi)
+    }
+
+    // Uncomment for verbose debug logging.
+    // FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel.debug)
+  }
+
+  override func tearDown() {
+    if APITests.useFakeConfig {
+      fakeConsole.empty()
+    } else {
+      console.removeRemoteConfigValue(forKey: Constants.sith)
+      console.removeRemoteConfigValue(forKey: Constants.jedi)
+    }
+    app = nil
+    config = nil
+    super.tearDown()
+  }
+}

+ 0 - 33
FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift → FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift

@@ -17,40 +17,7 @@ import FirebaseCore
 
 import XCTest
 
-/// String constants used for testing.
-private enum Constants {
-  static let key1 = "Key1"
-  static let jedi = "Jedi"
-  static let sith = "Sith_Lord"
-  static let value1 = "Value1"
-  static let obiwan = "Obi-Wan"
-  static let yoda = "Yoda"
-  static let darthSidious = "Darth Sidious"
-}
-
 class APITests: APITestBase {
-  var console: RemoteConfigConsole!
-
-  override func setUp() {
-    super.setUp()
-    if APITests.useFakeConfig {
-      fakeConsole.config = [Constants.key1: Constants.value1]
-    } else {
-      console = RemoteConfigConsole()
-      console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi)
-    }
-  }
-
-  override func tearDown() {
-    super.tearDown()
-
-    // If using RemoteConfigConsole, reset remote config values.
-    if !APITests.useFakeConfig {
-      console.removeRemoteConfigValue(forKey: Constants.sith)
-      console.removeRemoteConfigValue(forKey: Constants.jedi)
-    }
-  }
-
   func testFetchThenActivate() {
     let expectation = self.expectation(description: #function)
     config.fetch { status, error in

+ 6 - 33
FirebaseRemoteConfig/Tests/SwiftAPI/AsyncAwaitTests.swift → FirebaseRemoteConfigSwift/Tests/SwiftAPI/AsyncAwaitTests.swift

@@ -17,42 +17,9 @@ import FirebaseCore
 
 import XCTest
 
-/// String constants used for testing.
-private enum Constants {
-  static let key1 = "Key1"
-  static let jedi = "Jedi"
-  static let sith = "Sith_Lord"
-  static let value1 = "Value1"
-  static let obiwan = "Obi-Wan"
-  static let yoda = "Yoda"
-  static let darthSidious = "Darth Sidious"
-}
-
 #if compiler(>=5.5) && canImport(_Concurrency)
   @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
   class AsyncAwaitTests: APITestBase {
-    var console: RemoteConfigConsole!
-
-    override func setUp() {
-      super.setUp()
-      if APITests.useFakeConfig {
-        fakeConsole.config = [Constants.key1: Constants.value1]
-      } else {
-        console = RemoteConfigConsole()
-        console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi)
-      }
-    }
-
-    override func tearDown() {
-      super.tearDown()
-
-      // If using RemoteConfigConsole, reset remote config values.
-      if !APITests.useFakeConfig {
-        console.removeRemoteConfigValue(forKey: Constants.sith)
-        console.removeRemoteConfigValue(forKey: Constants.jedi)
-      }
-    }
-
     func testFetchThenActivate() async throws {
       let status = try await config.fetch()
       XCTAssertEqual(status, RemoteConfigFetchStatus.success)
@@ -73,6 +40,12 @@ private enum Constants {
       XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
     }
 
+    func testFetchAndActivateGenericValue() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
+    }
+
     // Contrast with testChangedActivateWillNotFlag in FakeConsole.swift.
     func testUnchangedActivateWillFlag() async throws {
       let status = try await config.fetch()

+ 174 - 0
FirebaseRemoteConfigSwift/Tests/SwiftAPI/Codable.swift

@@ -0,0 +1,174 @@
+// Copyright 2021 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 FirebaseRemoteConfig
+import FirebaseRemoteConfigSwift
+
+import XCTest
+
+#if compiler(>=5.5) && canImport(_Concurrency)
+  @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
+  class CodableTests: APITestBase {
+    // MARK: - Test decoding Remote Config JSON values
+
+    // Contrast this test with the subsequent one to see the value of the Codable API.
+    func testFetchAndActivateWithoutCodable() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let dict = try XCTUnwrap(config[Constants.jsonKey].jsonValue as? [String: AnyHashable])
+      XCTAssertEqual(dict["recipeName"], "PB&J")
+      XCTAssertEqual(dict["ingredients"], ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(dict["cookTime"], 7)
+      XCTAssertEqual(
+        config[Constants.jsonKey].jsonValue as! [String: AnyHashable],
+        Constants.jsonValue
+      )
+    }
+
+    struct Recipe: Decodable {
+      var recipeName: String
+      var ingredients: [String]
+      var cookTime: Int
+    }
+
+    func testFetchAndActivateWithCodable() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let recipe = try XCTUnwrap(config[Constants.jsonKey].decoded(asType: Recipe.self))
+      XCTAssertEqual(recipe.recipeName, "PB&J")
+      XCTAssertEqual(recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(recipe.cookTime, 7)
+    }
+
+    func testFetchAndActivateWithCodableAlternativeAPI() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let recipe: Recipe = try XCTUnwrap(config[Constants.jsonKey].decoded())
+      XCTAssertEqual(recipe.recipeName, "PB&J")
+      XCTAssertEqual(recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(recipe.cookTime, 7)
+    }
+
+    func testFetchAndActivateWithCodableBadJson() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      do {
+        _ = try config[Constants.nonJsonKey].decoded(asType: Recipe.self)
+      } catch let DecodingError.typeMismatch(_, context) {
+        XCTAssertEqual(context.debugDescription,
+                       "Expected to decode Dictionary<String, Any> but found " +
+                         "FirebaseRemoteConfigValueDecoderHelper instead.")
+        return
+      }
+      XCTFail("Failed to catch trying to decode non-JSON key as JSON")
+    }
+
+    // MARK: - Test setting Remote Config defaults via an encodable struct
+
+    struct DataTestDefaults: Encodable {
+      var bool: Bool
+      var int: Int32
+      var long: Int64
+      var string: String
+    }
+
+    func testSetEncodeableDefaults() throws {
+      let data = DataTestDefaults(
+        bool: true,
+        int: 2,
+        long: 9_876_543_210,
+        string: "four"
+      )
+      try config.setDefaults(from: data)
+      let boolValue = try XCTUnwrap(config.defaultValue(forKey: "bool")).numberValue.boolValue
+      XCTAssertTrue(boolValue)
+      let intValue = try XCTUnwrap(config.defaultValue(forKey: "int")).numberValue.intValue
+      XCTAssertEqual(intValue, 2)
+      let longValue = try XCTUnwrap(config.defaultValue(forKey: "long")).numberValue.int64Value
+      XCTAssertEqual(longValue, 9_876_543_210)
+      let stringValue = try XCTUnwrap(config.defaultValue(forKey: "string")).stringValue
+      XCTAssertEqual(stringValue, "four")
+    }
+
+    func testSetEncodeableDefaultsInvalid() throws {
+      do {
+        _ = try config.setDefaults(from: 7)
+      } catch let RemoteConfigCodableError.invalidSetDefaultsInput(message) {
+        XCTAssertEqual(message,
+                       "The setDefaults input: 7, must be a Struct that encodes to a Dictionary")
+        return
+      }
+      XCTFail("Failed to catch trying to encode an invalid input to setDefaults.")
+    }
+
+    // MARK: - Test extracting config to an decodable struct.
+
+    struct MyConfig: Decodable {
+      var Recipe: Recipe
+      var notJSON: String
+      var myInt: Int
+      var myFloat: Float
+      var myDecimal: Decimal
+      var myTrue: Bool
+      var myData: Data
+    }
+
+    func testExtractConfig() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let myConfig: MyConfig = try config.decoded()
+      XCTAssertEqual(myConfig.notJSON, Constants.nonJsonValue)
+      XCTAssertEqual(myConfig.myInt, Constants.intValue)
+      XCTAssertEqual(myConfig.myTrue, true)
+      XCTAssertEqual(myConfig.myFloat, Constants.floatValue)
+      XCTAssertEqual(myConfig.myDecimal, Constants.decimalValue)
+      XCTAssertEqual(myConfig.myData, Constants.dataValue)
+      XCTAssertEqual(myConfig.Recipe.recipeName, "PB&J")
+      XCTAssertEqual(myConfig.Recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(myConfig.Recipe.cookTime, 7)
+    }
+
+    // Additional fields in config are ignored.
+    func testExtractConfigExtra() async throws {
+      guard APITests.useFakeConfig else { return }
+      fakeConsole.config["extra"] = "extra Value"
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let myConfig: MyConfig = try config.decoded()
+      XCTAssertEqual(myConfig.notJSON, Constants.nonJsonValue)
+      XCTAssertEqual(myConfig.Recipe.recipeName, "PB&J")
+      XCTAssertEqual(myConfig.Recipe.ingredients, ["bread", "peanut butter", "jelly"])
+      XCTAssertEqual(myConfig.Recipe.cookTime, 7)
+    }
+
+    // Failure if requested field does not exist.
+    func testExtractConfigMissing() async throws {
+      struct MyConfig: Decodable {
+        var missing: String
+        var Recipe: String
+        var notJSON: String
+      }
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      do {
+        let _: MyConfig = try config.decoded()
+      } catch let DecodingError.keyNotFound(codingKey, context) {
+        XCTAssertEqual(codingKey.stringValue, "missing")
+        print(codingKey, context)
+        return
+      }
+      XCTFail("Failed to throw on missing field")
+    }
+  }
+#endif

+ 46 - 0
FirebaseRemoteConfigSwift/Tests/SwiftAPI/Constants.swift

@@ -0,0 +1,46 @@
+// Copyright 2021 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
+
+/// String constants used for testing.
+enum Constants {
+  static let key1 = "Key1"
+  static let jedi = "Jedi"
+  static let sith = "Sith_Lord"
+  static let value1 = "Value1"
+  static let obiwan = "Obi-Wan"
+  static let yoda = "Yoda"
+  static let darthSidious = "Darth Sidious"
+
+  static let stringKey = "myString"
+  static let stringValue = "string contents"
+  static let intKey = "myInt"
+  static let intValue: Int = 123
+  static let floatKey = "myFloat"
+  static let floatValue: Float = 42.75
+  static let doubleValue: Double = 42.75
+  static let decimalKey = "myDecimal"
+  static let decimalValue: Decimal = 375.785
+  static let trueKey = "myTrue"
+  static let falseKey = "myFalse"
+  static let dataKey = "myData"
+  static let dataValue: Data = "data".data(using: .utf8)!
+  static let jsonKey = "Recipe"
+  static let jsonValue: [String: AnyHashable] = ["recipeName": "PB&J",
+                                                 "ingredients": ["bread", "peanut butter", "jelly"],
+                                                 "cookTime": 7]
+  static let nonJsonKey = "notJSON"
+  static let nonJsonValue = "notJSON"
+}

+ 0 - 0
FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift → FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift


+ 197 - 0
FirebaseRemoteConfigSwift/Tests/SwiftAPI/Value.swift

@@ -0,0 +1,197 @@
+// Copyright 2021 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 FirebaseRemoteConfig
+import FirebaseRemoteConfigSwift
+
+import XCTest
+
+#if compiler(>=5.5) && canImport(_Concurrency)
+  @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
+  class ValueTests: APITestBase {
+    func testFetchAndActivateAllTypes() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(config[Constants.stringKey].stringValue, Constants.stringValue)
+      XCTAssertEqual(config[Constants.intKey].numberValue.intValue, Constants.intValue)
+      XCTAssertEqual(config[Constants.intKey].numberValue.int8Value, Int8(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.int16Value, Int16(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.int32Value, Int32(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.int64Value, Int64(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uintValue, UInt(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint8Value, UInt8(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint16Value, UInt16(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint32Value, UInt32(Constants.intValue))
+      XCTAssertEqual(config[Constants.intKey].numberValue.uint64Value, UInt64(Constants.intValue))
+      XCTAssertEqual(
+        config[Constants.floatKey].numberValue.decimalValue,
+        Decimal(Constants.doubleValue)
+      )
+      XCTAssertEqual(config[Constants.floatKey].numberValue.floatValue, Constants.floatValue)
+      XCTAssertEqual(config[Constants.floatKey].numberValue.doubleValue, Constants.doubleValue)
+      XCTAssertEqual(config[Constants.trueKey].boolValue, true)
+      XCTAssertEqual(config[Constants.falseKey].boolValue, false)
+      XCTAssertEqual(
+        config[Constants.stringKey].dataValue,
+        Constants.stringValue.data(using: .utf8)
+      )
+      XCTAssertEqual(
+        config[Constants.jsonKey].jsonValue as! [String: AnyHashable],
+        Constants.jsonValue
+      )
+    }
+
+    func testStrongTypingViaSubscriptApi() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(config[decodedValue: Constants.stringKey], Constants.stringValue)
+      XCTAssertEqual(config[decodedValue: Constants.intKey], Constants.intValue)
+      XCTAssertEqual(config[decodedValue: Constants.floatKey], Constants.floatValue)
+      XCTAssertEqual(config[decodedValue: Constants.floatKey], Constants.doubleValue)
+      XCTAssertEqual(config[decodedValue: Constants.trueKey], true)
+      XCTAssertEqual(config[decodedValue: Constants.falseKey], false)
+      XCTAssertEqual(
+        config[decodedValue: Constants.stringKey],
+        Constants.stringValue.data(using: .utf8)
+      )
+      XCTAssertEqual(try XCTUnwrap(config[jsonValue: Constants.jsonKey]), Constants.jsonValue)
+    }
+
+    func testStrongTypingViaDecoder() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      XCTAssertEqual(
+        try config[Constants.stringKey].decoded(asType: String.self),
+        Constants.stringValue
+      )
+      XCTAssertEqual(try config[Constants.intKey].decoded(asType: Int.self), Constants.intValue)
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int8.self),
+        Int8(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int16.self),
+        Int16(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int32.self),
+        Int32(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: Int64.self),
+        Int64(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt.self),
+        UInt(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt8.self),
+        UInt8(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt16.self),
+        UInt16(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt32.self),
+        UInt32(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.intKey].decoded(asType: UInt64.self),
+        UInt64(Constants.intValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.floatKey].decoded(asType: Decimal.self),
+        Decimal(Constants.doubleValue)
+      )
+      XCTAssertEqual(
+        try config[Constants.floatKey].decoded(asType: Float.self),
+        Constants.floatValue
+      )
+      XCTAssertEqual(
+        try config[Constants.floatKey].decoded(asType: Double.self),
+        Constants.doubleValue
+      )
+      XCTAssertEqual(try config[Constants.trueKey].decoded(asType: Bool.self), true)
+      XCTAssertEqual(try config[Constants.falseKey].decoded(asType: Bool.self), false)
+      XCTAssertEqual(
+        try config[Constants.stringKey].decoded(asType: Data.self),
+        Constants.stringValue.data(using: .utf8)
+      )
+    }
+
+    func testStrongTypingViaDecoderAlternateDecoderApi() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      let myString: String = try config[Constants.stringKey].decoded()
+      XCTAssertEqual(myString, Constants.stringValue)
+      let myInt: Int = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt, Constants.intValue)
+      let myInt8: Int8 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt8, Int8(Constants.intValue))
+      let myInt16: Int16 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt16, Int16(Constants.intValue))
+      let myInt32: Int32 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt32, Int32(Constants.intValue))
+      let myInt64: Int64 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myInt64, Int64(Constants.intValue))
+      let myUInt: UInt = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt, UInt(Constants.intValue))
+      let myUInt8: UInt8 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt8, UInt8(Constants.intValue))
+      let myUInt16: UInt16 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt16, UInt16(Constants.intValue))
+      let myUInt32: UInt32 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt32, UInt32(Constants.intValue))
+      let myUInt64: UInt64 = try config[Constants.intKey].decoded()
+      XCTAssertEqual(myUInt64, UInt64(Constants.intValue))
+      let myDecimal: Decimal = try config[Constants.floatKey].decoded()
+      XCTAssertEqual(myDecimal, Decimal(Constants.doubleValue))
+      let myFloat: Float = try config[Constants.floatKey].decoded()
+      XCTAssertEqual(myFloat, Constants.floatValue)
+      let myDouble: Double = try config[Constants.floatKey].decoded()
+      XCTAssertEqual(myDouble, Constants.doubleValue)
+      let myTrue: Bool = try config[Constants.trueKey].decoded()
+      XCTAssertEqual(myTrue, true)
+      let myFalse: Bool = try config[Constants.falseKey].decoded()
+      XCTAssertEqual(myFalse, false)
+      let myData: Data = try config[Constants.stringKey].decoded()
+      XCTAssertEqual(myData, Constants.stringValue.data(using: .utf8))
+    }
+
+    func testStringFails() {
+      XCTAssertEqual(config[decodedValue: "UndefinedKey"], "")
+    }
+
+    func testJSONFails() {
+      XCTAssertNil(config[jsonValue: "UndefinedKey"])
+      XCTAssertNil(config[jsonValue: Constants.stringKey])
+    }
+
+    func testDateDecodingNotYetSupported() async throws {
+      let status = try await config.fetchAndActivate()
+      XCTAssertEqual(status, .successFetchedFromRemote)
+      do {
+        let _: Date = try config[Constants.stringKey].decoded()
+      } catch let RemoteConfigValueCodableError.unsupportedType(message) {
+        XCTAssertEqual(message,
+                       "Date type is not currently supported for  Remote Config Value decoding. " +
+                         "Please file a feature request")
+        return
+      }
+      XCTFail("Failed to throw unsupported Date error.")
+    }
+  }
+#endif

+ 6 - 3
FirebaseSharedSwift.podspec

@@ -4,9 +4,11 @@ Pod::Spec.new do |s|
   s.summary                 = 'Shared Swift Extensions for Firebase'
 
   s.description      = <<-DESC
-This pod is for Firebase internal use and not supported for independent use.
+This pod provides capabilities like Codable support that is shared by multiple
+Firebase products. FirebaseSharedSwift is not supported for non-Firebase usage.
                        DESC
 
+
   s.homepage                = 'https://developers.google.com/'
   s.license                 = { :type => 'Apache', :file => 'FirebaseSharedSwift/LICENSE' }
   s.authors                 = 'Google, Inc.'
@@ -36,12 +38,13 @@ This pod is for Firebase internal use and not supported for independent use.
   ]
 
   s.test_spec 'unit' do |unit_tests|
-    unit_tests.scheme = { :code_coverage => true }
     unit_tests.platforms = {
       :ios => ios_deployment_target,
       :osx => osx_deployment_target,
       :tvos => tvos_deployment_target
     }
-    unit_tests.source_files = 'FirebaseSharedSwift/Tests/**/*.swift'
+    unit_tests.source_files = [
+      'FirebaseSharedSwift/Tests/**/*.swift',
+    ]
   end
 end

+ 24 - 0
FirebaseSharedSwift/Sources/FirebaseRemoteConfigValueDecoding.swift

@@ -0,0 +1,24 @@
+// Copyright 2021 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
+
+/// Conform to this protocol for the Firebase Decoder to extract values as from a RemoteConfigValue object.
+public protocol FirebaseRemoteConfigValueDecoding {
+  func numberValue() -> NSNumber
+  func boolValue() -> Bool
+  func stringValue() -> String
+  func dataValue() -> Data
+  func jsonValue() -> [String: AnyHashable]?
+}

+ 54 - 49
FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift

@@ -1251,9 +1251,15 @@ fileprivate class __JSONDecoder : Decoder {
                                         DecodingError.Context(codingPath: self.codingPath,
                                                               debugDescription: "Cannot get keyed decoding container -- found null value instead."))
     }
-
-    guard let topContainer = self.storage.topContainer as? [String : Any] else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
+    var topContainer : [String : Any]
+    if let rcValue = self.storage.topContainer as? FirebaseRemoteConfigValueDecoding,
+       let top = rcValue.jsonValue() {
+      topContainer = top
+    } else {
+      guard let top = self.storage.topContainer as? [String : Any] else {
+        throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
+      }
+      topContainer = top
     }
 
     let container = _JSONKeyedDecodingContainer<Key>(referencing: self, wrapping: topContainer)
@@ -2118,6 +2124,9 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
     guard !(value is NSNull) else { return nil }
 
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.boolValue()
+    }
     if let number = value as? NSNumber {
       // TODO: Add a flag to coerce non-boolean numbers into Bools?
       if number === kCFBooleanTrue as NSNumber {
@@ -2136,13 +2145,25 @@ extension __JSONDecoder {
     throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
   }
 
-  fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
-    guard !(value is NSNull) else { return nil }
+  fileprivate func rcValNumberAdaptor(_ value: Any) -> Any {
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.numberValue()
+    }
+    return value
+  }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
+  fileprivate func getNumber(_ value: Any, as type: Any.Type) throws -> NSNumber {
+    let val = rcValNumberAdaptor(value)
+    guard let number = val as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
+      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: val)
     }
+    return number
+  }
 
+  fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
+    guard !(value is NSNull) else { return nil }
+
+    let number = try getNumber(value, as: type)
     let int = number.intValue
     guard NSNumber(value: int) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2154,10 +2175,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int8 = number.int8Value
     guard NSNumber(value: int8) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2169,10 +2187,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int16 = number.int16Value
     guard NSNumber(value: int16) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2184,10 +2199,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int32 = number.int32Value
     guard NSNumber(value: int32) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2199,10 +2211,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let int64 = number.int64Value
     guard NSNumber(value: int64) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2214,10 +2223,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint = number.uintValue
     guard NSNumber(value: uint) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2229,10 +2235,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint8 = number.uint8Value
     guard NSNumber(value: uint8) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2244,10 +2247,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint16 = number.uint16Value
     guard NSNumber(value: uint16) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2259,10 +2259,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint32 = number.uint32Value
     guard NSNumber(value: uint32) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2274,10 +2271,7 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? {
     guard !(value is NSNull) else { return nil }
 
-    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
-      throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
-    }
-
+    let number = try getNumber(value, as: type)
     let uint64 = number.uint64Value
     guard NSNumber(value: uint64) == number else {
       throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -2289,7 +2283,8 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Float.Type) throws -> Float? {
     guard !(value is NSNull) else { return nil }
 
-    if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
+    let val = rcValNumberAdaptor(value)
+    if let number = val as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
       // We are willing to return a Float by losing precision:
       // * If the original value was integral,
       //   * and the integral value was > Float.greatestFiniteMagnitude, we will fail
@@ -2318,7 +2313,7 @@ extension __JSONDecoder {
        overflow = true
        */
 
-    } else if let string = value as? String,
+    } else if let string = val as? String,
               case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy {
       if string == posInfString {
         return Float.infinity
@@ -2335,7 +2330,8 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Double.Type) throws -> Double? {
     guard !(value is NSNull) else { return nil }
 
-    if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
+    let val = rcValNumberAdaptor(value)
+    if let number = val as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse {
       // We are always willing to return the number as a Double:
       // * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double
       // * If it was a Float or Double, you will get back the precise value
@@ -2353,7 +2349,7 @@ extension __JSONDecoder {
        overflow = true
        */
 
-    } else if let string = value as? String,
+    } else if let string = val as? String,
               case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy {
       if string == posInfString {
         return Double.infinity
@@ -2370,6 +2366,9 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: String.Type) throws -> String? {
     guard !(value is NSNull) else { return nil }
 
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.stringValue()
+    }
     guard let string = value as? String else {
       throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
     }
@@ -2424,6 +2423,10 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Data.Type) throws -> Data? {
     guard !(value is NSNull) else { return nil }
 
+    if let rcValue = value as? FirebaseRemoteConfigValueDecoding {
+      return rcValue.dataValue()
+    }
+
     switch self.options.dataDecodingStrategy {
     case .deferredToData:
       self.storage.push(container: value)
@@ -2451,11 +2454,13 @@ extension __JSONDecoder {
   fileprivate func unbox(_ value: Any, as type: Decimal.Type) throws -> Decimal? {
     guard !(value is NSNull) else { return nil }
 
+    let val = rcValNumberAdaptor(value)
+
     // Attempt to bridge from NSDecimalNumber.
-    if let decimal = value as? Decimal {
+    if let decimal = val as? Decimal {
       return decimal
     } else {
-      let doubleValue = try self.unbox(value, as: Double.self)!
+      let doubleValue = try self.unbox(val, as: Double.self)!
       return Decimal(doubleValue)
     }
   }

+ 40 - 0
Package.swift

@@ -123,6 +123,10 @@ let package = Package(
       name: "FirebaseRemoteConfig",
       targets: ["FirebaseRemoteConfig"]
     ),
+    .library(
+      name: "FirebaseRemoteConfigSwift-Beta",
+      targets: ["FirebaseRemoteConfigSwift"]
+    ),
     .library(
       name: "FirebaseStorage",
       targets: ["FirebaseStorage"]
@@ -938,6 +942,8 @@ let package = Package(
       ]
     ),
 
+    // MARK: - Firebase Remote Config
+
     .target(
       name: "FirebaseRemoteConfig",
       dependencies: [
@@ -973,6 +979,40 @@ let package = Package(
         .headerSearchPath("../../.."),
       ]
     ),
+    .target(
+      name: "FirebaseRemoteConfigSwift",
+      dependencies: [
+        "FirebaseRemoteConfig",
+        "FirebaseSharedSwift",
+      ],
+      path: "FirebaseRemoteConfigSwift/Sources"
+    ),
+    .testTarget(
+      name: "RemoteConfigFakeConsole",
+      dependencies: ["FirebaseRemoteConfigSwift",
+                     "RemoteConfigFakeConsoleObjC"],
+      path: "FirebaseRemoteConfigSwift/Tests",
+      exclude: [
+        "AccessToken.json",
+        "README.md",
+        "ObjC/",
+      ],
+      cSettings: [
+        .headerSearchPath("../../"),
+      ]
+    ),
+    .target(
+      name: "RemoteConfigFakeConsoleObjC",
+      dependencies: ["OCMock"],
+      path: "FirebaseRemoteConfigSwift/Tests/ObjC",
+      publicHeadersPath: ".",
+      cSettings: [
+        .headerSearchPath("../../../"),
+      ]
+    ),
+
+    // MARK: - Firebase Storage
+
     .target(
       name: "FirebaseStorage",
       dependencies: [

+ 2 - 1
ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift

@@ -23,6 +23,7 @@ import Foundation
 public let shared = Manifest(
   version: "8.11.0",
   pods: [
+    Pod("FirebaseSharedSwift"),
     Pod("FirebaseCoreDiagnostics", zip: true),
     Pod("FirebaseCore", zip: true),
     Pod("FirebaseInstallations", zip: true),
@@ -32,7 +33,7 @@ public let shared = Manifest(
     Pod("FirebaseABTesting", zip: true),
     Pod("FirebaseAppCheck", isBeta: true, zip: true),
     Pod("FirebaseRemoteConfig", zip: true),
-    Pod("FirebaseSharedSwift"),
+    Pod("FirebaseRemoteConfigSwift", isBeta: true),
     Pod("FirebaseAppDistribution", isBeta: true, platforms: ["ios"], zip: true),
     Pod("FirebaseAuth", zip: true),
     Pod("FirebaseCrashlytics", zip: true),

+ 3 - 2
SharedTestUtilities/FIROptionsMock.m

@@ -18,7 +18,8 @@
 #import "SharedTestUtilities/FIROptionsMock.h"
 
 NSString *const kAndroidClientID = @"correct_android_client_id";
-NSString *const kAPIKey = @"correct_api_key";
+// FIS requires 39 characters starting with A.
+NSString *const kAPIKey = @"A23456789012345678901234567890123456789";
 NSString *const kCustomizedAPIKey = @"customized_api_key";
 NSString *const kClientID = @"correct_client_id";
 NSString *const kTrackingID = @"correct_tracking_id";
@@ -31,7 +32,7 @@ NSString *const kDeepLinkURLScheme = @"comgoogledeeplinkurl";
 NSString *const kNewDeepLinkURLScheme = @"newdeeplinkurlfortest";
 
 NSString *const kBundleID = @"com.google.FirebaseSDKTests";
-NSString *const kProjectID = @"abc-xyz-123";
+NSString *const kProjectID = @"Mocked Project ID";
 
 @interface FIROptionsMock ()
 

+ 4 - 4
SwiftDashboard.md

@@ -9,11 +9,11 @@ tasks for additional Swift improvements.
 
 |                       | AB  | An     | ApC    | ApD    | Aut    | Cor    | Crs    | DB     | Fst    | Fn     | IAM    | Ins    | Msg    | MLM    | Prf    | RC     |    Str |
 |   :---                | :--- | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: | :----: |
-| **Swift Library**     | ❌   |   ✔   | ❌     |❌     | ❌     | ❌     | ❌      |  ✔     |  ✔    | 1      |  ✔     | ❌    | ❌     | ✔      | ❌     |     | ✔     |
+| **Swift Library**     | ❌   |   ✔   | ❌     |❌     | ❌     | ❌     | ❌      |  ✔     |  ✔    | 1      |  ✔     | ❌    | ❌     | ✔      | ❌     |     | ✔     |
 | **API Tests**         | ❌   |  ❌    |  ✔    |❌     | ❌     | ✔       | ❌     | 3      | 2     |  ✔     | 2      | ✔      | 2     | 2      | ❌    |  ✔     | ✔    |
-| **async/await**       | ❌   |  n/a   |  ✔    |❌     | ❌     |  ✔      | ❌     | 3     | ✔     |  ✔     | ❌     | ❌    | ✔     | ❌    | ❌     |  ✔    | ✔    |
-| **Swift Errors**      |  ❌  |  ❌    | ❌    |❌     | 4      | ❌     | ❌     | ❌     | ❌    | ❌     | ❌     | ❌    | ❌     | ✔      | ❌     |    | 5   |
-| **Codable**           | n/a  | n/a     | n/a   |n/a    | n/a     | n/a    |n/a     |  ✔     |  ✔     | 1      | n/a     | n/a   | ❌     | n/a    | n/a    |    |n/a   |
+| **async/await**       | ❌   |  n/a   |  ✔    |❌     | ❌     |  ✔      | ❌     | 3     |  ✔     |  ✔     | ❌     | ❌    | ✔     | ❌    | ❌     |  ✔    | ✔    |
+| **Swift Errors**      |  ❌  |  ❌    | ❌    |❌     | 4      | ❌     | ❌     | ❌     | ❌    | ❌     | ❌     | ❌    | ❌     | ✔      | ❌     |    | 5   |
+| **Codable**           | n/a  | n/a     | n/a   |n/a    | n/a     | n/a    |n/a     |  ✔     |  ✔     | 1      | n/a     | n/a   | ❌     | n/a    | n/a    |    |n/a   |
 | **SwiftUI Lifecycle** | n/a  |  ❌    | n/a    |❌     | ❌     | n/a    |n/a     | n/a    | n/a    | n/a     | n/a    | n/a   | ❌     | n/a    | n/a    | n/a   |n/a  |
 | **SwiftUI Interop**   | ❌   |  ✔     | ❌     |❌    | ❌     | ❌     |❌      | ❌     | ❌    | ❌     | ✔      | ❌    | ❌     | ❌    | ❌     | ❌    |n/a  |
 | **Property Wrappers** |  ❌  |  ❌    | ❌    |❌     | ❌     | ❌     | ❌     | ❌     | 6     | ❌     | ❌     | ❌    | ❌     | ❌    | ❌     | ❌   |❌    |

+ 12 - 27
scripts/build.sh

@@ -484,48 +484,33 @@ case "$product-$platform-$method" in
       test
     ;;
 
-  RemoteConfig-*-unit)
-    pod_gen FirebaseRemoteConfig.podspec --platforms="${gen_platform}"
-    RunXcodebuild \
-      -workspace 'gen/FirebaseRemoteConfig/FirebaseRemoteConfig.xcworkspace' \
-      -scheme "FirebaseRemoteConfig-Unit-unit" \
-      "${xcb_flags[@]}" \
-      build \
-      test
-    ;;
-
   RemoteConfig-*-fakeconsole)
-    pod_gen FirebaseRemoteConfig.podspec --platforms="${gen_platform}"
-
-    # Add GoogleService-Info.plist to generated Test Wrapper App.
-    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfig/Pods/Pods.xcodeproj \
-      AppHost-FirebaseRemoteConfig-Unit-Tests \
-      ../../../FirebaseRemoteConfig/Tests/FakeUtils/GoogleService-Info.plist
+    pod_gen FirebaseRemoteConfigSwift.podspec --platforms="${gen_platform}"
 
     RunXcodebuild \
-      -workspace 'gen/FirebaseRemoteConfig/FirebaseRemoteConfig.xcworkspace' \
-      -scheme "FirebaseRemoteConfig-Unit-fake-console-tests" \
+      -workspace 'gen/FirebaseRemoteConfigSwift/FirebaseRemoteConfigSwift.xcworkspace' \
+      -scheme "FirebaseRemoteConfigSwift-Unit-fake-console-tests" \
       "${xcb_flags[@]}" \
       build \
       test
     ;;
 
   RemoteConfig-*-integration)
-    pod_gen FirebaseRemoteConfig.podspec --platforms="${gen_platform}"
+    pod_gen FirebaseRemoteConfigSwift.podspec --platforms="${gen_platform}"
 
     # Add GoogleService-Info.plist to generated Test Wrapper App.
-    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfig/Pods/Pods.xcodeproj \
-      AppHost-FirebaseRemoteConfig-Unit-Tests \
-      ../../../FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist
+    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfigSwift/Pods/Pods.xcodeproj \
+      AppHost-FirebaseRemoteConfigSwift-Unit-Tests \
+      ../../../FirebaseRemoteConfigSwift/Tests/SwiftAPI/GoogleService-Info.plist
 
     # Add AccessToken to generated Test Wrapper App.
-    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfig/Pods/Pods.xcodeproj \
-      AppHost-FirebaseRemoteConfig-Unit-Tests \
-      ../../../FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json
+    ruby ./scripts/update_xcode_target.rb gen/FirebaseRemoteConfigSwift/Pods/Pods.xcodeproj \
+      AppHost-FirebaseRemoteConfigSwift-Unit-Tests \
+      ../../../FirebaseRemoteConfigSwift/Tests/AccessToken.json
 
     RunXcodebuild \
-      -workspace 'gen/FirebaseRemoteConfig/FirebaseRemoteConfig.xcworkspace' \
-      -scheme "FirebaseRemoteConfig-Unit-swift-api-tests" \
+      -workspace 'gen/FirebaseRemoteConfigSwift/FirebaseRemoteConfigSwift.xcworkspace' \
+      -scheme "FirebaseRemoteConfigSwift-Unit-swift-api-tests" \
       "${xcb_flags[@]}" \
       build \
       test

+ 1 - 1
scripts/health_metrics/file_patterns.json

@@ -171,7 +171,7 @@
   },
   {
     "sdk": "remoteconfig",
-    "podspecs": ["FirebaseRemoteConfig.podspec"],
+    "podspecs": ["FirebaseRemoteConfig.podspec", "FirebaseRemoteConfigSwift.podspec"],
     "filePatterns": [
       "^FirebaseRemoteConfig.*",
       "Interop/Analytics/Public/[^/]+\\.h",

+ 77 - 0
scripts/spm_test_schemes/RemoteConfigFakeConsole.xcscheme

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1320"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "RemoteConfigFakeConsole"
+               BuildableName = "RemoteConfigFakeConsole"
+               BlueprintName = "RemoteConfigFakeConsole"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "RemoteConfigFakeConsole"
+               BuildableName = "RemoteConfigFakeConsole"
+               BlueprintName = "RemoteConfigFakeConsole"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "RemoteConfigFakeConsole"
+            BuildableName = "RemoteConfigFakeConsole"
+            BlueprintName = "RemoteConfigFakeConsole"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>