import Foundation
/// The decode table shared by every request:
///
/// | status | body | result |
/// |---------|-------------------------------|-----------------------------------------------|
/// | 2xx | empty | `{}` |
/// | 2xx | JSON | the value |
/// | 2xx | not JSON | `.transport("Failed to parse JSON response")` |
/// | non-2xx | empty | `.api(status, nil, "Empty error response")` |
/// | non-2xx | `{error:{code,message}}` | `.api(status, code, message, body)` |
/// | non-2xx | `{error:{reason,details}}` | `.api(status, reason, details ?? reason)` |
/// | non-2xx | `{error:"code"}` | `.api(status, code, code, body)` |
/// | non-2xx | not JSON | `.api(status, nil, text, nil)` |
enum ResponseDecoder {
static func decode(status: Int, body: Data) throws -> (data: Data, json: JSONValue) {
let ok = (200..<300).contains(status)
let text = String(decoding: body, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
if text.isEmpty {
if ok { return (Data("{}".utf8), .object([:])) }
throw IrdError.api(status: status, code: nil, message: "Empty error response", body: nil)
}
let parsed = try? JSONValue.parse(body)
if ok {
guard let parsed else { throw IrdError.transport("Failed to parse JSON response") }
return (body, parsed)
}
guard let parsed, case .object = parsed else {
throw IrdError.api(status: status, code: nil, message: text, body: nil)
}
let (code, message) = errorEnvelope(parsed, fallback: text)
throw IrdError.api(status: status, code: code, message: message, body: parsed)
}
static func errorEnvelope(_ body: JSONValue, fallback: String) -> (code: String?, message: String) {
switch body["error"] {
case .string(let code)?:
return (code, code)
case .object(let e)?:
let code = e["code"]?.stringValue ?? e["reason"]?.stringValue
let message = e["message"]?.stringValue ?? e["details"]?.stringValue ?? code ?? fallback
return (code, message)
default:
return (nil, fallback)
}
}
}