[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:
51
Sources/LCFeatureControl/FeatureControlError.swift
Normal file
51
Sources/LCFeatureControl/FeatureControlError.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
import LCEssentials
|
||||
|
||||
/// Explicit failure for callers that want to see it (`evaluateOrThrow`, the
|
||||
/// notifications client). The safe `evaluate(...)` path never surfaces this —
|
||||
/// it degrades to cache/defaults instead (FC-060 §4).
|
||||
public enum FeatureControlError: Error, Sendable, Equatable {
|
||||
/// 400 `FEATURE_CONTROL_CONTEXT_INVALID`
|
||||
case invalidContext(code: String)
|
||||
/// 401 `…MISSING_AUTH` / `…INVALID_MODULE_TOKEN`
|
||||
case unauthorized(code: String)
|
||||
/// 403 `…PANEL_ROLE_REQUIRED` / `…INSUFFICIENT_PERMISSIONS`
|
||||
case forbidden(code: String)
|
||||
/// 429 `FEATURE_CONTROL_RATE_LIMIT`
|
||||
case rateLimited(code: String)
|
||||
/// Any other non-2xx status.
|
||||
case server(code: String, status: Int)
|
||||
/// Decoding failure, or any error not raised by `LCEssentials.API`'s HTTP path.
|
||||
case transport(message: String)
|
||||
}
|
||||
|
||||
enum FeatureControlErrorMapper {
|
||||
/// `LCEssentials.API` throws an `NSError` (domain `LCEssentials.DEFAULT_ERROR_DOMAIN`,
|
||||
/// `code` = HTTP status, `localizedFailureReason` = pretty-printed body) for non-2xx
|
||||
/// responses, or a bridged `DecodingError`/`URLError` for anything else. Only the
|
||||
/// former carries a real HTTP status to map.
|
||||
static func map(_ error: Error) -> FeatureControlError {
|
||||
let nsError = error as NSError
|
||||
guard nsError.domain == LCEssentials.DEFAULT_ERROR_DOMAIN else {
|
||||
return .transport(message: nsError.localizedDescription)
|
||||
}
|
||||
|
||||
let status = nsError.code
|
||||
let bodyCode = extractCode(from: nsError.localizedFailureReason) ?? "UNKNOWN"
|
||||
switch status {
|
||||
case 400: return .invalidContext(code: bodyCode)
|
||||
case 401: return .unauthorized(code: bodyCode)
|
||||
case 403: return .forbidden(code: bodyCode)
|
||||
case 429: return .rateLimited(code: bodyCode)
|
||||
default: return .server(code: bodyCode, status: status)
|
||||
}
|
||||
}
|
||||
|
||||
private static func extractCode(from prettyJSON: String?) -> String? {
|
||||
guard let prettyJSON,
|
||||
let data = prettyJSON.data(using: .utf8),
|
||||
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let code = object["code"] as? String else { return nil }
|
||||
return code
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user