Skip to content

Commit

Permalink
[Rollouts] RC interop implementation (#12173)
Browse files Browse the repository at this point in the history
  • Loading branch information
themiswang authored Dec 7, 2023
1 parent 739571a commit 5e1f24c
Show file tree
Hide file tree
Showing 17 changed files with 321 additions and 10 deletions.
1 change: 1 addition & 0 deletions ClientApp/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ target 'ClientApp-CocoaPods' do
pod 'FirebaseAppCheck', :path => '../'
pod 'FirebaseRemoteConfig', :path => '../'
pod 'FirebaseRemoteConfigSwift', :path => '../'
pod 'FirebaseRemoteConfigInterop', :path => '../'
pod 'FirebaseAppDistribution', :path => '../'
pod 'FirebaseAuth', :path => '../'
pod 'FirebaseCrashlytics', :path => '../'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ target 'CocoapodsIntegrationTest' do
pod 'FirebaseInstallations', :path => '../'
pod 'FirebaseMessaging', :path => '../'
pod 'FirebaseMessagingInterop', :path => '../'
pod 'FirebaseRemoteConfigInterop', :path => '../'
pod 'FirebasePerformance', :path => '../'
pod 'FirebaseStorage', :path => '../'
end
Expand Down
1 change: 1 addition & 0 deletions CoreOnly/Tests/FirebasePodTest/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ target 'FirebasePodTest' do
pod 'FirebaseAppCheckInterop', :path => '../../../'
pod 'FirebaseAuthInterop', :path => '../../../'
pod 'FirebaseMessagingInterop', :path => '../../../'
pod 'FirebaseRemoteConfigInterop', :path => '../../../'
pod 'FirebaseCoreInternal', :path => '../../../'
pod 'FirebaseCoreExtension', :path => '../../../'
pod 'FirebaseSessions', :path => '../../../'
Expand Down
1 change: 1 addition & 0 deletions Example/watchOSSample/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ target 'SampleWatchAppWatchKitExtension' do
pod 'FirebaseDatabase', :path => '../../'
pod 'FirebaseAppCheckInterop', :path => '../../'
pod 'FirebaseAuthInterop', :path => '../../'
pod 'FirebaseRemoteConfigInterop', :path => '../../'

pod 'Firebase/Messaging', :path => '../../'
pod 'Firebase/Storage', :path => '../../'
Expand Down
4 changes: 3 additions & 1 deletion FirebaseRemoteConfig.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ app update.
s.dependency 'FirebaseInstallations', '~> 10.0'
s.dependency 'GoogleUtilities/Environment', '~> 7.8'
s.dependency 'GoogleUtilities/NSData+zlib', '~> 7.8'
s.dependency 'FirebaseRemoteConfigInterop', '~> 10.20'

s.test_spec 'unit' do |unit_tests|
unit_tests.scheme = { :code_coverage => true }
Expand All @@ -77,7 +78,8 @@ app update.
'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.m',
'FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m',
'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h',
'FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m'
'FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m',
'FirebaseRemoteConfig/Tests/SwiftUnit/*.swift'
# Supply plist custom plist testing.
unit_tests.resources =
'FirebaseRemoteConfig/Tests/Unit/Defaults-testInfo.plist',
Expand Down
21 changes: 21 additions & 0 deletions FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

@objc(FIRRemoteConfigInterop)
public protocol RemoteConfigInterop {
func registerRolloutsStateSubscriber(_ subscriber: RolloutsStateSubscriber,
for namespace: String)
}
46 changes: 46 additions & 0 deletions FirebaseRemoteConfig/Interop/RolloutAssignment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

@objc(FIRRolloutAssignment)
public class RolloutAssignment: NSObject {
@objc public var rolloutId: String
@objc public var variantId: String
@objc public var templateVersion: Int64
@objc public var parameterKey: String
@objc public var parameterValue: String

public init(rolloutId: String, variantId: String, templateVersion: Int64, parameterKey: String,
parameterValue: String) {
self.rolloutId = rolloutId
self.variantId = variantId
self.templateVersion = templateVersion
self.parameterKey = parameterKey
self.parameterValue = parameterValue
super.init()
}
}

@objc(FIRRolloutsState)
public class RolloutsState: NSObject {
@objc public var assignments: Set<RolloutAssignment> = Set()

public init(assignmentList: [RolloutAssignment]) {
for assignment in assignmentList {
assignments.insert(assignment)
}
super.init()
}
}
20 changes: 20 additions & 0 deletions FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

@objc(FIRRolloutsStateSubscriber)
public protocol RolloutsStateSubscriber {
func rolloutsStateDidChange(_ rolloutsState: RolloutsState)
}
8 changes: 7 additions & 1 deletion FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import <Foundation/Foundation.h>

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
@import FirebaseRemoteConfigInterop;

@class FIRApp;
@class FIRRemoteConfig;
Expand All @@ -37,14 +38,19 @@ NS_ASSUME_NONNULL_BEGIN

/// A concrete implementation for FIRRemoteConfigInterop to create Remote Config instances and
/// register with Core's component system.
@interface FIRRemoteConfigComponent : NSObject <FIRRemoteConfigProvider, FIRLibrary>
@interface FIRRemoteConfigComponent
: NSObject <FIRRemoteConfigProvider, FIRLibrary, FIRRemoteConfigInterop>

/// The FIRApp that instances will be set up with.
@property(nonatomic, weak, readonly) FIRApp *app;

/// Cached instances of Remote Config objects.
@property(nonatomic, strong) NSMutableDictionary<NSString *, FIRRemoteConfig *> *instances;

/// Clear all the component instances from the singleton which created previously, this is for
/// testing only
+ (void)clearAllComponentInstances;

/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist.
- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace;

Expand Down
49 changes: 47 additions & 2 deletions FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@

@implementation FIRRemoteConfigComponent

// Because Component now need to register two protocols (provider and interop), we need a way to
// return the same component instance for both registered protocol, this singleton pattern allow us
// to return the same component object for both registration callback.
static NSMutableDictionary<NSString *, FIRRemoteConfigComponent *> *_componentInstances = nil;

+ (FIRRemoteConfigComponent *)getComponentForApp:(FIRApp *)app {
@synchronized(_componentInstances) {
// need to init the dictionary first
if (!_componentInstances) {
_componentInstances = [[NSMutableDictionary alloc] init];
}
if (![_componentInstances objectForKey:app.name]) {
_componentInstances[app.name] = [[self alloc] initWithApp:app];
}
return _componentInstances[app.name];
}
return nil;
}

+ (void)clearAllComponentInstances {
@synchronized(_componentInstances) {
[_componentInstances removeAllObjects];
}
}

/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist.
- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace {
if (!remoteConfigNamespace) {
Expand Down Expand Up @@ -102,9 +127,29 @@ + (void)load {
creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
// Cache the component so instances of Remote Config are cached.
*isCacheable = YES;
return [[FIRRemoteConfigComponent alloc] initWithApp:container.app];
return [FIRRemoteConfigComponent getComponentForApp:container.app];
}];

// Unlike provider needs to setup a hard dependency on remote config, interop allows an optional
// dependency on RC
FIRComponent *rcInterop = [FIRComponent
componentWithProtocol:@protocol(FIRRemoteConfigInterop)
instantiationTiming:FIRInstantiationTimingAlwaysEager
dependencies:@[]
creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
// Cache the component so instances of Remote Config are cached.
*isCacheable = YES;
return [FIRRemoteConfigComponent getComponentForApp:container.app];
}];
return @[ rcProvider ];
return @[ rcProvider, rcInterop ];
}

#pragma mark - Remote Config Interop Protocol

- (void)registerRolloutsStateSubscriber:(id<FIRRolloutsStateSubscriber>)subscriber
for:(NSString * _Nonnull)namespace {
// TODO(Themisw): Adding the registered subscriber reference to the namespace instance
// [self.instances[namespace] addRemoteConfigInteropSubscriber:subscriber];
}

@end
1 change: 1 addition & 0 deletions FirebaseRemoteConfig/Tests/Sample/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ target 'RemoteConfigSampleApp' do
pod 'FirebaseInstallations', :path => '../../../'
pod 'FirebaseRemoteConfig', :path => '../../../'
pod 'FirebaseABTesting', :path => '../../..'
pod 'FirebaseRemoteConfigInterop', :path => '../../..'

# Pods for RemoteConfigSampleApp

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import FirebaseRemoteConfigInterop
import XCTest

class MockRCInterop: RemoteConfigInterop {
weak var subscriber: FirebaseRemoteConfigInterop.RolloutsStateSubscriber?
func registerRolloutsStateSubscriber(_ subscriber: FirebaseRemoteConfigInterop
.RolloutsStateSubscriber,
for namespace: String) {
self.subscriber = subscriber
}
}

class MockRolloutSubscriber: RolloutsStateSubscriber {
var isSubscriberCalled = false
var rolloutsState: RolloutsState?
func rolloutsStateDidChange(_ rolloutsState: FirebaseRemoteConfigInterop.RolloutsState) {
isSubscriberCalled = true
self.rolloutsState = rolloutsState
}
}

final class RemoteConfigInteropTests: XCTestCase {
let rollouts: RolloutsState = {
let assignment1 = RolloutAssignment(
rolloutId: "rollout_1",
variantId: "control",
templateVersion: 1,
parameterKey: "my_feature",
parameterValue: "false"
)
let assignment2 = RolloutAssignment(
rolloutId: "rollout_2",
variantId: "enabled",
templateVersion: 123,
parameterKey: "themis_big_feature",
parameterValue: "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
)
let rollouts = RolloutsState(assignmentList: [assignment1, assignment2])
return rollouts
}()

func testRemoteConfigIntegration() throws {
let rcSubscriber = MockRolloutSubscriber()
let rcInterop = MockRCInterop()
rcInterop.registerRolloutsStateSubscriber(rcSubscriber, for: "namespace")
rcInterop.subscriber?.rolloutsStateDidChange(rollouts)

XCTAssertTrue(rcSubscriber.isSubscriberCalled)
XCTAssertEqual(rcSubscriber.rolloutsState?.assignments.count, 2)
}
}
43 changes: 42 additions & 1 deletion FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
@import FirebaseRemoteConfigInterop;

@interface FIRRemoteConfigComponentTest : XCTestCase
@end
Expand All @@ -31,6 +32,7 @@ - (void)tearDown {

// Clear out any apps that were called with `configure`.
[FIRApp resetApps];
[FIRRemoteConfigComponent clearAllComponentInstances];
}

- (void)testRCInstanceCreationAndCaching {
Expand Down Expand Up @@ -92,7 +94,8 @@ - (void)testInitialization {
}

- (void)testRegistersAsLibrary {
XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 1);
// Now component has two register, one is provider and another one is Interop
XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 2);

// Configure a test FIRApp for fetching an instance of the FIRRemoteConfigProvider.
NSString *appName = [self generatedTestAppName];
Expand All @@ -101,12 +104,50 @@ - (void)testRegistersAsLibrary {

// Attempt to fetch the component and verify it's a valid instance.
id<FIRRemoteConfigProvider> provider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container);
id<FIRRemoteConfigInterop> interop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container);
XCTAssertNotNil(provider);
XCTAssertNotNil(interop);

// Ensure that the instance that comes from the container is cached.
id<FIRRemoteConfigProvider> sameProvider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container);
id<FIRRemoteConfigInterop> sameInterop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container);
XCTAssertNotNil(sameProvider);
XCTAssertNotNil(sameInterop);
XCTAssertEqual(provider, sameProvider);
XCTAssertEqual(interop, sameInterop);

// Dynamic typing, both prototols are refering to the same component instance
id providerID = provider;
id interopID = interop;
XCTAssertEqualObjects(providerID, interopID);
}

- (void)testTwoAppsCreateTwoComponents {
NSString *appName = [self generatedTestAppName];
[FIRApp configureWithName:appName options:[self fakeOptions]];
FIRApp *app = [FIRApp appNamed:appName];

[FIRApp configureWithOptions:[self fakeOptions]];
FIRApp *defaultApp = [FIRApp defaultApp];
XCTAssertNotNil(defaultApp);
XCTAssertNotEqualObjects(app, defaultApp);

id<FIRRemoteConfigProvider> provider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container);
id<FIRRemoteConfigInterop> interop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container);
id<FIRRemoteConfigProvider> defaultAppProvider =
FIR_COMPONENT(FIRRemoteConfigProvider, defaultApp.container);
id<FIRRemoteConfigInterop> defaultAppInterop =
FIR_COMPONENT(FIRRemoteConfigInterop, defaultApp.container);

id providerID = provider;
id interopID = interop;
id defaultAppProviderID = defaultAppProvider;
id defaultAppInteropID = defaultAppInterop;

XCTAssertEqualObjects(providerID, interopID);
XCTAssertEqualObjects(defaultAppProviderID, defaultAppInteropID);
// Check two apps get their own component to register
XCTAssertNotEqualObjects(interopID, defaultAppInteropID);
}

- (void)testThrowsWithEmptyGoogleAppID {
Expand Down
2 changes: 2 additions & 0 deletions FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
Expand Down Expand Up @@ -286,6 +287,7 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,

- (void)tearDown {
[_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath];
[FIRRemoteConfigComponent clearAllComponentInstances];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:_userDefaultsSuiteName];
[_DBManagerMock stopMocking];
_DBManagerMock = nil;
Expand Down
Loading

0 comments on commit 5e1f24c

Please sign in to comment.