Files
hyungi_document_server/clients/ds-app/Sources/DSKit/JSONValue.swift
T

72 lines
3.1 KiB
Swift

import Foundation
/// A Sendable, fully-Codable representation of arbitrary JSON, for the contract's open-shape
/// `[String: Any]?` fields (ai_suggestion, md_frontmatter, md_extraction_quality, memo_task_state,
/// source_metadata, freshness_debug, debug). Plain Codable cannot decode those; this can.
///
/// Numeric note (B-1 review): on Foundation's JSONDecoder, a JSON boolean fails `Int` decode
/// (no silent 1/0 coercion), so Bool ordering is harmless. The real disambiguation is Int-before-Double
/// so integral numbers stay `.int`. BUT whole-valued floats like `1.0` can land as `.int` depending on
/// toolchain so numeric meaning MUST be read through `doubleValue`/`intValue` (which cross-convert),
/// never by pattern-matching the raw case.
public enum JSONValue: Codable, Sendable, Hashable {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
case null
public init(from decoder: Decoder) throws {
let c = try decoder.singleValueContainer()
if c.decodeNil() { self = .null; return }
if let b = try? c.decode(Bool.self) { self = .bool(b); return }
if let i = try? c.decode(Int.self) { self = .int(i); return }
if let d = try? c.decode(Double.self) { self = .double(d); return }
if let s = try? c.decode(String.self) { self = .string(s); return }
if let o = try? c.decode([String: JSONValue].self) { self = .object(o); return }
if let a = try? c.decode([JSONValue].self) { self = .array(a); return }
throw DecodingError.dataCorruptedError(in: c, debugDescription: "Unsupported JSON value")
}
public func encode(to encoder: Encoder) throws {
var c = encoder.singleValueContainer()
switch self {
case .string(let s): try c.encode(s)
case .int(let i): try c.encode(i)
case .double(let d): try c.encode(d)
case .bool(let b): try c.encode(b)
case .object(let o): try c.encode(o)
case .array(let a): try c.encode(a)
case .null: try c.encodeNil()
}
}
// Accessors cross-convert so numeric reads are robust regardless of int/double storage.
public var stringValue: String? { if case .string(let s) = self { return s }; return nil }
public var intValue: Int? {
switch self {
case .int(let i): return i
case .double(let d): return Int(d)
default: return nil
}
}
public var doubleValue: Double? {
switch self {
case .double(let d): return d
case .int(let i): return Double(i)
default: return nil
}
}
public var boolValue: Bool? { if case .bool(let b) = self { return b }; return nil }
public var objectValue: [String: JSONValue]? { if case .object(let o) = self { return o }; return nil }
public var arrayValue: [JSONValue]? { if case .array(let a) = self { return a }; return nil }
public subscript(key: String) -> JSONValue? { objectValue?[key] }
public subscript(index: Int) -> JSONValue? {
guard let a = arrayValue, index >= 0, index < a.count else { return nil }
return a[index]
}
}