[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:
Daniel Arantes Loverde
2026-09-16 09:23:57 -03:00
parent d067791930
commit 2ba487a6e8
47 changed files with 4264 additions and 113 deletions

View File

@@ -0,0 +1,65 @@
import Foundation
/// `GET /api/feature-control/notifications` `status` query filter.
public enum FeatureControlNotificationStatus: String, Sendable {
case all, unread
}
/// One item from `NotificationListItem`.
public struct FeatureControlNotification: Sendable, Equatable, Identifiable {
public let id: String
public let title: String
public let body: String
public let severity: String
public let createdAt: Date
public let read: Bool
public let ctaLabel: String?
public let ctaUrl: String?
public init(id: String, title: String, body: String, severity: String,
createdAt: Date, read: Bool, ctaLabel: String?, ctaUrl: String?) {
self.id = id
self.title = title
self.body = body
self.severity = severity
self.createdAt = createdAt
self.read = read
self.ctaLabel = ctaLabel
self.ctaUrl = ctaUrl
}
}
extension FeatureControlNotification: Decodable {
private enum CodingKeys: String, CodingKey {
case id, title, body, severity, createdAt, read, ctaLabel, ctaUrl
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let rawDate = try container.decode(String.self, forKey: .createdAt)
guard let date = FeatureControlDateParsing.parse(rawDate) else {
throw DecodingError.dataCorruptedError(forKey: .createdAt, in: container,
debugDescription: "Unrecognized date format: \(rawDate)")
}
self.id = try container.decode(String.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.body = try container.decode(String.self, forKey: .body)
self.severity = try container.decode(String.self, forKey: .severity)
self.createdAt = date
self.read = try container.decode(Bool.self, forKey: .read)
self.ctaLabel = try container.decodeIfPresent(String.self, forKey: .ctaLabel)
self.ctaUrl = try container.decodeIfPresent(String.self, forKey: .ctaUrl)
}
}
/// `GET /api/feature-control/notifications` `result` shape.
struct FeatureControlNotificationListResult: Decodable, Sendable {
let items: [FeatureControlNotification]
let nextCursor: String?
}
/// Wire envelope for the notifications list `{error, result}`.
struct FeatureControlNotificationListEnvelope: Decodable, Sendable {
let error: Bool
let result: FeatureControlNotificationListResult
}