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: 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": { } }` struct DataEnvelope: 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 } }