Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rollouts] RC interop implementation #12173

Merged
merged 5 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the other interops are protocols only. Why is there code here?

Copy link
Contributor Author

@themiswang themiswang Dec 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This object will be shared between Crashlytics and RC to transfer the Rollout related data. We are following the similar implementation on Android side. cc @danasilver if you want to add more input on this!


@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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinking: this asserts the singleton logic works 👍

// 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
Loading