///|
/// Discord's JSON error body (`{"code": 50008, "message": "...", "errors": {...}}`).
pub(all) struct ApiError {
  code : Int32
  message : String
  errors : Json?
} derive(FromJson, ToJson, Debug)

///|
/// Everything a REST call can raise, from client-side validation to decoded
/// Discord API errors.
///
/// `Transport` and `Timeout` carry the `phase` of the exchange that failed.
/// `Pool` and `Dial` mean no byte of the request was sent, so Discord cannot
/// have acted on it (`is_unsent`); any other phase means it may have.
pub(all) suberror DiscordHttpError {
  /// Discord answered with an error status; `error` is the decoded body.
  Api(status~ : Int32, error~ : ApiError)
  /// Rate limited and retries were exhausted.
  RateLimited(retry_after_ms~ : Int64, global~ : Bool)
  /// The 2xx response body did not decode into the expected model.
  Deserialize(message~ : String)
  /// The exchange failed below HTTP during `phase`: `Pool` when the client
  /// is closed, `Dial` when no connection was made or a proxy refused the
  /// tunnel, `Write`, `Head` or `Body` after connecting, and `Unknown` when
  /// the step cannot be told (a malformed response, a failing rate limiter,
  /// or an error raised by middleware). `message` describes the failure and
  /// never holds the request's URL, headers, or body.
  Transport(phase~ : @ghttp.Phase, message~ : String)
  /// A limit of `timeout_ms` milliseconds ran out during `phase` of one
  /// network attempt (`request_timeout_ms` for the default transport).
  Timeout(phase~ : @ghttp.Phase, timeout_ms~ : Int32)
  /// The request was rejected client-side before any I/O.
  Validation(message~ : String)
} derive(Debug)

///|
/// Whether the error proves that no byte of the request reached Discord, so
/// Discord cannot have acted on it and even a non-idempotent request may be
/// sent again: a `Validation` failure, or a `Transport` failure or `Timeout`
/// in the `Pool` or `Dial` phase. The client already resends such a transport
/// failure up to `max_retries` times before raising it.
///
/// ```mbt check
/// test {
///   assert_true(@http.Transport(phase=Dial, message="refused").is_unsent())
///   assert_true(@http.Timeout(phase=Pool, timeout_ms=100).is_unsent())
///   assert_false(@http.Transport(phase=Write, message="reset").is_unsent())
///   assert_false(@http.Timeout(phase=Head, timeout_ms=100).is_unsent())
/// }
/// ```
pub fn DiscordHttpError::is_unsent(self : DiscordHttpError) -> Bool {
  match self {
    Validation(_)
    | Transport(phase=Pool | Dial, ..)
    | Timeout(phase=Pool | Dial, ..) => true
    Transport(..) | Timeout(..) | Api(..) | RateLimited(..) | Deserialize(_) =>
      false
  }
}