matroskin 0.1.0

whatsminer api client lib
Documentation
//! API response status codes returned by WhatsMiner devices.
//!
//! Maps integer codes from the wire protocol to typed variants.
//! Unknown codes produce a deserialization error.
//!
//! | Code | Variant | Meaning |
//! |------|---------|---------|
//! | 0 | [ResponseCode::Success] | Request succeeded |
//! | -1 | [ResponseCode::Fail] | Generic failure |
//! | -2 | [ResponseCode::InvalidCommand] | Invalid command |
//! | -3 | [ResponseCode::InvalidJsonItem] | Invalid JSON item |
//! | -4 | [ResponseCode::NoPermission] | No permission |
//! | -5 | [ResponseCode::OutOfMemory] | Out of memory |
//!
//! Ref: <https://apidoc.whatsminer.com/#api-Overview>

use std::fmt;

use serde::de::{self, Visitor};

/// API response status codes returned by WhatsMiner devices.
///
/// Maps integer codes from the wire protocol to typed variants.
/// Unknown codes produce a deserialization error.
///
/// Ref: <https://apidoc.whatsminer.com/#api-Overview>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i8)]
pub enum ResponseCode {
    /// `0` — Request succeeded.
    Success = 0,
    /// `-1` — Generic failure (e.g. wrong password, invalid JSON).
    Fail = -1,
    /// `-2` — Invalid command or incorrect parameters.
    InvalidCommand = -2,
    /// `-3` — Invalid or null JSON item in request.
    InvalidJsonItem = -3,
    /// `-4` — Insufficient permissions (write access disabled or wrong account).
    NoPermission = -4,
    /// `-5` — Device out of memory.
    OutOfMemory = -5,
}

impl ResponseCode {
    /// Returns `true` for [ResponseCode::Success].
    pub const fn is_success(self) -> bool {
        matches!(self, Self::Success)
    }
}

impl fmt::Display for ResponseCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let (code, desc) = match self {
            Self::Success => (0, "Success"),
            Self::Fail => (-1, "Fail"),
            Self::InvalidCommand => (-2, "Invalid command"),
            Self::InvalidJsonItem => (-3, "Invalid JSON item"),
            Self::NoPermission => (-4, "No permission"),
            Self::OutOfMemory => (-5, "Out of memory"),
        };
        write!(f, "{code} ({desc})")
    }
}

impl TryFrom<i8> for ResponseCode {
    type Error = UnknownResponseCode;

    fn try_from(value: i8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Self::Success),
            -1 => Ok(Self::Fail),
            -2 => Ok(Self::InvalidCommand),
            -3 => Ok(Self::InvalidJsonItem),
            -4 => Ok(Self::NoPermission),
            -5 => Ok(Self::OutOfMemory),
            other => Err(UnknownResponseCode(other)),
        }
    }
}

/// Error returned when the device sends a response code not in the known set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownResponseCode(pub i8);

impl fmt::Display for UnknownResponseCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "unknown response code: {}", self.0)
    }
}

impl std::error::Error for UnknownResponseCode {}

// --- Serde impl ---

#[derive(Debug)]
struct ResponseCodeVisitor;

/// Convert arbitrary integer → `i8` → [`ResponseCode`].
/// Shared by `visit_i64` and `visit_u64`.
fn code_from_int<E: de::Error>(v: impl TryInto<i8>) -> Result<ResponseCode, E> {
    let code: i8 = v
        .try_into()
        .map_err(|_| E::custom("response code out of i8 range"))?;
    ResponseCode::try_from(code).map_err(|e| E::custom(e.to_string()))
}

impl Visitor<'_> for ResponseCodeVisitor {
    type Value = ResponseCode;

    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("a known WhatsMiner response code (0, -1, -2, -3, -4, -5)")
    }

    fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
        code_from_int(v)
    }

    fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
        code_from_int(v)
    }
}

impl<'de> serde::Deserialize<'de> for ResponseCode {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        deserializer.deserialize_any(ResponseCodeVisitor)
    }
}

// No Serialize — we never send response codes, only receive them.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deserialize_success_code() {
        let json = r#"0"#;
        let code: ResponseCode = serde_json::from_str(json).unwrap();
        assert_eq!(code, ResponseCode::Success);
        assert!(code.is_success());
    }

    #[test]
    fn deserialize_fail_codes() {
        for (raw, expected) in [
            (-1i8, ResponseCode::Fail),
            (-2, ResponseCode::InvalidCommand),
            (-3, ResponseCode::InvalidJsonItem),
            (-4, ResponseCode::NoPermission),
            (-5, ResponseCode::OutOfMemory),
        ] {
            let json = raw.to_string();
            let code: ResponseCode = serde_json::from_str(&json).unwrap();
            assert_eq!(code, expected, "mismatch for code {raw}");
            assert!(!code.is_success());
        }
    }

    #[test]
    fn deserialize_unknown_code_returns_error() {
        let json = r#"42"#;
        let result = serde_json::from_str::<ResponseCode>(json);
        assert!(result.is_err(), "expected error for unknown code 42");
    }

    #[test]
    fn try_from_roundtrip() {
        assert_eq!(ResponseCode::try_from(0).unwrap(), ResponseCode::Success);
        assert_eq!(
            ResponseCode::try_from(-4).unwrap(),
            ResponseCode::NoPermission
        );
        assert!(ResponseCode::try_from(99).is_err());
    }

    #[test]
    fn display() {
        assert_eq!(ResponseCode::Success.to_string(), "0 (Success)");
        assert_eq!(ResponseCode::NoPermission.to_string(), "-4 (No permission)");
    }
}