diff --git a/Package.swift b/Package.swift index f991cf7..2db2a8f 100644 --- a/Package.swift +++ b/Package.swift @@ -34,6 +34,10 @@ let package = Package( url: "https://github.com/grpc/grpc-swift.git", from: "1.19.1" // TODO: Constrain to a range at time of release ), + .package( + url: "https://github.com/google/GoogleUtilities.git", + "8.0.0" ..< "9.0.0" + ), ], targets: [ .target( @@ -44,7 +48,7 @@ let package = Package( // TODO: Investigate switching Auth and AppCheck to interop. .product(name: "FirebaseAuth", package: "firebase-ios-sdk"), .product(name: "FirebaseAppCheck", package: "firebase-ios-sdk"), - + .product(name: "GULEnvironment", package: "GoogleUtilities"), ], path: "Sources" ), diff --git a/Sources/DataConnect.swift b/Sources/DataConnect.swift index 4688380..251afb9 100644 --- a/Sources/DataConnect.swift +++ b/Sources/DataConnect.swift @@ -27,6 +27,8 @@ public class DataConnect { private(set) var grpcClient: GrpcClient private var operationsManager: OperationsManager + private var callerSDKType: CallerSDKType = .base + private static var instanceStore = InstanceStore() public enum EmulatorDefaults { @@ -38,13 +40,20 @@ public class DataConnect { public class func dataConnect(app: FirebaseApp? = FirebaseApp.app(), connectorConfig: ConnectorConfig, - settings: DataConnectSettings = DataConnectSettings()) + settings: DataConnectSettings = DataConnectSettings(), + callerSDKType: CallerSDKType = .base) -> DataConnect { guard let app = app else { fatalError("No Firebase App present") } - return instanceStore.instance(for: app, config: connectorConfig, settings: settings) + return instanceStore + .instance( + for: app, + config: connectorConfig, + settings: settings, + callerSDKType: callerSDKType + ) } // MARK: Emulator @@ -66,7 +75,8 @@ public class DataConnect { settings: settings, connectorConfig: connectorConfig, auth: Auth.auth(app: app), - appCheck: AppCheck.appCheck(app: app) + appCheck: AppCheck.appCheck(app: app), + callerSDKType: callerSDKType ) operationsManager = OperationsManager(grpcClient: grpcClient) @@ -74,21 +84,24 @@ public class DataConnect { // MARK: Init - init(app: FirebaseApp, connectorConfig: ConnectorConfig, settings: DataConnectSettings) { - self.app = app - self.settings = settings - self.connectorConfig = connectorConfig - + init(app: FirebaseApp, connectorConfig: ConnectorConfig, settings: DataConnectSettings, + callerSDKType: CallerSDKType = .base) { guard app.options.projectID != nil else { fatalError("Firebase DataConnect requires the projectID to be set in the app options") } + self.app = app + self.settings = settings + self.connectorConfig = connectorConfig + self.callerSDKType = callerSDKType + grpcClient = GrpcClient( app: self.app, settings: settings, connectorConfig: connectorConfig, auth: Auth.auth(app: app), - appCheck: AppCheck.appCheck(app: app) + appCheck: AppCheck.appCheck(app: app), + callerSDKType: self.callerSDKType ) operationsManager = OperationsManager(grpcClient: grpcClient) } @@ -114,6 +127,13 @@ public class DataConnect { } } +// This enum is public so the gen sdk can access it +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public enum CallerSDKType { + case base // base sdk is directly used + case generated // generated sdk is calling the base +} + // MARK: Instance Creation // Support for creating or reusing DataConnect instances. @@ -145,13 +165,18 @@ private class InstanceStore { var instances = [InstanceKey: DataConnect]() func instance(for app: FirebaseApp, config: ConnectorConfig, - settings: DataConnectSettings) -> DataConnect { + settings: DataConnectSettings, callerSDKType: CallerSDKType) -> DataConnect { accessQ.sync { let key = InstanceKey(app: app, config: config) if let inst = instances[key] { return inst } else { - let dc = DataConnect(app: app, connectorConfig: config, settings: settings) + let dc = DataConnect( + app: app, + connectorConfig: config, + settings: settings, + callerSDKType: callerSDKType + ) instances[key] = dc return dc } diff --git a/Sources/Internal/Component.swift b/Sources/Internal/Component.swift index 869196c..c5d427a 100644 --- a/Sources/Internal/Component.swift +++ b/Sources/Internal/Component.swift @@ -18,6 +18,6 @@ import Foundation @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @objc(FIRDataConnectComponent) class DataConnectComponent: NSObject { @objc class func sdkVersion() -> String { - return Version.version + return Version.sdkVersion } } diff --git a/Sources/Internal/GrpcClient.swift b/Sources/Internal/GrpcClient.swift index fe37b7a..7b88514 100644 --- a/Sources/Internal/GrpcClient.swift +++ b/Sources/Internal/GrpcClient.swift @@ -44,11 +44,14 @@ actor GrpcClient: CustomStringConvertible { private let appCheck: AppCheck? + private let callerSDKType: CallerSDKType + enum RequestHeaders { static let googRequestParamsHeader = "x-goog-request-params" static let authorizationHeader = "x-firebase-auth-token" static let appCheckHeader = "X-Firebase-AppCheck" static let firebaseAppId = "x-firebase-gmpid" + static let googApiClient = "x-goog-api-client" } private let googRequestHeaderValue: String @@ -72,9 +75,23 @@ actor GrpcClient: CustomStringConvertible { } }() + private lazy var googApiClientHeaderValue: String = { + let header = + "gl-swift/\(Version.swiftVersion()) fire/\(Version.sdkVersion) \(Version.platformVersionHeader()) grpc-swift/" + + switch self.callerSDKType { + case .base: + return header + case .generated: + return "\(header) swift/gen" + } + + }() + init(app: FirebaseApp, settings: DataConnectSettings, connectorConfig: ConnectorConfig, auth: Auth, - appCheck: AppCheck?) { + appCheck: AppCheck?, + callerSDKType: CallerSDKType) { self.app = app guard let projectId = app.options.projectID else { @@ -86,6 +103,7 @@ actor GrpcClient: CustomStringConvertible { self.connectorConfig = connectorConfig self.auth = auth self.appCheck = appCheck + self.callerSDKType = callerSDKType connectorName = "projects/\(projectId)/locations/\(connectorConfig.location)/services/\(connectorConfig.serviceId)/connectors/\(connectorConfig.connector)" @@ -187,6 +205,7 @@ actor GrpcClient: CustomStringConvertible { headers.add(name: RequestHeaders.googRequestParamsHeader, value: googRequestHeaderValue) headers.add(name: RequestHeaders.firebaseAppId, value: app.options.googleAppID) + headers.add(name: RequestHeaders.googApiClient, value: googApiClientHeaderValue) // Add Auth token if available do { diff --git a/Sources/Internal/Version.swift b/Sources/Internal/Version.swift index cbcb7c6..b3e53cb 100644 --- a/Sources/Internal/Version.swift +++ b/Sources/Internal/Version.swift @@ -14,7 +14,68 @@ import Foundation +import GoogleUtilities_Environment + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct Version { - static let version = "11.3.0-beta" + static let sdkVersion = "11.3.0-beta" + + // returns value of form gl-PLATFORM_NAME/PLATFORM_VERSION + static func platformVersionHeader() -> String { + let versionString = GULAppEnvironmentUtil.systemVersion() + let platformName = GULAppEnvironmentUtil.applePlatform() + + return "gl-\(platformName)/\(versionString)" + } + + // returns the build time major version of swift + static func swiftVersion() -> String { + #if swift(>=6) + return "6" + #elseif swift(>=5.10) + return "5.10" + #elseif swift(>=5.9) + return "5.9" + #elseif swift(>=5.8) + return "5.8" + #elseif swift(>=5.7) + return "5.7" + #elseif swift(>=5.6) + return "5.6" + #elseif swift(>=5.5) + return "5.5" + #elseif swift(>=5.4) + return "5.4" + #elseif swift(>=5.2) + return "5.2" + #elseif swift(>=5.1) + return "5.1" + #elseif swift(>=5.0) + return "5.0" + #elseif swift(>=4.2) + return "4.2" + #elseif swift(>=4.1) + return "4.1" + #elseif swift(>=4.0) + return "4.0" + #elseif swift(>=3.1) + return "3.1" + #elseif swift(>=3.0) + return "3.0" + #elseif swift(>=2.2) + return "2.2" + #elseif swift(>=2.1) + return "2.1" + #elseif swift(>=2.0) + return "2.0" + #elseif swift(>=1.2) + return "1.2" + #elseif swift(>=1.1) + return "1.1" + #elseif swift(>=1.0) + return "1.0" + #else + return "" + #endif + } } diff --git a/Tests/Unit/HeaderTests.swift b/Tests/Unit/HeaderTests.swift index 68207f1..1633982 100644 --- a/Tests/Unit/HeaderTests.swift +++ b/Tests/Unit/HeaderTests.swift @@ -36,6 +36,12 @@ final class HeaderTests: XCTestCase { connector: "kitchensink" ) + var fakeConnectorConfigTwo = ConnectorConfig( + serviceId: "dataconnect", + location: "us-east1", + connector: "kitchensinkgen" + ) + override class func setUp() { FirebaseApp.configure(options: options) defaultApp = FirebaseApp.app() @@ -50,4 +56,35 @@ final class HeaderTests: XCTestCase { let contains = values.contains { $0 == HeaderTests.defaultApp!.options.googleAppID } XCTAssertTrue(contains) } + + func testGoogApiClientHeaderBaseSdk() async throws { + let dcOne = DataConnect.dataConnect(connectorConfig: fakeConnectorConfigOne) + let callOptions = await dcOne.grpcClient.createCallOptions() + let values = callOptions.customMetadata.values( + forHeader: GrpcClient.RequestHeaders.googApiClient, canonicalForm: false + ) + let contains = values + .contains { + $0 == + "gl-swift/\(Version.swiftVersion()) fire/\(Version.sdkVersion) \(Version.platformVersionHeader()) grpc-swift/" + } + XCTAssertTrue(contains) + } + + func testGoogleApiClientHeaderGenSdk() async throws { + let dcOne = DataConnect.dataConnect( + connectorConfig: fakeConnectorConfigTwo, + callerSDKType: .generated + ) + let callOptions = await dcOne.grpcClient.createCallOptions() + let values = callOptions.customMetadata.values( + forHeader: GrpcClient.RequestHeaders.googApiClient, canonicalForm: false + ) + let contains = values + .contains { + $0 == + "gl-swift/\(Version.swiftVersion()) fire/\(Version.sdkVersion) \(Version.platformVersionHeader()) grpc-swift/ swift/gen" + } + XCTAssertTrue(contains) + } } diff --git a/Tests/Unit/UserAgentTests.swift b/Tests/Unit/UserAgentTests.swift index c186055..5dcc561 100644 --- a/Tests/Unit/UserAgentTests.swift +++ b/Tests/Unit/UserAgentTests.swift @@ -34,7 +34,7 @@ final class UserAgentTests: XCTestCase { /// Confirm that Data Connect gets added to the user agent. func testUserAgent() { let userAgent = FirebaseApp.firebaseUserAgent() - let version = Version.version + let version = Version.sdkVersion XCTAssertTrue(userAgent.contains("fire-dc/\(version)")) } }