Skip to content

Commit

Permalink
[Rollouts] RC interop implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
themiswang committed Dec 5, 2023
1 parent 739571a commit 3fc43c6
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 10 deletions.
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.19'

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)
}
4 changes: 3 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,7 +38,8 @@ 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;
Expand Down
40 changes: 38 additions & 2 deletions FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@

@implementation FIRRemoteConfigComponent

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;
}

/// 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 +118,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];
}];
return @[ rcProvider ];

// Crashlytics only works on default app, the only shared component between Provider and Interop
// is the component for default app
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, rcInterop ];
}

#pragma mark - Remote Config Interop Protocol

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

@end
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)
}
}
42 changes: 41 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 Down Expand Up @@ -92,7 +93,8 @@ - (void)testInitialization {
}

- (void)testRegistersAsLibrary {
XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 1);
// Now compoment 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 +103,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
29 changes: 29 additions & 0 deletions FirebaseRemoteConfigInterop.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Pod::Spec.new do |s|
s.name = 'FirebaseRemoteConfigInterop'
s.version = '10.19.0'
s.summary = 'Interfaces that allow other Firebase SDKs to use Remote Config functionality.'

s.description = <<-DESC
Not for public use.
A set of protocols that other Firebase SDKs can use to interoperate with FirebaseRemoetConfig in a safe
and reliable manner.
DESC

s.homepage = 'https://firebase.google.com'
s.license = { :type => 'Apache-2.0', :file => 'LICENSE' }
s.authors = 'Google, Inc.'

# NOTE that these should not be used externally, this is for Firebase pods to depend on each
# other.
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.ios.deployment_target = '11.0'
s.osx.deployment_target = '10.13'
s.tvos.deployment_target = '12.0'
s.watchos.deployment_target = '6.0'

s.source_files = 'FirebaseRemoteConfig/Interop/*.swift'
end
Loading

0 comments on commit 3fc43c6

Please sign in to comment.