// Copyright © 2024 Ryan Booker. All rights reserved. import Foundation import Toolbox public struct Response<T>: CustomStringConvertible, Decodable where T: CustomStringConvertible & Decodable { public let meta: Metadata public let data: Result<T, Failure> public var description: String { data.reduce(success: \.description, failure: \.description) } public init(meta: Metadata, data: Result<T, Failure>) { self.meta = meta self.data = data } public enum CodingKeys: CodingKey { case meta, data, error } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.meta = try container.decode(Metadata.self, forKey: .meta) do { let data = try container.decode(T.self, forKey: .data) self.data = .success(data) } catch { let reasons = try container.decode([Failure.Reason].self, forKey: .error) self.data = .failure(.init(reasons: reasons)) } } } // MARK: - Repsonse types extension Response { public struct Failure: CustomStringConvertible, Decodable, Error { public struct Reason: CustomStringConvertible, Decodable { public let code: Int public let msg: String public var description: String { "\(code): \(msg)" } } public let reasons: [Reason] public var description: String { """ Whoops! \(reasons.map(\.description).joined(separator: "\n\n")) """ } } public struct Metadata: Decodable { public let id: String public let node: String public let ms: Int } }