[sub-spm-optional-products] Add LCECryptoKit and LCFeatureControl as optional sub-SPM products
LCEssentials installs standalone; each sub target-depends on it so linking a sub's product always pulls LCEssentials in too, without the consumer having to declare it separately. - LCECryptoKit: internalized from the remote LCECryptoKitBinary git dependency (embedded token URL removed) into a local binaryTarget vendoring Frameworks/LCECryptoKit.xcframework. LCECryptoKitManager moved out of LCEssentials core into its own LCECryptoKitManager target/product; the no-op fallback for when the binary wasn't linked is gone (breaking change for existing consumers, see decisions/2026-09-15-sub-spm-optional-products.md). - LCFeatureControl: new product wrapping Atomenta's Feature Control API (flag evaluation with TTL cache + safe-degrade fallback to defaults, notifications inbox, batched exposure telemetry). 45 new tests.
This commit is contained in:
61
Sources/LCFeatureControl/FeatureControlFlag.swift
Normal file
61
Sources/LCFeatureControl/FeatureControlFlag.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
/// One evaluated flag, as returned inside `EvaluateResponse.result.flags[key]`.
|
||||
public struct FeatureControlFlag: Decodable, Sendable, Equatable {
|
||||
public let enabled: Bool
|
||||
public let variant: String?
|
||||
public let payload: FeatureControlJSON?
|
||||
public let reason: String?
|
||||
|
||||
public init(enabled: Bool, variant: String?, payload: FeatureControlJSON?, reason: String?) {
|
||||
self.enabled = enabled
|
||||
self.variant = variant
|
||||
self.payload = payload
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
/// `EvaluateResponse.result` — the batch evaluation result for a set of keys.
|
||||
public struct FeatureControlSnapshot: Sendable, Equatable {
|
||||
public let evaluatedAt: Date
|
||||
public let configVersion: Int
|
||||
public let flags: [String: FeatureControlFlag]
|
||||
|
||||
public init(evaluatedAt: Date, configVersion: Int, flags: [String: FeatureControlFlag]) {
|
||||
self.evaluatedAt = evaluatedAt
|
||||
self.configVersion = configVersion
|
||||
self.flags = flags
|
||||
}
|
||||
}
|
||||
|
||||
extension FeatureControlSnapshot: Decodable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case evaluatedAt, configVersion, flags
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let rawDate = try container.decode(String.self, forKey: .evaluatedAt)
|
||||
guard let date = FeatureControlDateParsing.parse(rawDate) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: .evaluatedAt, in: container,
|
||||
debugDescription: "Unrecognized date format: \(rawDate)")
|
||||
}
|
||||
self.evaluatedAt = date
|
||||
self.configVersion = try container.decode(Int.self, forKey: .configVersion)
|
||||
self.flags = try container.decode([String: FeatureControlFlag].self, forKey: .flags)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire envelope for `POST /api/feature-control/evaluate` — `{error, code, result}`.
|
||||
struct FeatureControlEvaluateEnvelope: Decodable, Sendable {
|
||||
let error: Bool
|
||||
let code: String?
|
||||
let result: FeatureControlSnapshot
|
||||
}
|
||||
|
||||
/// Request body for `POST /api/feature-control/evaluate`.
|
||||
struct FeatureControlEvaluateRequestBody: Encodable, Sendable {
|
||||
let environment: String
|
||||
let keys: [String]
|
||||
let context: FeatureControlContext
|
||||
}
|
||||
Reference in New Issue
Block a user