From 05e2f2f102468666002aaa8b9385a8f7fb7bfe0d Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Sun, 19 Dec 2021 21:56:28 +0900 Subject: [PATCH 1/4] just code copy from SwiftyRemoteConfig --- FirebaseRemoteConfigSwift/CHANGELOG.md | 313 ++++++++++++++++++ .../Sources/BuiltIns.swift | 85 +++++ .../Sources/RemoteConfig+Subscripts.swift | 76 +++++ .../Sources/RemoteConfig.swift | 18 + .../Sources/RemoteConfigAdapter.swift | 35 ++ .../Sources/RemoteConfigBridge.swift | 239 +++++++++++++ .../Sources/RemoteConfigKey.swift | 46 +++ .../Sources/RemoteConfigKeys.swift | 15 + .../Sources/RemoteConfigSerializable.swift | 18 + .../Sources/Utils/OptionalType.swift | 33 ++ .../Tests/BuiltIns/RemoteConfig+Bool.swift | 29 ++ .../Tests/BuiltIns/RemoteConfig+Data.swift | 29 ++ .../Tests/BuiltIns/RemoteConfig+Double.swift | 29 ++ .../Tests/BuiltIns/RemoteConfig+Int.swift | 29 ++ .../Tests/BuiltIns/RemoteConfig+String.swift | 29 ++ .../Tests/BuiltIns/RemoteConfig+URL.swift | 29 ++ .../External types/RemoteConfig+Codable.swift | 29 ++ .../External types/RemoteConfig+Color.swift | 42 +++ .../RemoteConfig+CustomSerializable.swift | 29 ++ .../External types/RemoteConfig+Enum.swift | 29 ++ .../RemoteConfig+Serializable.swift | 29 ++ .../RemoteConfigSerializableSpec.swift | 155 +++++++++ .../Tests/Helpers/TestHelper.swift | 122 +++++++ 23 files changed, 1487 insertions(+) create mode 100644 FirebaseRemoteConfigSwift/CHANGELOG.md create mode 100644 FirebaseRemoteConfigSwift/Sources/BuiltIns.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift create mode 100644 FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift create mode 100644 FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift diff --git a/FirebaseRemoteConfigSwift/CHANGELOG.md b/FirebaseRemoteConfigSwift/CHANGELOG.md new file mode 100644 index 00000000000..54563b5b9e1 --- /dev/null +++ b/FirebaseRemoteConfigSwift/CHANGELOG.md @@ -0,0 +1,313 @@ +# SwiftyRemoteConfig + +**Modern Swift API for `FirebaseRemoteConfig`** + +SwiftyRemoteConfig makes Firebase Remote Config enjoyable to use by combining expressive Swifty API with the benefits fo static typing. This library is strongly inspired by [SwiftyUserDefaults](https://github.com/sunshinejr/SwiftyUserDefaults). + +## Features + +There is only one step to start using SwiftyRemoteConfig. + +Define your Keys ! + +```swift +extension RemoteConfigKeys { + var recommendedAppVersion: RemoteConfigKey { .init("recommendedAppVersion")} + var isEnableExtendedFeature: RemoteConfigKey { .init("isEnableExtendedFeature", defaultValue: false) } +} +``` + +... and just use it ! + +```swift +// get remote config value easily +let recommendedVersion = RemoteConfigs[.recommendedAppVersion] + +// eality work with custom deserialized types +let themaColor: UIColor = RemoteConfigs[.themaColor] +``` + +If you use Swift 5.1 or later, you can also use keyPath `dynamicMemberLookup`: + +```swift +let subColor: UIColor = RemoteConfigs.subColor +``` + +## Usage + +### Define your keys + +To get the most out of SwiftyRemoteConfig, define your remote config keys ahead of time: + +```swift +let flag = RemoteConfigKey("flag", defaultValue: false) +``` + +Just create a `RemoteConfigKey` object. If you want to have a non-optional value, just provide a `defaultValue` in the key (look at the example above). + +You can now use `RemoteConfig` shortcut to access those values: + +```swift +RemoteConfigs[key: flag] // => false, type as "Bool" +``` + +THe compiler won't let you fetching conveniently returns `Bool`. + +### Take shortcuts + +For extra convenience, define your keys by extending magic `RemoteConfigKeys` class and adding static properties: + +```swift +extension RemoteConfigKeys { + var flag: RemoteConfigKey { .init("flag", defaultValue: false) } + var userSectionName: RemoteConfigKey { .init("default") } +} +``` + +and use the shortcut dot syntax: + +```swift +RemoteConfigs[\.flag] // => false +``` + +### Supported types + +SwiftyRemoteConfig supports standard types as following: + +| Single value | Array | +|:---:|:---:| +| `String` | `[String]` | +| `Int` | `[Int]` | +| `Double` | `[Double]` | +| `Bool` | `[Bool]` | +| `Data` | `[Data]` | +| `Date` | `[Date]` | +| `URL` | `[URL]` | +| `[String: Any]` | `[[String: Any]]` | + +and that's not all ! + +## Extending existing types + +### Codable + +`SwiftyRemoteConfig` supports `Codable` ! Just conform to `RemoteConfigSerializable` in your type: + +```swift +final class UserSection: Codable, RemoteConfigSerializable { + let name: String +} +``` + +No implementation needed ! By doing this you will get an option to specify an optional `RemoteConfigKey`: + +```swift +let userSection = RemoteConfigKey("userSection") +``` + +Additionally, you've get an array support for free: + +```swift +let userSections = RemoteConfigKey<[UserSection]?>("userSections") +``` + +### NSCoding + +Support your custom NSCoding type the same way as with Codable support: + +```swift +final class UserSection: NSObject, NSCoding, RemoteCOnfigSerializable { + ... +} +``` + +### RawRepresentable + +And the last, `RawRepresentable` support ! Again, the same situation like with `Codable` and `NSCoding`: + +```swift +enum UserSection: String, RemoteConfigSerializable { + case Basic + case Royal +} +``` + +### Custom types + +If you want to add your own custom type that we don't support yet. we've got you covered. We use `RemoteConfigBridge` s of many kinds to specify how you get values and arrays of values. WHen you look at `RemoteConfigSerializable` protocol, it expects two properties in eacy type: `_remoteConfig` and `_remoteConfigArray`, where both are of type `RemoteConfigBridge`. + +For instance, this is a bridge for single value data retrieving using `NSKeyedUnarchiver`: + +```swift +public struct RemoteConfigKeyedArchiveBridge: RemoteConfigBridge { + + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + remoteConfig.data(forKey: key).flatMap(NSKyedUnarchiver.unarchiveObject) as? T + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + guard let data = object as? Data else { + return nil + } + + NSKyedUnarchiver.unarchiveObject(with: data) + } +} +``` + +Bridge for default retrieving array values: + +```swift +public struct RemoteConfigArrayBridge: RemoteConfigBridge { + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + remoteConfig.array(forKey: key) as? T + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + return nil + } +} +``` + +Now, to use these bridges in your type you simply declare it as follows: + +```swift +struct CustomSerializable: RemoteConfigSerializable { + static var _remoteConfig: RemoteConfigBridge { RemoteConfigKeyedArchiverBridge() } + static var _remoteConfigArray: RemoteConfigBridge<[CustomSerializable]> { RemoteConfigKeyedArchiverBridge() } + + let key: String +} +``` + +Unfortunately, if you find yourself in a situation where you need a custom bridge, you'll probably need to write your own: + +```swift +final class RemoteConfigCustomBridge: RemoteConfigBridge { + func get(key: String, remoteConfig: RemoteConfig) -> RemoteConfigCustomSerializable? { + let value = remoteConfig.string(forKey: key) + return value.map(RemoteConfigCustomSerializable.init) + } + + func deserializa(_ object: Any) -> RemoteConfigCustomSerializable? { + guard let value = object as? String { + return nil + } + + return RemoteConfigCustomSerializable(value: value) + } +} + +final class RemoteConfigCustomArrayBridge: RemoteConfigBridge { + func get(key: String, remoteConfig: RemoteConfig) -> [RemoteConfigCustomSerializable]? { + remoteConfig.array(forKey: key)? + .compactMap({ $0 as? String }) + .map(RemoteConfigCustomSerializable.init) + } + + func deserializa(_ object: Any) -> [RemoteConfigCustomSerializable]? { + guard let values as? [String] else { + return nil + } + + return values.map({ RemoteConfigCustomSerializable.init }) + } +} + +struct RemoteConfigCustomSerializable: RemoteConfigSerializable, Equatable { + static var _remoteConfig: RemoteConfigCustomBridge { RemoteConfigCustomBridge() } + static var _remoteConfigArrray: RemoteConfigCustomArrayBridge: { RemoteConfigCustomArrayBridge() } + + let value: String +} +``` + +To support existing types with different bridges, you can extend it similarly: + +```swift +extension Data: RemoteConfigSerializable { + public static var _remoteConfigArray: RemoteConfigArrayBridge<[T]> { RemoteConfigArrayBridge() } + public static var _remoteConfig: RemoteConfigBridge { RemoteConfigBridge() } +}d +``` +Also, take a look at our source code or tests to see more examples of bridges. If you find yourself confused with all these bridges, please create an issue and we will figure something out. + +## KeyPath dynamicMemberLookup + +SwiftyRemoteConfig makes KeyPath dynamicMemberLookpu usable in Swift 5.1. + +```swift +extension RemoteConfigKeys { + var recommendedAppVersion: RemoteConfigKey { .init("recommendedAppVersion")} + var themaColor: RemoteConfigKey { .init("themaColor", defaultValue: .white) } +} +``` + +and just use it ;-) + +```swift +// get remote config value easily +let recommendedVersion = RemoteConfig.recommendedAppVersion + +// eality work with custom deserialized types +let themaColor: UIColor = RemoteConfig.themaColor +``` + +## Dependencies + +- **Swift** version >= 5.0 + +### SDKs + +- **iOS** version >= 11.0 + +### Frameworks + +- **Firebase iOS SDK** >= 8.0.0 +- **StoreKit** + +## Installation + +### Cocoapods + +If you're using Cocoapods, just add this line to your `Podfile`: + +```ruby +pod 'SwiftyRemoteConfig`, `~> 0.0.2` +``` + +Install by running this command in your terminal: + +```sh +$ pod install +``` + +Then import the library in all files where you use it: + +```swift +import SwiftyRemoteConfig +``` + +### Carthage + +Just add your Cartfile + +``` +github "fumito-ito/SwiftyRemoteConfig" ~> 0.0.2 +``` + +### Swift Package Manager + +Just add to your `Package.swift` under dependencies + +```swift +let package = Package( + name: "MyPackage", + products: [...], + dependencies: [ + .package(url: "https://github.com/fumito-ito/SwiftyRemoteConfig.git", .upToNextMajor(from: "0.0.2")) + ] +) +``` + +SwiftyRemoteConfig is available under the Apache License 2.0. See the LICENSE file for more detail. diff --git a/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift b/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift new file mode 100644 index 00000000000..5a3823a8210 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift @@ -0,0 +1,85 @@ +// +// BuiltIns.swift +// SwiftyRemoteConfigExample +// +// Created by 伊藤史 on 2020/08/25. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation + +extension RemoteConfigSerializable { + public static var _remoteConfigArray: RemoteConfigArrayBridge<[T]> { RemoteConfigArrayBridge() } +} + +extension Date: RemoteConfigSerializable { + public static var _remoteConfig: RemoteConfigObjectBridge { RemoteConfigObjectBridge() } +} + +extension String: RemoteConfigSerializable { + public static var _remoteConfig: RemoteConfigStringBridge { RemoteConfigStringBridge() } +} + +extension Int: RemoteConfigSerializable { + public static var _remoteConfig: RemoteConfigIntBridge { RemoteConfigIntBridge() } +} + +extension Double: RemoteConfigSerializable { + public static var _remoteConfig: RemoteConfigDoubleBridge { return RemoteConfigDoubleBridge() } +} + +extension Bool: RemoteConfigSerializable { + public static var _remoteConfig: RemoteConfigBoolBridge { RemoteConfigBoolBridge() } +} + +extension Data: RemoteConfigSerializable { + public static var _remoteConfig: RemoteConfigDataBridge { RemoteConfigDataBridge() } +} + +extension URL: RemoteConfigSerializable { + public static var _remoteConfig: RemoteConfigUrlBridge { RemoteConfigUrlBridge() } + public static var _remoteConfigArray: RemoteConfigCodableBridge<[URL]> { RemoteConfigCodableBridge() } +} + +extension RemoteConfigSerializable where Self: Codable { + public static var _remoteConfig: RemoteConfigCodableBridge { RemoteConfigCodableBridge() } + public static var _remoteConfigArray: RemoteConfigCodableBridge<[Self]> { RemoteConfigCodableBridge() } +} + +extension RemoteConfigSerializable where Self: RawRepresentable { + public static var _remoteConfig: RemoteConfigRawRepresentableBridge { RemoteConfigRawRepresentableBridge() } + public static var _remoteConfigArray: RemoteConfigRawRepresentableArrayBridge<[Self]> { RemoteConfigRawRepresentableArrayBridge() } +} + +extension RemoteConfigSerializable where Self: NSCoding { + public static var _remoteConfig: RemoteConfigKeyedArchiverBridge { RemoteConfigKeyedArchiverBridge() } + public static var _remoteConfigArray: RemoteConfigKeyedArchiverBridge<[Self]> { RemoteConfigKeyedArchiverBridge() } +} + +extension Dictionary: RemoteConfigSerializable where Key == String { + public typealias T = [Key: Value] + public typealias Bridge = RemoteConfigObjectBridge + public typealias ArrayBridge = RemoteConfigArrayBridge<[T]> + + public static var _remoteConfig: Bridge { Bridge() } + public static var _remoteConfigArray: ArrayBridge { ArrayBridge() } +} + +extension Array: RemoteConfigSerializable where Element: RemoteConfigSerializable { + public typealias T = [Element.T] + public typealias Bridge = Element.ArrayBridge + public typealias ArrayBridge = RemoteConfigObjectBridge<[T]> + + public static var _remoteConfig: Bridge { Element._remoteConfigArray } + public static var _remoteConfigArray: ArrayBridge { + fatalError("Multidimensional arrays are not supported yet") + } +} + +extension Optional: RemoteConfigSerializable where Wrapped: RemoteConfigSerializable { + public typealias Bridge = RemoteConfigOptionalBridge + public typealias ArrayBridge = RemoteConfigOptionalBridge + + public static var _remoteConfig: Bridge { RemoteConfigOptionalBridge(bridge: Wrapped._remoteConfig) } + public static var _remoteConfigArray: ArrayBridge { RemoteConfigOptionalBridge(bridge: Wrapped._remoteConfigArray) } +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift new file mode 100644 index 00000000000..1f4016170cb --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift @@ -0,0 +1,76 @@ +// +// Defaults+Subscripts.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/21. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation +import FirebaseRemoteConfig + +public extension RemoteConfigAdapter { + + subscript(key: RemoteConfigKey) -> T.T where T: OptionalType, T.T == T { + get { + return self.remoteConfig[key] + } + } + + subscript(key: RemoteConfigKey) -> T.T where T.T == T { + get { + return self.remoteConfig[key] + } + } + + subscript(keyPath: KeyPath>) -> T.T where T: OptionalType, T.T == T { + get { + return self.remoteConfig[self.keyStore[keyPath: keyPath]] + } + } + + subscript(keyPath: KeyPath>) -> T.T where T.T == T { + get { + return self.remoteConfig[self.keyStore[keyPath: keyPath]] + } + } + + subscript(dynamicMember keyPath: KeyPath>) -> T.T where T: OptionalType, T.T == T { + get { + return self[keyPath] + } + } + + subscript(dynamicMember keyPath: KeyPath>) -> T.T where T.T == T { + get { + return self[keyPath] + } + } +} + +public extension RemoteConfig { + + subscript(key: RemoteConfigKey) -> T.T where T: OptionalType, T.T == T { + get { + if let value = T._remoteConfig.get(key: key._key, remoteConfig: self), let _value = value as? T.T.Wrapped { + return _value as! T + } else if let defaultValue = key.defaultValue { + return defaultValue + } else { + return T.T.empty + } + } + } + + subscript(key: RemoteConfigKey) -> T.T where T.T == T { + get { + if let value = T._remoteConfig.get(key: key._key, remoteConfig: self) { + return value + } else if let defaultValue = key.defaultValue { + return defaultValue + } else { + fatalError("Unexpected path is executed. please report to https://github.com/fumito-ito/SwiftyRemoteConfig") + } + } + } +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift new file mode 100644 index 00000000000..61f57034a7c --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift @@ -0,0 +1,18 @@ +// +// RemoteConfig.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/13. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation +import FirebaseRemoteConfig + +public var RemoteConfigs = RemoteConfigAdapter(remoteConfig: RemoteConfig.remoteConfig(), keyStore: .init()) + +public extension RemoteConfig { + func hasKey(_ key: RemoteConfigKey) -> Bool { + self.configValue(forKey: key._key).stringValue != nil + } +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift new file mode 100644 index 00000000000..ced810bbb94 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift @@ -0,0 +1,35 @@ +// +// RemoteConfigAdapter.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/15. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation +import FirebaseRemoteConfig + +@dynamicMemberLookup +public struct RemoteConfigAdapter { + + public let remoteConfig: RemoteConfig + public let keyStore: KeyStore + + public init(remoteConfig: RemoteConfig, keyStore: KeyStore) { + self.remoteConfig = remoteConfig + self.keyStore = keyStore + } + + @available(*, unavailable) + public subscript(dynamicMember member: String) -> Never { + fatalError() + } + + public func hasKey(_ key: RemoteConfigKey) -> Bool { + return self.remoteConfig.hasKey(key) + } + + public func hasKey(_ keyPath: KeyPath>) -> Bool { + return self.remoteConfig.hasKey(self.keyStore[keyPath: keyPath]) + } +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift new file mode 100644 index 00000000000..be0d9aeb157 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift @@ -0,0 +1,239 @@ +// +// RemoteConfigBridge.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/13. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation +import FirebaseRemoteConfig + +public protocol RemoteConfigBridge { + associatedtype T + + func get(key: String, remoteConfig: RemoteConfig) -> T? + func deserialize(_ object: RemoteConfigValue) -> T? +} + +public struct RemoteConfigObjectBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return remoteConfig.configValue(forKey: key) as? T + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + return nil + } +} + +public struct RemoteConfigArrayBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return remoteConfig.configValue(forKey: key) as? T + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + return nil + } +} + +public struct RemoteConfigStringBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> String? { + let configValue = remoteConfig.configValue(forKey: key) + + if configValue.stringValue?.isEmpty == true || configValue.stringValue.isNil { + return nil + } + + return configValue.stringValue + } + + public func deserialize(_ object: RemoteConfigValue) -> String? { + return nil + } +} + +public struct RemoteConfigIntBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> Int? { + let configValue = remoteConfig.configValue(forKey: key) + + if configValue.stringValue?.isEmpty == true || configValue.stringValue.isNil { + return nil + } + + return configValue.numberValue.intValue + } + + public func deserialize(_ object: RemoteConfigValue) -> Int? { + return nil + } +} + +public struct RemoteConfigDoubleBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> Double? { + let configValue = remoteConfig.configValue(forKey: key) + + if configValue.stringValue?.isEmpty == true || configValue.stringValue.isNil { + return nil + } + + return configValue.numberValue.doubleValue + } + + public func deserialize(_ object: RemoteConfigValue) -> Double? { + return nil + } +} + +public struct RemoteConfigBoolBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> Bool? { + let configValue = remoteConfig.configValue(forKey: key) + + if configValue.stringValue?.isEmpty == true || configValue.stringValue.isNil { + return nil + } + + return remoteConfig.configValue(forKey: key).boolValue + } + + public func deserialize(_ object: RemoteConfigValue) -> Bool? { + return nil + } +} + +public struct RemoteConfigDataBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> Data? { + let dataValue = remoteConfig.configValue(forKey: key).dataValue + return dataValue.isEmpty ? nil : dataValue + } + + public func deserialize(_ object: RemoteConfigValue) -> Data? { + return nil + } +} + +public struct RemoteConfigUrlBridge: RemoteConfigBridge { + public init() {} + + public func get(key: String, remoteConfig: RemoteConfig) -> URL? { + return self.deserialize(remoteConfig.configValue(forKey: key)) + } + + public func deserialize(_ object: RemoteConfigValue) -> URL? { + if let url = NSKeyedUnarchiver.unarchiveObject(with: object.dataValue) as? URL { + return url + } + + if let stringValue = object.stringValue, stringValue.isEmpty == false { + if let url = URL(string: stringValue) { + return url + } + + let path = (stringValue as NSString).expandingTildeInPath + return URL(fileURLWithPath: path) + } + + return nil + } +} + +public struct RemoteConfigCodableBridge: RemoteConfigBridge { + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return self.deserialize(remoteConfig.configValue(forKey: key)) + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + return try? JSONDecoder().decode(T.self, from: object.dataValue) + } +} + +public struct RemoteConfigKeyedArchiverBridge: RemoteConfigBridge { + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return self.deserialize(remoteConfig.configValue(forKey: key)) + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + return NSKeyedUnarchiver.unarchiveObject(with: object.dataValue) as? T + } +} + +public struct RemoteConfigRawRepresentableBridge: RemoteConfigBridge { + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return self.deserialize(remoteConfig.configValue(forKey: key)) + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + if let rawValue = object.stringValue as? T.RawValue { + return T(rawValue: rawValue) + } + + if let rawValue = object.numberValue as? T.RawValue { + return T(rawValue: rawValue) + } + + return nil + } +} + +public struct RemoteConfigRawRepresentableArrayBridge: RemoteConfigBridge where T.Element: RawRepresentable { + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return self.deserialize(remoteConfig.configValue(forKey: key)) + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + guard let rawValues = object.jsonValue as? [T.Element.RawValue] else { + return nil + } + + return rawValues.compactMap({ T.Element(rawValue: $0) }) as? T + } +} + +public struct RemoteConfigOptionalBridge: RemoteConfigBridge { + public typealias T = Bridge.T? + + private let bridge: Bridge + + public init(bridge: Bridge) { + self.bridge = bridge + } + + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return self.bridge.get(key: key, remoteConfig: remoteConfig) + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + return self.bridge.deserialize(object) + } +} + +public struct RemoteConfigOptionalArrayBridge: RemoteConfigBridge where Bridge.T: Collection { + public typealias T = Bridge.T + + private let bridge: Bridge + + public init(bridge: Bridge) { + self.bridge = bridge + } + + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return self.bridge.get(key: key, remoteConfig: remoteConfig) + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + return self.bridge.deserialize(object) + } +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift new file mode 100644 index 00000000000..aa0f5726624 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift @@ -0,0 +1,46 @@ +// +// RemoteConfigKey.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/15. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation + +public struct RemoteConfigKey { + + public let _key: String + public let defaultValue: ValueType.T? + internal var isOptional: Bool + + public init(_ key: String, defaultValue: ValueType.T) { + self._key = key + self.defaultValue = defaultValue + self.isOptional = false + } + + private init(key: String) { + self._key = key + self.defaultValue = nil + self.isOptional = true + } + + @available(*, unavailable, message: "This key needs a `defaultValue` parameter. If this type does not have a default value, consider using an optional key.") + public init(_ key: String) { + fatalError() + } +} + +public extension RemoteConfigKey where ValueType: RemoteConfigSerializable, ValueType: OptionalType, ValueType.Wrapped: RemoteConfigSerializable { + + init(_ key: String) { + self.init(key: key) + } + + init(_ key: String, defaultValue: ValueType.T) { + self._key = key + self.defaultValue = defaultValue + self.isOptional = true + } +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift new file mode 100644 index 00000000000..bf670208f22 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift @@ -0,0 +1,15 @@ +// +// RemoteConfigKeys.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/15. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation + +public protocol RemoteConfigKeyStore {} + +public struct RemoteConfigKeys: RemoteConfigKeyStore { + public init() {} +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift new file mode 100644 index 00000000000..e72133ffdf2 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift @@ -0,0 +1,18 @@ +// +// RemoteConfigSerializable.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/15. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +import Foundation + +public protocol RemoteConfigSerializable { + typealias T = Bridge.T + associatedtype Bridge: RemoteConfigBridge + associatedtype ArrayBridge: RemoteConfigBridge + + static var _remoteConfig: Bridge { get } + static var _remoteConfigArray: ArrayBridge { get } +} diff --git a/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift b/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift new file mode 100644 index 00000000000..76596d44597 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift @@ -0,0 +1,33 @@ +// +// OptionalType.swift +// SwiftyRemoteConfig +// +// Created by 伊藤史 on 2020/08/21. +// Copyright © 2020 Fumito Ito. All rights reserved. +// + +protocol OptionalTypeCheck { + var isNil: Bool { get } +} + +public protocol OptionalType { + associatedtype Wrapped + + var wrapped: Wrapped? { get } + + static var empty: Self { get } +} + +extension Optional: OptionalType, OptionalTypeCheck { + public var wrapped: Wrapped? { + return self + } + + public static var empty: Optional { + return nil + } + + var isNil: Bool { + return self == nil + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift new file mode 100644 index 00000000000..a89adc8a593 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift @@ -0,0 +1,29 @@ +// +// RemoteConfig+Bool.swift +// +// +// Created by 伊藤史 on 2021/11/18. +// + +import Foundation + +final class RemoteConfigBoolSpec: RemoteConfigSerializableSpec { + var defaultValue: Bool = true + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift new file mode 100644 index 00000000000..55fe05b83bf --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation + +final class RemoteConfigDataSpec: RemoteConfigSerializableSpec { + var defaultValue: Data = Data() + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift new file mode 100644 index 00000000000..b4a4a504b1d --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation + +final class RemoteConfigDoubleSpec: RemoteConfigSerializableSpec { + var defaultValue: Double = 1.0 + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift new file mode 100644 index 00000000000..04aeb03d612 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation + +final class RemoteConfigIntSpec: RemoteConfigSerializableSpec { + var defaultValue: Int = 1 + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift new file mode 100644 index 00000000000..2e5f8ffccb2 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation + +final class RemoteConfigStringSpec: RemoteConfigSerializableSpec { + var defaultValue: String = "Firebase" + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift new file mode 100644 index 00000000000..3d4007e5b4f --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation + +final class RemoteConfigURLSpec: RemoteConfigSerializableSpec { + var defaultValue: URL = URL(string: "https://console.firebase.google.com/")! + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift new file mode 100644 index 00000000000..c200c353666 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation + +final class RemoteConfigCodableSpec: RemoteConfigSerializableSpec { + var defaultValue: FrogCodable = FrogCodable(name: "default") + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift new file mode 100644 index 00000000000..4a602481ff8 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift @@ -0,0 +1,42 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation +@testable import SwiftyRemoteConfig + +#if canImport(UIKit) || canImport(AppKit) +#if canImport(UIKit) + import UIKit.UIColor + public typealias Color = UIColor +#elseif canImport(AppKit) + import AppKit.NSColor + public typealias Color = NSColor +#endif + +extension Color: RemoteConfigSerializable {} + +final class RemoteConfigColorSerializableSpec: RemoteConfigSerializableSpec { + var defaultValue: Color = .blue + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} +#endif diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift new file mode 100644 index 00000000000..83524376438 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation + +final class RemoteConfigCustomSerializableSpec: RemoteConfigSerializableSpec { + var defaultValue: FrogCustomSerializable = FrogCustomSerializable(name: "default") + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift new file mode 100644 index 00000000000..9440557a13c --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation + +final class RemoteConfigBestFroggiesEnumSerializableSpec: RemoteConfigSerializableSpec { + var defaultValue: BestFroggiesEnum = .Dandy + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift new file mode 100644 index 00000000000..d2aac2f983f --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation + +final class RemoteConfigFrogSerializableSpec: RemoteConfigSerializableSpec { + var defaultValue: FrogSerializable = FrogSerializable(name: "default") + var keyStore = FrogKeyStore() + + override class func setUp() { + super.setupFirebase() + } + + func testValues() { + super.testValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValues() { + super.testOptionalValues(defaultValue: defaultValue, keyStore: keyStore) + } + + func testOptionalValuesWithoutDefaultValue() { + super.testOptionalValuesWithoutDefaultValue(defaultValue: defaultValue, keyStore: keyStore) + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift b/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift new file mode 100644 index 00000000000..0614d600c2b --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift @@ -0,0 +1,155 @@ +// +// File.swift +// +// +// Created by 伊藤史 on 2021/11/06. +// + +import Foundation +import XCTest +@testable import SwiftyRemoteConfig +import FirebaseRemoteConfig +import FirebaseCore + +class RemoteConfigSerializableSpec: XCTestCase { +} + +extension RemoteConfigSerializableSpec where Serializable.T: Equatable, Serializable.T == Serializable, Serializable.ArrayBridge.T == [Serializable.T] { + + static func setupFirebase() { + if FirebaseApp.app() == nil { + FirebaseApp.configure(options: FrogFirebaseConfig.firebaseOptions) + } + } + + func testValues(defaultValue: Serializable.T, keyStore: FrogKeyStore) { + given(String(describing: Serializable.self)) { _ in + when("key-default value") { _ in + var config: RemoteConfigAdapter>! + let remoteConfig = RemoteConfig.remoteConfig() + config = RemoteConfigAdapter(remoteConfig: remoteConfig, + keyStore: keyStore) + + then("create a key") { _ in + let key = RemoteConfigKey("test", defaultValue: defaultValue) + XCTAssert(key._key == "test") + XCTAssert(key.defaultValue == defaultValue) + } + + then("create an array key") { _ in + let key = RemoteConfigKey<[Serializable]>("test", defaultValue: [defaultValue]) + XCTAssert(key._key == "test") + XCTAssert(key.defaultValue == [defaultValue]) + } + + then("get a default value") { _ in + let key = RemoteConfigKey("test", defaultValue: defaultValue) + XCTAssert(config[key] == defaultValue) + } + + #if swift(>=5.1) + then("get a default value with dynamicMemberLookup") { _ in + keyStore.testValue = RemoteConfigKey("test", defaultValue: defaultValue) + XCTAssert(config.testValue == defaultValue) + } + #endif + + then("get a default array value") { _ in + let key = RemoteConfigKey<[Serializable]>("test", defaultValue: [defaultValue]) + XCTAssert(config[key] == [defaultValue]) + } + + #if swift(>=5.1) + then("get a default array value with dynamicMemberLookup") { _ in + keyStore.testArray = RemoteConfigKey<[Serializable]>("test", defaultValue: [defaultValue]) + XCTAssert(config.testArray == [defaultValue]) + } + #endif + } + } + } + + func testOptionalValues(defaultValue: Serializable.T, keyStore: FrogKeyStore) { + given(String(describing: Serializable.self)) { _ in + when("key-default optional value") { _ in + var config: RemoteConfigAdapter>! + let remoteConfig = RemoteConfig.remoteConfig() + config = RemoteConfigAdapter(remoteConfig: remoteConfig, + keyStore: keyStore) + + then("create a key") { _ in + let key = RemoteConfigKey("test", defaultValue: defaultValue) + XCTAssert(key._key == "test") + XCTAssert(key.defaultValue == defaultValue) + } + + then("create an array key") { _ in + let key = RemoteConfigKey<[Serializable]?>("test", defaultValue: [defaultValue]) + XCTAssert(key._key == "test") + XCTAssert(key.defaultValue == [defaultValue]) + } + + then("get a default value") { _ in + let key = RemoteConfigKey("test", defaultValue: defaultValue) + XCTAssert(config[key] == defaultValue) + } + + #if swift(>=5.1) + then("get a default value with dynamicMemberLookup") { _ in + keyStore.testOptionalValue = RemoteConfigKey("test", defaultValue: defaultValue) + XCTAssert(config.testOptionalValue == defaultValue) + } + #endif + + then("get a default array value") { _ in + let key = RemoteConfigKey<[Serializable]?>("test", defaultValue: [defaultValue]) + XCTAssert(config[key] == [defaultValue]) + } + + #if swift(>=5.1) + then("get a default array value with dynamicMemberLookup") { _ in + keyStore.testOptionalArray = RemoteConfigKey<[Serializable]?>("test", defaultValue: [defaultValue]) + XCTAssert(config.testOptionalArray == [defaultValue]) + } + #endif + } + } + } + + func testOptionalValuesWithoutDefaultValue(defaultValue: Serializable.T, keyStore: FrogKeyStore) { + given(String(describing: Serializable.self)) { _ in + when("key-nil optional value") { _ in + var config: RemoteConfigAdapter>! + let remoteConfig = RemoteConfig.remoteConfig() + config = RemoteConfigAdapter(remoteConfig: remoteConfig, + keyStore: keyStore) + + then("create a key") { _ in + let key = RemoteConfigKey("test") + XCTAssert(key._key == "test") + XCTAssertNil(key.defaultValue) + } + + then("create an array key") { _ in + let key = RemoteConfigKey<[Serializable]?>("test") + XCTAssert(key._key == "test") + XCTAssertNil(key.defaultValue) + } + + then("compare optional value to non-optional value") { _ in + let key = RemoteConfigKey("test") + XCTAssertTrue(config[key] == nil) + XCTAssertTrue(config[key] != defaultValue) + } + + #if swift(>=5.1) + then("compare optional value to non-optional value with dynamicMemberLookup") { _ in + keyStore.testOptionalValue = RemoteConfigKey("test") + XCTAssertTrue(config.testOptionalValue == nil) + XCTAssertTrue(config.testOptionalValue != defaultValue) + } + #endif + } + } + } +} diff --git a/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift b/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift new file mode 100644 index 00000000000..fc4a85c2d45 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift @@ -0,0 +1,122 @@ +// +// TestHelper.swift +// +// +// Created by 伊藤史 on 2021/11/16. +// + +import Foundation +import SwiftyRemoteConfig +import Firebase +import XCTest + +func given(_ description: String, closure: @escaping (XCTActivity) -> Void) { + XCTContext.runActivity(named: description, block: closure) +} + +func when(_ description: String, closure: @escaping (XCTActivity) -> Void) { + XCTContext.runActivity(named: description, block: closure) +} + +func then(_ description: String, closure: @escaping (XCTActivity) -> Void) { + XCTContext.runActivity(named: description, block: closure) +} + +final class FrogSerializable: NSObject, RemoteConfigSerializable, NSCoding { + typealias T = FrogSerializable + + let name: String + + init(name: String = "Froggy") { + self.name = name + } + + init?(coder: NSCoder) { + guard let name = coder.decodeObject(forKey: "name") as? String else { + return nil + } + + self.name = name + } + + func encode(with coder: NSCoder) { + coder.encode(name, forKey: "name") + } + + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? FrogSerializable else { + return false + } + + return name == object.name + } +} + +struct FrogCodable: Codable, Equatable, RemoteConfigSerializable { + let name: String + + init(name: String = "Froggy") { + self.name = name + } +} + +enum BestFroggiesEnum: String, RemoteConfigSerializable { + case Andy + case Dandy +} + +struct FrogCustomSerializable: RemoteConfigSerializable, Equatable { + static var _remoteConfig: RemoteConfigFrogBridge { return RemoteConfigFrogBridge() } + static var _remoteConfigArray: RemoteConfigFrogArrayBridge { return RemoteConfigFrogArrayBridge() } + + typealias Bridge = RemoteConfigFrogBridge + + typealias ArrayBridge = RemoteConfigFrogArrayBridge + + + let name: String +} + +final class RemoteConfigFrogBridge: RemoteConfigBridge { + func get(key: String, remoteConfig: RemoteConfig) -> FrogCustomSerializable? { + guard let name = remoteConfig.configValue(forKey: key).stringValue, name.isEmpty == false else { + return nil + } + + return FrogCustomSerializable.init(name: name) + } + + func deserialize(_ object: RemoteConfigValue) -> FrogCustomSerializable? { + guard let name = object.stringValue, name.isEmpty == false else { + return nil + } + + return FrogCustomSerializable.init(name: name) + } +} + +final class RemoteConfigFrogArrayBridge: RemoteConfigBridge { + func get(key: String, remoteConfig: RemoteConfig) -> [FrogCustomSerializable]? { + return remoteConfig.configValue(forKey: key) + .jsonValue + .map({ $0 as? [String] }) + .flatMap({ $0 })? + .map(FrogCustomSerializable.init) + } + + func deserialize(_ object: RemoteConfigValue) -> Array? { + // In remote config, array is configured as JSON value + guard let names = object.jsonValue as? [String] else { + return nil + } + + return names.map(FrogCustomSerializable.init) + } +} + +final class FrogKeyStore: RemoteConfigKeyStore { + lazy var testValue: RemoteConfigKey = { fatalError("not initialized yet") }() + lazy var testArray: RemoteConfigKey<[Serializable]> = { fatalError("not initialized yet") }() + lazy var testOptionalValue: RemoteConfigKey = { fatalError("not initialized yet") }() + lazy var testOptionalArray: RemoteConfigKey<[Serializable]?> = { fatalError("not initialized yet") }() +} From ff6e1f5a01fbc2e1ddf84a16c9ac6c2ff3e51217 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Mon, 20 Dec 2021 00:57:35 +0900 Subject: [PATCH 2/4] change name from SwiftyRemoteConfig to RemoteConfigSwift --- FirebaseRemoteConfigSwift/CHANGELOG.md | 24 +++++++++---------- .../Sources/BuiltIns.swift | 2 +- .../Sources/RemoteConfig+Subscripts.swift | 6 ++--- .../Sources/RemoteConfig.swift | 2 +- .../Sources/RemoteConfigAdapter.swift | 2 +- .../Sources/RemoteConfigBridge.swift | 2 +- .../Sources/RemoteConfigKey.swift | 2 +- .../Sources/RemoteConfigKeys.swift | 2 +- .../Sources/RemoteConfigSerializable.swift | 2 +- .../Sources/Utils/OptionalType.swift | 2 +- .../Tests/BuiltIns/RemoteConfig+Bool.swift | 1 + .../Tests/BuiltIns/RemoteConfig+Data.swift | 3 ++- .../Tests/BuiltIns/RemoteConfig+Double.swift | 3 ++- .../Tests/BuiltIns/RemoteConfig+Int.swift | 3 ++- .../Tests/BuiltIns/RemoteConfig+String.swift | 3 ++- .../Tests/BuiltIns/RemoteConfig+URL.swift | 3 ++- .../External types/RemoteConfig+Codable.swift | 3 ++- .../External types/RemoteConfig+Color.swift | 4 ++-- .../RemoteConfig+CustomSerializable.swift | 3 ++- .../External types/RemoteConfig+Enum.swift | 3 ++- .../RemoteConfig+Serializable.swift | 3 ++- .../RemoteConfigSerializableSpec.swift | 4 ++-- .../Tests/Helpers/TestHelper.swift | 2 +- 23 files changed, 47 insertions(+), 37 deletions(-) diff --git a/FirebaseRemoteConfigSwift/CHANGELOG.md b/FirebaseRemoteConfigSwift/CHANGELOG.md index 54563b5b9e1..3f4b238217d 100644 --- a/FirebaseRemoteConfigSwift/CHANGELOG.md +++ b/FirebaseRemoteConfigSwift/CHANGELOG.md @@ -1,12 +1,12 @@ -# SwiftyRemoteConfig +# RemoteConfigSwift **Modern Swift API for `FirebaseRemoteConfig`** -SwiftyRemoteConfig makes Firebase Remote Config enjoyable to use by combining expressive Swifty API with the benefits fo static typing. This library is strongly inspired by [SwiftyUserDefaults](https://github.com/sunshinejr/SwiftyUserDefaults). +RemoteConfigSwift makes Firebase Remote Config enjoyable to use by combining expressive Swifty API with the benefits fo static typing. This library is strongly inspired by [SwiftyUserDefaults](https://github.com/sunshinejr/SwiftyUserDefaults). ## Features -There is only one step to start using SwiftyRemoteConfig. +There is only one step to start using RemoteConfigSwift. Define your Keys ! @@ -37,7 +37,7 @@ let subColor: UIColor = RemoteConfigs.subColor ### Define your keys -To get the most out of SwiftyRemoteConfig, define your remote config keys ahead of time: +To get the most out of RemoteConfigSwift, define your remote config keys ahead of time: ```swift let flag = RemoteConfigKey("flag", defaultValue: false) @@ -72,7 +72,7 @@ RemoteConfigs[\.flag] // => false ### Supported types -SwiftyRemoteConfig supports standard types as following: +RemoteConfigSwift supports standard types as following: | Single value | Array | |:---:|:---:| @@ -91,7 +91,7 @@ and that's not all ! ### Codable -`SwiftyRemoteConfig` supports `Codable` ! Just conform to `RemoteConfigSerializable` in your type: +`RemoteConfigSwift` supports `Codable` ! Just conform to `RemoteConfigSerializable` in your type: ```swift final class UserSection: Codable, RemoteConfigSerializable { @@ -234,7 +234,7 @@ Also, take a look at our source code or tests to see more examples of bridges. I ## KeyPath dynamicMemberLookup -SwiftyRemoteConfig makes KeyPath dynamicMemberLookpu usable in Swift 5.1. +RemoteConfigSwift makes KeyPath dynamicMemberLookpu usable in Swift 5.1. ```swift extension RemoteConfigKeys { @@ -273,7 +273,7 @@ let themaColor: UIColor = RemoteConfig.themaColor If you're using Cocoapods, just add this line to your `Podfile`: ```ruby -pod 'SwiftyRemoteConfig`, `~> 0.0.2` +pod 'RemoteConfigSwift`, `~> 0.0.2` ``` Install by running this command in your terminal: @@ -285,7 +285,7 @@ $ pod install Then import the library in all files where you use it: ```swift -import SwiftyRemoteConfig +import RemoteConfigSwift ``` ### Carthage @@ -293,7 +293,7 @@ import SwiftyRemoteConfig Just add your Cartfile ``` -github "fumito-ito/SwiftyRemoteConfig" ~> 0.0.2 +github "fumito-ito/RemoteConfigSwift" ~> 0.0.2 ``` ### Swift Package Manager @@ -305,9 +305,9 @@ let package = Package( name: "MyPackage", products: [...], dependencies: [ - .package(url: "https://github.com/fumito-ito/SwiftyRemoteConfig.git", .upToNextMajor(from: "0.0.2")) + .package(url: "https://github.com/fumito-ito/RemoteConfigSwift.git", .upToNextMajor(from: "0.0.2")) ] ) ``` -SwiftyRemoteConfig is available under the Apache License 2.0. See the LICENSE file for more detail. +RemoteConfigSwift is available under the Apache License 2.0. See the LICENSE file for more detail. diff --git a/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift b/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift index 5a3823a8210..34c17d5b5c8 100644 --- a/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift +++ b/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift @@ -1,6 +1,6 @@ // // BuiltIns.swift -// SwiftyRemoteConfigExample +// RemoteConfigSwiftExample // // Created by 伊藤史 on 2020/08/25. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift index 1f4016170cb..e042a0592c8 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift @@ -1,6 +1,6 @@ // -// Defaults+Subscripts.swift -// SwiftyRemoteConfig +// RemoteConfig+Subscripts.swift +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/21. // Copyright © 2020 Fumito Ito. All rights reserved. @@ -69,7 +69,7 @@ public extension RemoteConfig { } else if let defaultValue = key.defaultValue { return defaultValue } else { - fatalError("Unexpected path is executed. please report to https://github.com/fumito-ito/SwiftyRemoteConfig") + fatalError("Unexpected path is executed. please report to https://github.com/fumito-ito/RemoteConfigSwift") } } } diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift index 61f57034a7c..b74bb853f50 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift @@ -1,6 +1,6 @@ // // RemoteConfig.swift -// SwiftyRemoteConfig +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/13. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift index ced810bbb94..e0bb4a25815 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift @@ -1,6 +1,6 @@ // // RemoteConfigAdapter.swift -// SwiftyRemoteConfig +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/15. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift index be0d9aeb157..367d0ff8903 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift @@ -1,6 +1,6 @@ // // RemoteConfigBridge.swift -// SwiftyRemoteConfig +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/13. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift index aa0f5726624..96b0cb0cddc 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift @@ -1,6 +1,6 @@ // // RemoteConfigKey.swift -// SwiftyRemoteConfig +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/15. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift index bf670208f22..e0131e75c81 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift @@ -1,6 +1,6 @@ // // RemoteConfigKeys.swift -// SwiftyRemoteConfig +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/15. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift index e72133ffdf2..7c7abcce4ae 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift @@ -1,6 +1,6 @@ // // RemoteConfigSerializable.swift -// SwiftyRemoteConfig +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/15. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift b/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift index 76596d44597..dc8b1688470 100644 --- a/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift +++ b/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift @@ -1,6 +1,6 @@ // // OptionalType.swift -// SwiftyRemoteConfig +// RemoteConfigSwift // // Created by 伊藤史 on 2020/08/21. // Copyright © 2020 Fumito Ito. All rights reserved. diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift index a89adc8a593..2bf6414a943 100644 --- a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift @@ -6,6 +6,7 @@ // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigBoolSpec: RemoteConfigSerializableSpec { var defaultValue: Bool = true diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift index 55fe05b83bf..3f5411af58d 100644 --- a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+Data.swift // // // Created by 伊藤史 on 2021/11/21. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigDataSpec: RemoteConfigSerializableSpec { var defaultValue: Data = Data() diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift index b4a4a504b1d..cf543c2092b 100644 --- a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+Double.swift // // // Created by 伊藤史 on 2021/11/21. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigDoubleSpec: RemoteConfigSerializableSpec { var defaultValue: Double = 1.0 diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift index 04aeb03d612..36f9889c8e1 100644 --- a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+Int.swift // // // Created by 伊藤史 on 2021/11/21. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigIntSpec: RemoteConfigSerializableSpec { var defaultValue: Int = 1 diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift index 2e5f8ffccb2..99a670c259b 100644 --- a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+String.swift // // // Created by 伊藤史 on 2021/11/21. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigStringSpec: RemoteConfigSerializableSpec { var defaultValue: String = "Firebase" diff --git a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift index 3d4007e5b4f..016a4b332da 100644 --- a/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+URL.swift // // // Created by 伊藤史 on 2021/11/21. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigURLSpec: RemoteConfigSerializableSpec { var defaultValue: URL = URL(string: "https://console.firebase.google.com/")! diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift index c200c353666..544dcf0b0e1 100644 --- a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+Codable.swift // // // Created by 伊藤史 on 2021/11/23. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigCodableSpec: RemoteConfigSerializableSpec { var defaultValue: FrogCodable = FrogCodable(name: "default") diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift index 4a602481ff8..de88d41bdf0 100644 --- a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift @@ -1,12 +1,12 @@ // -// File.swift +// RemoteConfig+Color.swift // // // Created by 伊藤史 on 2021/11/21. // import Foundation -@testable import SwiftyRemoteConfig +@testable import FirebaseRemoteConfigSwift #if canImport(UIKit) || canImport(AppKit) #if canImport(UIKit) diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift index 83524376438..67c40bab0de 100644 --- a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+CustomSerializable.swift // // // Created by 伊藤史 on 2021/11/23. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigCustomSerializableSpec: RemoteConfigSerializableSpec { var defaultValue: FrogCustomSerializable = FrogCustomSerializable(name: "default") diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift index 9440557a13c..f4f6295167f 100644 --- a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+Enum.swift // // // Created by 伊藤史 on 2021/11/23. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigBestFroggiesEnumSerializableSpec: RemoteConfigSerializableSpec { var defaultValue: BestFroggiesEnum = .Dandy diff --git a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift index d2aac2f983f..54a729f5136 100644 --- a/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift @@ -1,11 +1,12 @@ // -// File.swift +// RemoteConfig+Serializable.swift // // // Created by 伊藤史 on 2021/11/23. // import Foundation +import FirebaseRemoteConfigSwift final class RemoteConfigFrogSerializableSpec: RemoteConfigSerializableSpec { var defaultValue: FrogSerializable = FrogSerializable(name: "default") diff --git a/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift b/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift index 0614d600c2b..00ebd855030 100644 --- a/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift +++ b/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift @@ -1,5 +1,5 @@ // -// File.swift +// RemoteConfigSerializableSpec.swift // // // Created by 伊藤史 on 2021/11/06. @@ -7,7 +7,7 @@ import Foundation import XCTest -@testable import SwiftyRemoteConfig +@testable import FirebaseRemoteConfigSwift import FirebaseRemoteConfig import FirebaseCore diff --git a/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift b/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift index fc4a85c2d45..855607052d7 100644 --- a/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift +++ b/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift @@ -6,7 +6,7 @@ // import Foundation -import SwiftyRemoteConfig +import RemoteConfigSwift import Firebase import XCTest From f0ce00d8f621c53c67401ae2c22bcc04a5601228 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Wed, 5 Jan 2022 00:26:55 +0900 Subject: [PATCH 3/4] add package settings for swiftpm and cocoapods --- FirebaseRemoteConfigSwift.podspec | 43 +++++++++++++++++++++++++++++++ Package.swift | 21 +++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 FirebaseRemoteConfigSwift.podspec diff --git a/FirebaseRemoteConfigSwift.podspec b/FirebaseRemoteConfigSwift.podspec new file mode 100644 index 00000000000..ab29140e060 --- /dev/null +++ b/FirebaseRemoteConfigSwift.podspec @@ -0,0 +1,43 @@ +# +# Be sure to run `pod lib lint FirebaseRemoteConfigSwift.podspec' to ensure this is a +# valid spec before submitting. +# + +Pod::Spec.new do |s| + s.name = 'FirebaseRemoteConfigSwift' + s.version = '8.10.0-beta' + s.summary = 'Swift Extensions for Google Cloud RemoteConfig' + + 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.social_media_url = 'https://twitter.com/Firebase' + + s.swift_version = '5.3' + + s.ios.deployment_target = '10.0' + s.osx.deployment_target = '10.12' + s.tvos.deployment_target = '10.0' + s.watchos.deployment_target = '6.0' + + s.cocoapods_version = '>= 1.4.0' + s.prefix_header_file = false + + s.requires_arc = true + s.source_files = [ + 'FirebaseRemoteConfigSwift/Sources/**/*.swift', + ] + + s.dependency 'FirebaseRemoteConfig', '~> 8.0' +end diff --git a/Package.swift b/Package.swift index e08c35942a0..34bd8724251 100644 --- a/Package.swift +++ b/Package.swift @@ -123,6 +123,10 @@ let package = Package( name: "FirebaseRemoteConfig", targets: ["FirebaseRemoteConfig"] ), + .library( + name: "FirebaseRemoteConfigSwift-Beta", + targets: ["FirebaseRemoteConfigSwift"] + ), .library( name: "FirebaseStorage", targets: ["FirebaseStorage"] @@ -973,6 +977,22 @@ let package = Package( .headerSearchPath("../../.."), ] ), + + .target( + name: "FirebaseRemoteConfigSwift", + dependencies: [ + "FirebaseRemoteConfig", + ], + path: "FirebaseRemoteConfigSwift/Sources" + ), + .testTarget( + name: "FirebaseRemoteConfigSwiftUnit", + dependencies: [ + "FirebaseRemoteConfigSwift", + ], + path: "FirebaseRemoteConfigSwift/Tests" + ), + .target( name: "FirebaseStorage", dependencies: [ @@ -1030,6 +1050,7 @@ let package = Package( .target(name: "FirebasePerformance", condition: .when(platforms: [.iOS, .tvOS])), "FirebaseRemoteConfig", + "FirebaseRemoteConfigSwift", "FirebaseStorage", "FirebaseStorageSwift", .product(name: "nanopb", package: "nanopb"), From cbf7095e3bc3cfebf34ce4d50b70fdfc781207f9 Mon Sep 17 00:00:00 2001 From: Fumito Ito Date: Wed, 5 Jan 2022 10:21:01 +0900 Subject: [PATCH 4/4] fix cocoapods warning with avoiding to use NSKeyedUnarchiver for newer version OSs --- .../Sources/BuiltIns.swift | 2 +- .../Sources/RemoteConfigBridge.swift | 36 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift b/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift index 34c17d5b5c8..0a0c969f4c4 100644 --- a/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift +++ b/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift @@ -53,7 +53,7 @@ extension RemoteConfigSerializable where Self: RawRepresentable { extension RemoteConfigSerializable where Self: NSCoding { public static var _remoteConfig: RemoteConfigKeyedArchiverBridge { RemoteConfigKeyedArchiverBridge() } - public static var _remoteConfigArray: RemoteConfigKeyedArchiverBridge<[Self]> { RemoteConfigKeyedArchiverBridge() } + public static var _remoteConfigArray: RemoteConfigKeyedArchiverArrayBridge<[Self]> { RemoteConfigKeyedArchiverArrayBridge() } } extension Dictionary: RemoteConfigSerializable where Key == String { diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift index 367d0ff8903..dfd28e02bc8 100644 --- a/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift @@ -133,10 +133,6 @@ public struct RemoteConfigUrlBridge: RemoteConfigBridge { } public func deserialize(_ object: RemoteConfigValue) -> URL? { - if let url = NSKeyedUnarchiver.unarchiveObject(with: object.dataValue) as? URL { - return url - } - if let stringValue = object.stringValue, stringValue.isEmpty == false { if let url = URL(string: stringValue) { return url @@ -160,13 +156,41 @@ public struct RemoteConfigCodableBridge: RemoteConfigBridge { } } -public struct RemoteConfigKeyedArchiverBridge: RemoteConfigBridge { +public struct RemoteConfigKeyedArchiverBridge: RemoteConfigBridge { + public func get(key: String, remoteConfig: RemoteConfig) -> T? { + return self.deserialize(remoteConfig.configValue(forKey: key)) + } + + public func deserialize(_ object: RemoteConfigValue) -> T? { + guard #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) else { + return NSKeyedUnarchiver.unarchiveObject(with: object.dataValue) as? T + } + + guard let object = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [T.self], from: object.dataValue) as? T else { + return nil + } + + return object + } +} + +public struct RemoteConfigKeyedArchiverArrayBridge: RemoteConfigBridge where T.Element: NSCoding { public func get(key: String, remoteConfig: RemoteConfig) -> T? { return self.deserialize(remoteConfig.configValue(forKey: key)) } public func deserialize(_ object: RemoteConfigValue) -> T? { - return NSKeyedUnarchiver.unarchiveObject(with: object.dataValue) as? T + guard #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) else { + return NSKeyedUnarchiver.unarchiveObject(with: object.dataValue) as? T + } + + guard let objects = object.jsonValue as? [Data] else { + return nil + } + + return objects.compactMap({ + try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [T.Element.self], from: $0) as? T.Element + }) as? T } }