mana-swift-event-sync/Sources/ManaEventSync/Core/JSONValue.swift
Till JS 3be550a154 feat: ManaEventSync E-4.0–E-4.2 — geteilte native Event-Sync-Lib
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>
2026-05-25 12:39:59 +02:00

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]
}
}