Addresses a multi-agent code review (26 confirmed findings + a URLSession leak) across correctness, concurrency, security, and cleanup: - MongoDocument.id: deterministic fallback (was UUID() on every access) to stop SwiftUI List/ForEach identity churn; getAll pagination drives its cursor off the real _id via documentId, so a missing _id stops paging instead of looping on a fabricated cursor. - View models: per-load generation guard (+ captured-tab guard on ONU detail) so a cancelled or overlapping load can't commit stale results or land data on the wrong tab. - JSONValue.intValue: guard Int(d) against NaN/inf/out-of-range (crash). - ServerTrustEvaluator: hash the real SubjectPublicKeyInfo (prepend the algorithm-specific ASN.1 SPKI header) so a standard openssl-captured public-key pin actually matches; unknown key types fail closed. - AppEnvironment.configure: clear stale cookies + Keychain creds on host change. - FirmwareFile.isCompatible: empty metadata no longer passes as compatible-for-all. - APIClient: invalidate URLSession on deinit (per-configure/probe leak). - Dashboard best-effort sections keep last-good on transient failure; unified optical thresholds; LoginView prefill-once; populatedBuckets unique ForEach id; Format rounding rollover; dead-code/docstring/dedup cleanups. Verification pending on the Mac (no Swift toolchain on the Linux dev box). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
4.3 KiB
Swift
120 lines
4.3 KiB
Swift
import Foundation
|
|
|
|
/// The common MCMS response envelope.
|
|
///
|
|
/// Success: `{ "status": "success", "data": { ... } }`
|
|
/// Failure: `{ "status": "fail", "details": { ... } }`
|
|
///
|
|
/// `data` is generic; `details` is free-form so we keep it as `JSONValue`.
|
|
struct APIResponse<T: Decodable>: Decodable {
|
|
let status: String
|
|
let data: T?
|
|
let details: JSONValue?
|
|
|
|
var isSuccess: Bool { status.lowercased() == "success" }
|
|
}
|
|
|
|
/// Write requests wrap their document under a `data` field:
|
|
/// `{ "data": { <document> } }`
|
|
struct DataEnvelope<B: Encodable>: Encodable {
|
|
let data: B
|
|
}
|
|
|
|
// MARK: - JSONValue
|
|
|
|
/// A fully dynamic, `Codable` JSON value. Used to (a) carry the free-form
|
|
/// `details` payload on failures and (b) hold MongoDB documents whose full
|
|
/// schema we haven't typed yet, so screens can display/edit raw config without
|
|
/// every field being modeled up front. Replace with concrete structs as you
|
|
/// pin down shapes from `openapi.json`.
|
|
indirect enum JSONValue: Codable, Equatable {
|
|
case null
|
|
case bool(Bool)
|
|
case int(Int)
|
|
case double(Double)
|
|
case string(String)
|
|
case array([JSONValue])
|
|
case object([String: JSONValue])
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let c = try decoder.singleValueContainer()
|
|
if c.decodeNil() {
|
|
self = .null
|
|
} else if let b = try? c.decode(Bool.self) {
|
|
self = .bool(b)
|
|
} else if let i = try? c.decode(Int.self) {
|
|
self = .int(i)
|
|
} else if let d = try? c.decode(Double.self) {
|
|
self = .double(d)
|
|
} else if let s = try? c.decode(String.self) {
|
|
self = .string(s)
|
|
} else if let a = try? c.decode([JSONValue].self) {
|
|
self = .array(a)
|
|
} else if let o = try? c.decode([String: JSONValue].self) {
|
|
self = .object(o)
|
|
} else {
|
|
throw DecodingError.dataCorruptedError(
|
|
in: c, debugDescription: "Unsupported JSON value")
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var c = encoder.singleValueContainer()
|
|
switch self {
|
|
case .null: try c.encodeNil()
|
|
case .bool(let b): try c.encode(b)
|
|
case .int(let i): try c.encode(i)
|
|
case .double(let d): try c.encode(d)
|
|
case .string(let s): try c.encode(s)
|
|
case .array(let a): try c.encode(a)
|
|
case .object(let o): try c.encode(o)
|
|
}
|
|
}
|
|
|
|
// Convenience accessors
|
|
subscript(_ key: String) -> JSONValue? {
|
|
if case .object(let o) = self { return o[key] }
|
|
return nil
|
|
}
|
|
var stringValue: String? { if case .string(let s) = self { return s }; return nil }
|
|
var intValue: Int? {
|
|
switch self {
|
|
case .int(let i): return i
|
|
// `Int(_:)` traps on NaN/±inf/out-of-range; JSONDecoder can yield those
|
|
// from extreme literals (e.g. `1e400` → .infinity). Guard so a malformed
|
|
// server number becomes nil instead of a crash.
|
|
case .double(let d): return (d.isFinite && d >= Double(Int.min) && d < Double(Int.max)) ? Int(d) : nil
|
|
default: return nil
|
|
}
|
|
}
|
|
var doubleValue: Double? {
|
|
switch self { case .double(let d): return d; case .int(let i): return Double(i); default: return nil }
|
|
}
|
|
var boolValue: Bool? { if case .bool(let b) = self { return b }; return nil }
|
|
var arrayValue: [JSONValue]? { if case .array(let a) = self { return a }; return nil }
|
|
var objectValue: [String: JSONValue]? { if case .object(let o) = self { return o }; return nil }
|
|
|
|
/// A short, human-readable rendering for error surfaces.
|
|
var compactDescription: String {
|
|
switch self {
|
|
case .null: return "null"
|
|
case .bool(let b): return "\(b)"
|
|
case .int(let i): return "\(i)"
|
|
case .double(let d): return "\(d)"
|
|
case .string(let s): return s
|
|
case .array(let a): return a.map(\.compactDescription).joined(separator: ", ")
|
|
case .object(let o):
|
|
return o.map { "\($0): \($1.compactDescription)" }.joined(separator: "; ")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Types that can represent an "empty" value, returned by the client when the
|
|
/// server responds 204 / no body to a request whose return type is non-optional.
|
|
protocol EmptyRepresentable {
|
|
static var emptyValue: Self { get }
|
|
}
|
|
|
|
extension JSONValue: EmptyRepresentable {
|
|
static var emptyValue: JSONValue { .null }
|
|
}
|