Skip to content

Commit

Permalink
chore: extract classes needed to be shared (#408)
Browse files Browse the repository at this point in the history
Co-authored-by: Muhammad Rehan <mrehan.official@gmail.com>
  • Loading branch information
Shahroz16 and mrehan27 authored Nov 27, 2023
1 parent 624312f commit d90ae74
Showing 46 changed files with 3,688 additions and 3,459 deletions.
2 changes: 1 addition & 1 deletion CustomerIOMessagingInApp.podspec
Original file line number Diff line number Diff line change
@@ -20,5 +20,5 @@ Pod::Spec.new do |spec|
spec.source_files = "Sources/MessagingInApp/**/*"
spec.module_name = "CioMessagingInApp" # the `import X` name when using SDK in Swift files

spec.dependency "CustomerIOTracking", "= #{spec.version.to_s}"
spec.dependency "CustomerIOCommon", "= #{spec.version.to_s}"
end
4 changes: 2 additions & 2 deletions CustomerIOMessagingPush.podspec
Original file line number Diff line number Diff line change
@@ -20,5 +20,5 @@ Pod::Spec.new do |spec|
spec.source_files = "Sources/MessagingPush/**/*"
spec.module_name = "CioMessagingPush" # the `import X` name when using SDK in Swift files

spec.dependency "CustomerIOTracking", "= #{spec.version.to_s}"
end
spec.dependency "CustomerIOCommon", "= #{spec.version.to_s}"
end
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -7,10 +7,10 @@ SHELL = /bin/sh
generate:
./binny sourcery --sources Sources/Common --templates Sources/Templates --output Sources/Common/autogenerated
./binny sourcery --sources Sources/Tracking --templates Sources/Templates --output Sources/Tracking/autogenerated --args imports=CioInternalCommon
./binny sourcery --sources Sources/MessagingPush --templates Sources/Templates --output Sources/MessagingPush/autogenerated --args imports=CioTracking-CioInternalCommon
./binny sourcery --sources Sources/MessagingPushAPN --templates Sources/Templates --output Sources/MessagingPushAPN/autogenerated --args imports=CioMessagingPush-CioTracking-CioInternalCommon
./binny sourcery --sources Sources/MessagingPushFCM --templates Sources/Templates --output Sources/MessagingPushFCM/autogenerated --args imports=CioMessagingPush-CioTracking-CioInternalCommon
./binny sourcery --sources Sources/MessagingInApp --templates Sources/Templates --output Sources/MessagingInApp/autogenerated --args imports=CioTracking-CioInternalCommon
./binny sourcery --sources Sources/MessagingPush --templates Sources/Templates --output Sources/MessagingPush/autogenerated --args imports=CioInternalCommon
./binny sourcery --sources Sources/MessagingPushAPN --templates Sources/Templates --output Sources/MessagingPushAPN/autogenerated --args imports=CioMessagingPush-CioInternalCommon
./binny sourcery --sources Sources/MessagingPushFCM --templates Sources/Templates --output Sources/MessagingPushFCM/autogenerated --args imports=CioMessagingPush-CioInternalCommon
./binny sourcery --sources Sources/MessagingInApp --templates Sources/Templates --output Sources/MessagingInApp/autogenerated --args imports=CioInternalCommon

lint:
./binny swiftlint lint --strict
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ let package = Package(

// Messaging Push
.target(name: "CioMessagingPush",
dependencies: ["CioTracking"],
dependencies: ["CioInternalCommon"],
path: "Sources/MessagingPush"),
.testTarget(name: "MessagingPushTests",
dependencies: ["CioMessagingPush", "SharedTests"],
@@ -92,7 +92,7 @@ let package = Package(

// Messaging in-app
.target(name: "CioMessagingInApp",
dependencies: ["CioTracking"],
dependencies: ["CioInternalCommon"],
path: "Sources/MessagingInApp"),
.testTarget(name: "MessagingInAppTests",
dependencies: ["CioMessagingInApp", "SharedTests"],
40 changes: 1 addition & 39 deletions Sources/Common/DIGraph.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public class DIGraph {
public class DIGraph: DIManager {
public let sdkConfig: SdkConfig

public init(sdkConfig: SdkConfig) {
@@ -9,42 +9,4 @@ public class DIGraph {

public var overrides: [String: Any] = [:]
public var singletons: [String: Any] = [:]

/**
Designed to be used only in test classes to override dependencies.

```
let mockOffRoadWheels = // make a mock of OffRoadWheels class
DIGraph.shared.override(mockOffRoadWheels, OffRoadWheels.self)
```
*/
public func override<T: Any>(value: T, forType type: T.Type) {
overrides[String(describing: type)] = value
}

// Retrieves an overridden instance of a specified type from the `overrides` dictionary.
// If an overridden instance exists and can be cast to the specified type, it is returned; otherwise, nil is returned.
public func getOverriddenInstance<T: Any>() -> T? {
// Get the type name as the key for the dictionary.
let typeName = String(describing: T.self)

guard overrides[typeName] != nil else {
return nil // no override set. Quit early.
}

// Get and cast the overridden instance from the dictionary.
guard let overriddenInstance = overrides[typeName] as? T else {
fatalError("Failed to cast overridden instance to type '\(typeName)'.")
}

return overriddenInstance
}

/**
Reset graph. Meant to be used in `tearDown()` of tests.
*/
public func reset() {
overrides = [:]
singletons = [:]
}
}
8 changes: 8 additions & 0 deletions Sources/Common/DIGraphShared.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

public class DIGraphShared: DIManager {
public static let shared: DIGraphShared = .init()

public var singletons: [String: Any] = [:]
public var overrides: [String: Any] = [:]
}
48 changes: 48 additions & 0 deletions Sources/Common/DIManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

public protocol DIManager: AnyObject {
var overrides: [String: Any] { get set }
var singletons: [String: Any] { get set }

func override<T: Any>(value: T, forType type: T.Type)
func getOverriddenInstance<T: Any>() -> T?
func reset()
}

public extension DIManager {
/**
Designed to be used only in test classes to override dependencies.

```
let mockOffRoadWheels = // make a mock of OffRoadWheels class
DIGraph.shared.override(mockOffRoadWheels, OffRoadWheels.self)
```
*/
func override<T>(value: T, forType type: T.Type) {
overrides[String(describing: type)] = value
}

// Retrieves an overridden instance of a specified type from the `overrides` dictionary.
// If an overridden instance exists and can be cast to the specified type, it is returned; otherwise, nil is returned.
func getOverriddenInstance<T: Any>() -> T? {
// Get the type name as the key for the dictionary.
let typeName = String(describing: T.self)

guard overrides[typeName] != nil else {
return nil // no override set. Quit early.
}

// Get and cast the overridden instance from the dictionary.
guard let overriddenInstance = overrides[typeName] as? T else {
fatalError("Failed to cast overridden instance to type '\(typeName)'.")
}

return overriddenInstance
}

// Reset the DI graph (useful for testing)
func reset() {
singletons = [:]
overrides = [:]
}
}
40 changes: 40 additions & 0 deletions Sources/Common/Module/ModuleTopLevelObject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

// Base class meant to be subclassed by top-level classes such as `MessagingPush` and `MessagingInApp`. Provides some
// boilerplate logic to creating an implementation object
// once the SDK has finally been initialized.
//
// Top-level class meaning it contains public facing SDK functions called by customers.
// There isn't a constructor populated via dependency injection. It's at the top node
// of dependencies.
// TODO: [CDP] Remove class if no longer needed
open class ModuleTopLevelObject<ImplementationClass> {
private(set) var alreadyCreatedImplementation: ImplementationClass?
public var implementation: ImplementationClass? {
alreadyCreatedImplementation ?? createAndSetImplementationInstance()
}

// for writing tests
public init(implementation: ImplementationClass?) {
self.alreadyCreatedImplementation = implementation
}

// singleton constructor
public init() {
}

private func createAndSetImplementationInstance() -> ImplementationClass? {
let newInstance = getImplementationInstance()
alreadyCreatedImplementation = newInstance
return newInstance
}

// We want each top level module to have an initialize function so that features get setup as early as possible
open func inititlizeModule() {
fatalError("forgot to override in subclass")
}

open func getImplementationInstance() -> ImplementationClass {
fatalError("forgot to override in subclass")
}
}
39 changes: 39 additions & 0 deletions Sources/Common/Store/GlobalDataStore.swift
Original file line number Diff line number Diff line change
@@ -53,3 +53,42 @@ public class CioGlobalDataStore: GlobalDataStore {
keyValueStorage.deleteAll()
}
}

// sourcery: InjectRegisterShared = "GlobalDataStore"
public class CioSharedDataStore: GlobalDataStore {
private let keyValueStorage: SharedKeyValueStorage

public var pushDeviceToken: String? {
get {
keyValueStorage.string(.pushDeviceToken)
}
set {
keyValueStorage.setString(newValue, forKey: .pushDeviceToken)
}
}

public var httpRequestsPauseEnds: Date? {
get {
keyValueStorage.date(.httpRequestsPauseEnds)
}
set {
keyValueStorage.setDate(newValue, forKey: .httpRequestsPauseEnds)
}
}

public init(keyValueStorage: SharedKeyValueStorage) {
self.keyValueStorage = keyValueStorage
}

// How to get instance before DI graph is constructed
public static func getInstance() -> GlobalDataStore {
let newInstance = UserDefaultsSharedKeyValueStorage(
deviceMetricsGrabber: DeviceMetricsGrabberImpl()
)
return CioSharedDataStore(keyValueStorage: newInstance)
}

public func deleteAll() {
keyValueStorage.deleteAll()
}
}
1 change: 1 addition & 0 deletions Sources/Common/Util/DeviceMetricsGrabber.swift
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ protocol DeviceMetricsGrabber: AutoMockable {
}

// sourcery: InjectRegister = "DeviceMetricsGrabber"
// sourcery: InjectRegisterShared = "DeviceMetricsGrabber"
class DeviceMetricsGrabberImpl: DeviceMetricsGrabber {
var appBundleId: String? {
Bundle.main.bundleIdentifier
90 changes: 89 additions & 1 deletion Sources/Common/Util/KeyValueStorage.swift
Original file line number Diff line number Diff line change
@@ -3,7 +3,22 @@ import Foundation
/**
Stores data in key/value pairs.
*/
public protocol KeyValueStorage {
public protocol SharedKeyValueStorage {
func integer(_ key: KeyValueStorageKey) -> Int?
func setInt(_ value: Int?, forKey key: KeyValueStorageKey)
func double(_ key: KeyValueStorageKey) -> Double?
func setDouble(_ value: Double?, forKey key: KeyValueStorageKey)
func string(_ key: KeyValueStorageKey) -> String?
func setString(_ value: String?, forKey key: KeyValueStorageKey)
func date(_ key: KeyValueStorageKey) -> Date?
func setDate(_ value: Date?, forKey key: KeyValueStorageKey)
func deleteAll()
}

/**
Stores data in key/value pairs.
*/
public protocol KeyValueStorage: SharedKeyValueStorage {
func switchToGlobalDataStore()

func integer(_ key: KeyValueStorageKey) -> Int?
@@ -104,3 +119,76 @@ public class UserDefaultsKeyValueStorage: KeyValueStorage {
userDefaults?.deleteAll()
}
}

// sourcery: InjectRegisterShared = "SharedKeyValueStorage"
public class UserDefaultsSharedKeyValueStorage: SharedKeyValueStorage {
private let deviceMetricsGrabber: DeviceMetricsGrabber

private var userDefaults: UserDefaults? {
UserDefaults(suiteName: getFileName())
}

init(deviceMetricsGrabber: DeviceMetricsGrabber) {
self.deviceMetricsGrabber = deviceMetricsGrabber
}

/**
We want to sandbox all of the data for each CIO workspace and app.
Therefore, we use the app's bundle ID to be unique to the app and the siteId to separate all of the sites.

We also need to have 1 set of UserPreferences that all workspaces of an app share.
For these moments, use `shared` as the `siteId` value.
*/
func getFileName() -> String {
var appUniqueIdentifier = ""
if let appBundleId = deviceMetricsGrabber.appBundleId {
appUniqueIdentifier = ".\(appBundleId)"
}

var siteIdPart = ".shared" // used for storing global data used for all site-ids.

return "io.customer.sdk\(appUniqueIdentifier)\(siteIdPart)"
}

public func integer(_ key: KeyValueStorageKey) -> Int? {
let value = userDefaults?.integer(forKey: key.rawValue)
return value == 0 ? nil : value
}

public func setInt(_ value: Int?, forKey key: KeyValueStorageKey) {
userDefaults?.set(value, forKey: key.rawValue)
}

public func double(_ key: KeyValueStorageKey) -> Double? {
let value = userDefaults?.double(forKey: key.rawValue)
return value == 0 ? nil : value
}

public func setDouble(_ value: Double?, forKey key: KeyValueStorageKey) {
userDefaults?.set(value, forKey: key.rawValue)
}

public func string(_ key: KeyValueStorageKey) -> String? {
userDefaults?.string(forKey: key.rawValue)
}

public func setString(_ value: String?, forKey key: KeyValueStorageKey) {
userDefaults?.set(value, forKey: key.rawValue)
}

public func date(_ key: KeyValueStorageKey) -> Date? {
guard let millis = userDefaults?.double(forKey: key.rawValue), millis > 0 else {
return nil
}

return Date(timeIntervalSince1970: millis)
}

public func setDate(_ value: Date?, forKey key: KeyValueStorageKey) {
userDefaults?.set(value?.timeIntervalSince1970, forKey: key.rawValue)
}

public func deleteAll() {
userDefaults?.deleteAll()
}
}
41 changes: 41 additions & 0 deletions Sources/Common/Util/Log.swift
Original file line number Diff line number Diff line change
@@ -111,6 +111,47 @@ public class ConsoleLogger: Logger {
}
}

// sourcery: InjectRegisterShared = "Logger"
public class SharedConsoleLogger: Logger {
// allows filtering in Console mac app
public static let logSubsystem = "io.customer.sdk"
public static let logCategory = "CIO"

private func printMessage(_ message: String, _ level: CioLogLevel) {
ConsoleLogger.logMessageToConsole(message, level: level)
}

public func debug(_ message: String) {
printMessage(message, .debug)
}

public func info(_ message: String) {
printMessage("ℹ️ \(message)", .info)
}

public func error(_ message: String) {
printMessage("🛑 \(message)", .error)
}

public static func logMessageToConsole(_ message: String, level: CioLogLevel) {
#if canImport(os)
// Unified logging for Swift. https://www.avanderlee.com/workflow/oslog-unified-logging/
// This means we can view logs in xcode console + Console app.
if #available(iOS 14, *) {
let logger = os.Logger(subsystem: self.logSubsystem, category: self.logCategory)
logger.log(level: level.osLogLevel, "\(message, privacy: .public)")
} else {
let logger = OSLog(subsystem: logSubsystem, category: logCategory)
os_log("%{public}@", log: logger, type: level.osLogLevel, message)
}
#else
// At this time, Linux cannot use `os.log` or `OSLog`. Instead, use: https://github.com/apple/swift-log/
// As we don't officially support Linux at this time, no need to add a dependency to the project.
// therefore, we are not logging if can't import os.log
#endif
}
}

// Log messages to customers when the SDK is not initialized.
// Great for alerts when the SDK may not be setup correctly.
// Since the SDK is not initialized, dependencies graph is not created.
Loading

0 comments on commit d90ae74

Please sign in to comment.