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/FirebaseRemoteConfigSwift/CHANGELOG.md b/FirebaseRemoteConfigSwift/CHANGELOG.md new file mode 100644 index 00000000000..3f4b238217d --- /dev/null +++ b/FirebaseRemoteConfigSwift/CHANGELOG.md @@ -0,0 +1,313 @@ +# RemoteConfigSwift + +**Modern Swift API for `FirebaseRemoteConfig`** + +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 RemoteConfigSwift. + +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 RemoteConfigSwift, 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 + +RemoteConfigSwift 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 + +`RemoteConfigSwift` 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 + +RemoteConfigSwift 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 'RemoteConfigSwift`, `~> 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 RemoteConfigSwift +``` + +### Carthage + +Just add your Cartfile + +``` +github "fumito-ito/RemoteConfigSwift" ~> 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/RemoteConfigSwift.git", .upToNextMajor(from: "0.0.2")) + ] +) +``` + +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 new file mode 100644 index 00000000000..0a0c969f4c4 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/BuiltIns.swift @@ -0,0 +1,85 @@ +// +// BuiltIns.swift +// RemoteConfigSwiftExample +// +// 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: RemoteConfigKeyedArchiverArrayBridge<[Self]> { RemoteConfigKeyedArchiverArrayBridge() } +} + +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..e042a0592c8 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfig+Subscripts.swift @@ -0,0 +1,76 @@ +// +// RemoteConfig+Subscripts.swift +// RemoteConfigSwift +// +// 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/RemoteConfigSwift") + } + } + } +} diff --git a/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift b/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift new file mode 100644 index 00000000000..b74bb853f50 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfig.swift @@ -0,0 +1,18 @@ +// +// RemoteConfig.swift +// RemoteConfigSwift +// +// 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..e0bb4a25815 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigAdapter.swift @@ -0,0 +1,35 @@ +// +// RemoteConfigAdapter.swift +// RemoteConfigSwift +// +// 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..dfd28e02bc8 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigBridge.swift @@ -0,0 +1,263 @@ +// +// RemoteConfigBridge.swift +// RemoteConfigSwift +// +// 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 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? { + 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? { + 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 + } +} + +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..96b0cb0cddc --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKey.swift @@ -0,0 +1,46 @@ +// +// RemoteConfigKey.swift +// RemoteConfigSwift +// +// 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..e0131e75c81 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigKeys.swift @@ -0,0 +1,15 @@ +// +// RemoteConfigKeys.swift +// RemoteConfigSwift +// +// 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..7c7abcce4ae --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/RemoteConfigSerializable.swift @@ -0,0 +1,18 @@ +// +// RemoteConfigSerializable.swift +// RemoteConfigSwift +// +// 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..dc8b1688470 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Sources/Utils/OptionalType.swift @@ -0,0 +1,33 @@ +// +// OptionalType.swift +// RemoteConfigSwift +// +// 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..2bf6414a943 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Bool.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+Bool.swift +// +// +// Created by 伊藤史 on 2021/11/18. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..3f5411af58d --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Data.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+Data.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..cf543c2092b --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Double.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+Double.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..36f9889c8e1 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+Int.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+Int.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..99a670c259b --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+String.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+String.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..016a4b332da --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/BuiltIns/RemoteConfig+URL.swift @@ -0,0 +1,30 @@ +// +// 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/")! + 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..544dcf0b0e1 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Codable.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+Codable.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..de88d41bdf0 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Color.swift @@ -0,0 +1,42 @@ +// +// RemoteConfig+Color.swift +// +// +// Created by 伊藤史 on 2021/11/21. +// + +import Foundation +@testable import FirebaseRemoteConfigSwift + +#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..67c40bab0de --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+CustomSerializable.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+CustomSerializable.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..f4f6295167f --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Enum.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+Enum.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..54a729f5136 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/External types/RemoteConfig+Serializable.swift @@ -0,0 +1,30 @@ +// +// RemoteConfig+Serializable.swift +// +// +// Created by 伊藤史 on 2021/11/23. +// + +import Foundation +import FirebaseRemoteConfigSwift + +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..00ebd855030 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/Helpers/RemoteConfigSerializableSpec.swift @@ -0,0 +1,155 @@ +// +// RemoteConfigSerializableSpec.swift +// +// +// Created by 伊藤史 on 2021/11/06. +// + +import Foundation +import XCTest +@testable import FirebaseRemoteConfigSwift +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..855607052d7 --- /dev/null +++ b/FirebaseRemoteConfigSwift/Tests/Helpers/TestHelper.swift @@ -0,0 +1,122 @@ +// +// TestHelper.swift +// +// +// Created by 伊藤史 on 2021/11/16. +// + +import Foundation +import RemoteConfigSwift +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") }() +} 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"),