[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,33 @@
import Foundation
/// Minimal "any JSON" box for a flag's `payload`, which the backend declares as
/// `additionalProperties: true` (arbitrary shape). No force operations an
/// unrecognized shape throws a `DecodingError`, it never crashes.
public indirect enum FeatureControlJSON: Decodable, Sendable, Equatable {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([FeatureControlJSON])
case object([String: FeatureControlJSON])
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Double.self) {
self = .number(value)
} else if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode([FeatureControlJSON].self) {
self = .array(value)
} else if let value = try? container.decode([String: FeatureControlJSON].self) {
self = .object(value)
} else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Unsupported JSON value for FeatureControlJSON")
}
}
}