Extrahiert die zuvor zweimal von Hand gebaute Plumbing aus pageta-native + nutriphi-native in ein Paket ManaEventSync (Counterpart zu @mana/event-sync): - Core: EventEnvelope (+clientId/payloadHash), JSONValue, ULID, JWTSubject, EventSyncError - EventStorage (SwiftData): parametrierbar (Store/App-Group/additionalModels/In-Memory), Outbox, Claim-Reattribute, Anonymous-Retention-Prune - Transport: SyncHTTPClient (+X-Schema-Hash/422), optionaler SyncWSClient - Crypto: CryptoProvider/NoOp/MasterKey (AES-GCM enc:1:), VaultClient — opt-in - EventSyncEngine: besitzt die Identität (makeEnvelope anon/echt), emit offline, signIn(realUserId) re-taggt idempotent + drained, Retention 10k/90d 18 Tests grün. Plan: mana/docs/playbooks/MANA_SWIFT_EVENT_SYNC.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.7 KiB
Swift
72 lines
2.7 KiB
Swift
import Foundation
|
|
|
|
/// Rekursiver JSON-Wert. Payload-Typ für ``EventEnvelope``, da die
|
|
/// Payload-Shape pro `eventType` variiert.
|
|
///
|
|
/// Roundtrip-Helper ``from(_:)`` und ``decode(_:)`` konvertieren
|
|
/// zwischen typed-Codable-Payloads (z.B. `ArticleSavedPayload` in der
|
|
/// App) und `JSONValue` via JSON-Serialization.
|
|
public enum JSONValue: Codable, Hashable, Sendable {
|
|
case null
|
|
case bool(Bool)
|
|
case int(Int64)
|
|
case double(Double)
|
|
case string(String)
|
|
case array([JSONValue])
|
|
case object([String: JSONValue])
|
|
|
|
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(Int64.self) {
|
|
self = .int(value)
|
|
} else if let value = try? container.decode(Double.self) {
|
|
self = .double(value)
|
|
} else if let value = try? container.decode(String.self) {
|
|
self = .string(value)
|
|
} else if let value = try? container.decode([JSONValue].self) {
|
|
self = .array(value)
|
|
} else if let value = try? container.decode([String: JSONValue].self) {
|
|
self = .object(value)
|
|
} else {
|
|
throw DecodingError.dataCorruptedError(
|
|
in: container,
|
|
debugDescription: "Unrecognized JSON value"
|
|
)
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
switch self {
|
|
case .null: try container.encodeNil()
|
|
case let .bool(value): try container.encode(value)
|
|
case let .int(value): try container.encode(value)
|
|
case let .double(value): try container.encode(value)
|
|
case let .string(value): try container.encode(value)
|
|
case let .array(value): try container.encode(value)
|
|
case let .object(value): try container.encode(value)
|
|
}
|
|
}
|
|
|
|
/// Baut einen `JSONValue` aus einem typed Encodable.
|
|
public static func from(_ encodable: some Encodable) throws -> JSONValue {
|
|
let data = try JSONEncoder().encode(encodable)
|
|
return try JSONDecoder().decode(JSONValue.self, from: data)
|
|
}
|
|
|
|
/// Dekodiert diesen `JSONValue` in einen typed Decodable.
|
|
public func decode<T: Decodable>(_: T.Type = T.self) throws -> T {
|
|
let data = try JSONEncoder().encode(self)
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
/// Liest ein Feld, wenn dieser Wert ein Objekt ist.
|
|
public func get(_ key: String) -> JSONValue? {
|
|
guard case let .object(object) = self else { return nil }
|
|
return object[key]
|
|
}
|
|
}
|