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/*.wav
|
||||
packages/swift-platform-sdk/build/
|
||||
packages/swift-platform-sdk/.build/
|
||||
packages/swift-platform-sdk/.swiftpm/
|
||||
packages/kotlin-platform-sdk/build/
|
||||
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