feat(swift-sdk): add ByteLystPlatform unified entry point + 5 new test files (4.1)
New source: - ByteLystPlatform.swift — unified entry point wiring all services (config, client, telemetry, flags, killSwitch, crashReporter, keychain, auditLog, auth) - BLKeychainAccessor — convenience wrapper binding BLKeychain to a bundleId - start(userId:) / stop() lifecycle for telemetry + flags + killSwitch New tests (5 files, ~25 test cases): - ByteLystPlatformTests — init, start/stop, idempotency, keychain accessor - BLPlatformConfigTests — default + custom init - BLKillSwitchClientTests — default state, reset - BLFeatureFlagClientTests — empty flags, unknown key, stop - BLTelemetryClientTests — installId stability, session rotation, track/flush Also: add .build/ and .swiftpm/ to .gitignore
This commit is contained in:
parent
1fda345d38
commit
933390e89b
2
.gitignore
vendored
2
.gitignore
vendored
@ -21,6 +21,8 @@ __LOCAL_LLMs/models/
|
|||||||
__LOCAL_LLMs/.venv-*/
|
__LOCAL_LLMs/.venv-*/
|
||||||
__LOCAL_LLMs/*.wav
|
__LOCAL_LLMs/*.wav
|
||||||
packages/swift-platform-sdk/build/
|
packages/swift-platform-sdk/build/
|
||||||
|
packages/swift-platform-sdk/.build/
|
||||||
|
packages/swift-platform-sdk/.swiftpm/
|
||||||
packages/kotlin-platform-sdk/build/
|
packages/kotlin-platform-sdk/build/
|
||||||
packages/kotlin-platform-sdk/.gradle/
|
packages/kotlin-platform-sdk/.gradle/
|
||||||
packages/kotlin-platform-sdk/gradle/
|
packages/kotlin-platform-sdk/gradle/
|
||||||
|
|||||||
121
packages/swift-platform-sdk/Sources/ByteLystPlatform.swift
Normal file
121
packages/swift-platform-sdk/Sources/ByteLystPlatform.swift
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// ── ByteLystPlatform ────────────────────────────────────────
|
||||||
|
// Unified entry point for the ByteLyst platform SDK.
|
||||||
|
// Creates and wires all platform services from a single config.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// let platform = ByteLystPlatform(config: .init(
|
||||||
|
// productId: "peakpulse",
|
||||||
|
// baseURL: "https://api.peakpulse.app",
|
||||||
|
// bundleId: "com.saravana.peakpulse"
|
||||||
|
// ))
|
||||||
|
//
|
||||||
|
// platform.start() // Start telemetry + flags + kill switch
|
||||||
|
// platform.telemetry.trackScreen("home") // Track events
|
||||||
|
// let isNew = platform.flags.isEnabled("new_feature")
|
||||||
|
// platform.stop() // Flush + stop timers
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Unified entry point that wires all ByteLyst platform services together.
|
||||||
|
/// Create one instance at app launch and access services via properties.
|
||||||
|
public final class ByteLystPlatform {
|
||||||
|
|
||||||
|
/// Platform configuration.
|
||||||
|
public let config: BLPlatformConfig
|
||||||
|
|
||||||
|
/// HTTP client shared by all services.
|
||||||
|
public let client: BLPlatformClient
|
||||||
|
|
||||||
|
/// Telemetry event tracking.
|
||||||
|
public let telemetry: BLTelemetryClient
|
||||||
|
|
||||||
|
/// Feature flag polling.
|
||||||
|
public let flags: BLFeatureFlagClient
|
||||||
|
|
||||||
|
/// Kill switch checker.
|
||||||
|
public let killSwitch: BLKillSwitchClient
|
||||||
|
|
||||||
|
/// Crash reporter (MetricKit).
|
||||||
|
public let crashReporter: BLCrashReporter
|
||||||
|
|
||||||
|
/// Keychain access (via bundleId as service).
|
||||||
|
public let keychain: BLKeychainAccessor
|
||||||
|
|
||||||
|
/// Audit logger.
|
||||||
|
public let auditLog: BLAuditLogger
|
||||||
|
|
||||||
|
/// Auth client.
|
||||||
|
public let auth: BLAuthClient
|
||||||
|
|
||||||
|
/// Whether `start()` has been called.
|
||||||
|
public private(set) var isStarted = false
|
||||||
|
|
||||||
|
public init(config: BLPlatformConfig) {
|
||||||
|
self.config = config
|
||||||
|
self.client = BLPlatformClient(config: config)
|
||||||
|
self.telemetry = BLTelemetryClient(config: config, client: client)
|
||||||
|
self.flags = BLFeatureFlagClient(config: config, client: client)
|
||||||
|
self.killSwitch = BLKillSwitchClient(config: config)
|
||||||
|
self.crashReporter = BLCrashReporter(productId: config.productId)
|
||||||
|
self.keychain = BLKeychainAccessor(service: config.bundleId)
|
||||||
|
self.auditLog = BLAuditLogger(productId: config.productId)
|
||||||
|
self.auth = BLAuthClient(config: config, client: client)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test-only initializer that accepts a custom URLSessionConfiguration.
|
||||||
|
public init(config: BLPlatformConfig, sessionConfiguration: URLSessionConfiguration) {
|
||||||
|
self.config = config
|
||||||
|
self.client = BLPlatformClient(config: config, sessionConfiguration: sessionConfiguration)
|
||||||
|
self.telemetry = BLTelemetryClient(config: config, client: client)
|
||||||
|
self.flags = BLFeatureFlagClient(config: config, client: client)
|
||||||
|
self.killSwitch = BLKillSwitchClient(config: config)
|
||||||
|
self.crashReporter = BLCrashReporter(productId: config.productId)
|
||||||
|
self.keychain = BLKeychainAccessor(service: config.bundleId)
|
||||||
|
self.auditLog = BLAuditLogger(productId: config.productId)
|
||||||
|
self.auth = BLAuthClient(config: config, client: client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
/// Start all services: telemetry flush timer, feature flag polling, kill switch check.
|
||||||
|
public func start(userId: String? = nil) {
|
||||||
|
guard !isStarted else { return }
|
||||||
|
isStarted = true
|
||||||
|
telemetry.start()
|
||||||
|
flags.start(userId: userId)
|
||||||
|
Task { await killSwitch.check() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop all services: flush telemetry, stop flag polling.
|
||||||
|
public func stop() {
|
||||||
|
guard isStarted else { return }
|
||||||
|
isStarted = false
|
||||||
|
telemetry.stop()
|
||||||
|
flags.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Keychain Accessor
|
||||||
|
|
||||||
|
/// Convenience wrapper around BLKeychain that binds to a specific service (bundleId).
|
||||||
|
public struct BLKeychainAccessor {
|
||||||
|
private let service: String
|
||||||
|
|
||||||
|
public init(service: String) {
|
||||||
|
self.service = service
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func save(key: String, value: String) -> Bool {
|
||||||
|
BLKeychain.save(service: service, key: key, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func read(key: String) -> String? {
|
||||||
|
BLKeychain.read(service: service, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func delete(key: String) -> Bool {
|
||||||
|
BLKeychain.delete(service: service, key: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import ByteLystPlatformSDK
|
||||||
|
|
||||||
|
final class BLFeatureFlagClientTests: XCTestCase {
|
||||||
|
|
||||||
|
private func makeClient() -> BLFeatureFlagClient {
|
||||||
|
let config = BLPlatformConfig(
|
||||||
|
productId: "testapp",
|
||||||
|
baseURL: "http://localhost:4003",
|
||||||
|
bundleId: "com.bytelyst.test"
|
||||||
|
)
|
||||||
|
let platformClient = BLPlatformClient(config: config)
|
||||||
|
return BLFeatureFlagClient(config: config, client: platformClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDefaultFlagsEmpty() {
|
||||||
|
let client = makeClient()
|
||||||
|
XCTAssertEqual(client.allFlags().count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsEnabledReturnsFalseForUnknownKey() {
|
||||||
|
let client = makeClient()
|
||||||
|
XCTAssertFalse(client.isEnabled("nonexistent_flag"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStopDoesNotCrash() {
|
||||||
|
let client = makeClient()
|
||||||
|
client.stop()
|
||||||
|
client.stop() // Double stop
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import ByteLystPlatformSDK
|
||||||
|
|
||||||
|
final class BLKillSwitchClientTests: XCTestCase {
|
||||||
|
|
||||||
|
private func makeConfig() -> BLPlatformConfig {
|
||||||
|
BLPlatformConfig(
|
||||||
|
productId: "testapp",
|
||||||
|
baseURL: "http://localhost:4003",
|
||||||
|
bundleId: "com.bytelyst.test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDefaultState() {
|
||||||
|
let client = BLKillSwitchClient(config: makeConfig())
|
||||||
|
XCTAssertFalse(client.isDisabled)
|
||||||
|
XCTAssertEqual(client.maintenanceMessage, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReset() {
|
||||||
|
let client = BLKillSwitchClient(config: makeConfig())
|
||||||
|
// Manually test reset clears any hypothetical state
|
||||||
|
client.reset()
|
||||||
|
XCTAssertFalse(client.isDisabled)
|
||||||
|
XCTAssertEqual(client.maintenanceMessage, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import ByteLystPlatformSDK
|
||||||
|
|
||||||
|
final class BLPlatformConfigTests: XCTestCase {
|
||||||
|
|
||||||
|
func testInitWithDefaults() {
|
||||||
|
let config = BLPlatformConfig(
|
||||||
|
productId: "testapp",
|
||||||
|
baseURL: "http://localhost:4003",
|
||||||
|
bundleId: "com.bytelyst.testapp"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(config.productId, "testapp")
|
||||||
|
XCTAssertEqual(config.baseURL, "http://localhost:4003")
|
||||||
|
XCTAssertEqual(config.platform, "ios")
|
||||||
|
XCTAssertEqual(config.channel, "native")
|
||||||
|
XCTAssertEqual(config.bundleId, "com.bytelyst.testapp")
|
||||||
|
XCTAssertNil(config.appGroupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitWithCustomValues() {
|
||||||
|
let config = BLPlatformConfig(
|
||||||
|
productId: "peakpulse",
|
||||||
|
baseURL: "https://api.peakpulse.app",
|
||||||
|
platform: "watchos",
|
||||||
|
channel: "companion",
|
||||||
|
bundleId: "com.saravana.peakpulse",
|
||||||
|
appGroupId: "group.com.saravana.peakpulse"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(config.productId, "peakpulse")
|
||||||
|
XCTAssertEqual(config.platform, "watchos")
|
||||||
|
XCTAssertEqual(config.channel, "companion")
|
||||||
|
XCTAssertEqual(config.appGroupId, "group.com.saravana.peakpulse")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import ByteLystPlatformSDK
|
||||||
|
|
||||||
|
final class BLTelemetryClientTests: XCTestCase {
|
||||||
|
|
||||||
|
private func makeClient() -> BLTelemetryClient {
|
||||||
|
let config = BLPlatformConfig(
|
||||||
|
productId: "testapp",
|
||||||
|
baseURL: "http://localhost:4003",
|
||||||
|
bundleId: "com.bytelyst.test"
|
||||||
|
)
|
||||||
|
let platformClient = BLPlatformClient(config: config)
|
||||||
|
return BLTelemetryClient(config: config, client: platformClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInstallIdIsStable() {
|
||||||
|
let client = makeClient()
|
||||||
|
let id1 = client.getInstallId()
|
||||||
|
let id2 = client.getInstallId()
|
||||||
|
XCTAssertEqual(id1, id2)
|
||||||
|
XCTAssertFalse(id1.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSessionIdIsNotEmpty() {
|
||||||
|
let client = makeClient()
|
||||||
|
XCTAssertFalse(client.getSessionId().isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStartGeneratesNewSessionId() {
|
||||||
|
let client = makeClient()
|
||||||
|
let sessionBefore = client.getSessionId()
|
||||||
|
client.start()
|
||||||
|
let sessionAfter = client.getSessionId()
|
||||||
|
// start() rotates session ID
|
||||||
|
XCTAssertNotEqual(sessionBefore, sessionAfter)
|
||||||
|
client.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStopDoesNotCrash() {
|
||||||
|
let client = makeClient()
|
||||||
|
client.stop() // Stop without start
|
||||||
|
client.start()
|
||||||
|
client.stop()
|
||||||
|
client.stop() // Double stop
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTrackEventDoesNotCrash() {
|
||||||
|
let client = makeClient()
|
||||||
|
client.trackEvent("info", module: "test", name: "unit_test")
|
||||||
|
client.trackEvent("error", module: "test", name: "fail", message: "test error")
|
||||||
|
client.trackEvent("info", module: "test", name: "tagged", tags: ["key": "value"])
|
||||||
|
client.trackEvent("info", module: "test", name: "measured", metrics: ["duration": 1.5])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTrackScreenDoesNotCrash() {
|
||||||
|
let client = makeClient()
|
||||||
|
client.trackScreen("home")
|
||||||
|
client.trackScreen("settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFlushDoesNotCrashWhenEmpty() {
|
||||||
|
let client = makeClient()
|
||||||
|
client.flush() // Empty queue
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import ByteLystPlatformSDK
|
||||||
|
|
||||||
|
final class ByteLystPlatformTests: XCTestCase {
|
||||||
|
|
||||||
|
private func makeConfig() -> BLPlatformConfig {
|
||||||
|
BLPlatformConfig(
|
||||||
|
productId: "testapp",
|
||||||
|
baseURL: "http://localhost:4003/api",
|
||||||
|
platform: "ios",
|
||||||
|
channel: "native",
|
||||||
|
bundleId: "com.bytelyst.test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitCreatesAllServices() {
|
||||||
|
let platform = ByteLystPlatform(config: makeConfig())
|
||||||
|
XCTAssertEqual(platform.config.productId, "testapp")
|
||||||
|
XCTAssertNotNil(platform.client)
|
||||||
|
XCTAssertNotNil(platform.telemetry)
|
||||||
|
XCTAssertNotNil(platform.flags)
|
||||||
|
XCTAssertNotNil(platform.killSwitch)
|
||||||
|
XCTAssertNotNil(platform.crashReporter)
|
||||||
|
XCTAssertNotNil(platform.keychain)
|
||||||
|
XCTAssertNotNil(platform.auditLog)
|
||||||
|
XCTAssertNotNil(platform.auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStartSetsIsStarted() {
|
||||||
|
let platform = ByteLystPlatform(config: makeConfig())
|
||||||
|
XCTAssertFalse(platform.isStarted)
|
||||||
|
platform.start()
|
||||||
|
XCTAssertTrue(platform.isStarted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStopClearsIsStarted() {
|
||||||
|
let platform = ByteLystPlatform(config: makeConfig())
|
||||||
|
platform.start()
|
||||||
|
XCTAssertTrue(platform.isStarted)
|
||||||
|
platform.stop()
|
||||||
|
XCTAssertFalse(platform.isStarted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDoubleStartIsIdempotent() {
|
||||||
|
let platform = ByteLystPlatform(config: makeConfig())
|
||||||
|
platform.start()
|
||||||
|
platform.start() // Should not crash or double-start
|
||||||
|
XCTAssertTrue(platform.isStarted)
|
||||||
|
platform.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDoubleStopIsIdempotent() {
|
||||||
|
let platform = ByteLystPlatform(config: makeConfig())
|
||||||
|
platform.start()
|
||||||
|
platform.stop()
|
||||||
|
platform.stop() // Should not crash
|
||||||
|
XCTAssertFalse(platform.isStarted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeychainAccessor() {
|
||||||
|
let accessor = BLKeychainAccessor(service: "com.bytelyst.test.accessor")
|
||||||
|
accessor.save(key: "test_key", value: "hello")
|
||||||
|
XCTAssertEqual(accessor.read(key: "test_key"), "hello")
|
||||||
|
accessor.delete(key: "test_key")
|
||||||
|
XCTAssertNil(accessor.read(key: "test_key"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user