matroskin 0.1.0

whatsminer api client lib
Documentation
//! API response types: [ResponseCode] enum + [Response]<`T`> struct.
//!
//! [ResponseCode] maps wire-protocol integer codes (0, −1, −2, −3, −4, −5) to typed variants.
//! [Response]<`T`> wraps the full JSON payload: code, timestamp, message, desc.

use std::borrow::Cow;

use serde::Deserialize;
use serde::de::DeserializeOwned;

use crate::error::{Error, Result};

pub mod response_code;

#[cfg(doc)]
use crate::command::Command;

pub use response_code::{ResponseCode, UnknownResponseCode};

/// Borrowed raw JSON envelope for one-pass parsing.
/// All fields are required by the wire specification.
#[derive(Debug, Deserialize)]
pub(crate) struct Envelope<'a> {
    pub code: ResponseCode,
    pub when: u64,
    #[serde(borrow)]
    pub msg: &'a serde_json::value::RawValue,
    #[serde(borrow)]
    pub desc: Cow<'a, str>,
}

/// Represents a response from the WhatsMiner API.
///
/// Generic over `T` — the typed payload in the `msg` field.
/// Each [Command] defines its own `T` via [Command::Response].
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Response<T> {
    /// Status code from the device.
    ///
    /// See [ResponseCode] for variant meanings.
    pub code: ResponseCode,
    /// UNIX timestamp of the response.
    pub when: u64,
    /// Response payload, deserialized into the command's expected type.
    pub msg: T,
    /// Command name this response corresponds to.
    pub desc: String,
}

/// Parse complete response envelope once with borrowed RawValue msg,
/// then only the msg into `T`.
///
/// Returns [Response]<`T`> if code is [ResponseCode::Success],
/// or [Error::Device] with retained string/object msg if code indicates an error.
pub fn parse_response<T: DeserializeOwned>(raw: &[u8]) -> Result<Response<T>> {
    let text = core::str::from_utf8(raw).map_err(|_| Error::InvalidUtf8)?;
    let envelope: Envelope<'_> = serde_json::from_str(text)?;
    if envelope.code.is_success() {
        let msg: T = serde_json::from_str(envelope.msg.get())?;
        Ok(Response {
            code: envelope.code,
            when: envelope.when,
            msg,
            desc: envelope.desc.into_owned(),
        })
    } else {
        let msg_str = match serde_json::from_str::<Cow<'_, str>>(envelope.msg.get()) {
            Ok(s) => s.into_owned(),
            Err(_) => envelope.msg.get().trim().to_string(),
        };
        Err(Error::Device {
            code: envelope.code,
            msg: msg_str,
            desc: envelope.desc.into_owned(),
        })
    }
}

/// Helper to deserialize a boolean from strings like "enable", "disable", "true", "false", "1", "0".
pub fn deserialize_bool_from_string<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    match s.to_lowercase().as_str() {
        "enable" | "true" | "1" => Ok(true),
        "disable" | "false" | "0" => Ok(false),
        _ => Err(serde::de::Error::custom(format!(
            "invalid boolean string: {}",
            s
        ))),
    }
}

/// Helper to deserialize an optional boolean from strings like "enable", "disable".
pub fn deserialize_opt_bool_from_string<'de, D>(
    deserializer: D,
) -> std::result::Result<Option<bool>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let opt = Option::<String>::deserialize(deserializer)?;
    match opt {
        Some(s) => match s.to_lowercase().as_str() {
            "enable" | "true" | "1" => Ok(Some(true)),
            "disable" | "false" | "0" => Ok(Some(false)),
            _ => Err(serde::de::Error::custom(format!(
                "invalid boolean string: {}",
                s
            ))),
        },
        None => Ok(None),
    }
}

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

    #[derive(Debug, PartialEq, Eq, Deserialize)]
    struct DummyPayload {
        key: String,
        count: u32,
    }

    #[test]
    fn parse_response_success_typed() {
        let raw =
            br#"{"code":0,"when":1692685443,"msg":{"key":"abc","count":42},"desc":"test.cmd"}"#;
        let r: Response<DummyPayload> = parse_response(raw).unwrap();
        assert_eq!(r.code, ResponseCode::Success);
        assert_eq!(r.when, 1692685443);
        assert_eq!(
            r.msg,
            DummyPayload {
                key: "abc".to_string(),
                count: 42
            }
        );
        assert_eq!(r.desc, "test.cmd");
    }

    #[test]
    fn parse_response_success_string() {
        let raw = br#"{"code":0,"when":123,"msg":"ok","desc":"test"}"#;
        let r: Response<String> = parse_response(raw).unwrap();
        assert_eq!(r.code, ResponseCode::Success);
        assert_eq!(r.msg, "ok");
    }

    #[test]
    fn parse_response_error_string() {
        let raw = br#"{"code":-1,"when":123,"msg":"some error","desc":"test"}"#;
        let err = parse_response::<String>(raw).unwrap_err();
        match err {
            Error::Device { code, msg, desc } => {
                assert_eq!(code, ResponseCode::Fail);
                assert_eq!(msg, "some error");
                assert_eq!(desc, "test");
            }
            other => panic!("unexpected error: {other:?}"),
        }
    }

    #[test]
    fn parse_response_error_object() {
        let raw = br#"{"code":-2,"when":123,"msg":{"detail":"invalid field"},"desc":"test"}"#;
        let err = parse_response::<String>(raw).unwrap_err();
        match err {
            Error::Device { code, msg, desc } => {
                assert_eq!(code, ResponseCode::InvalidCommand);
                assert_eq!(msg, r#"{"detail":"invalid field"}"#);
                assert_eq!(desc, "test");
            }
            other => panic!("unexpected error: {other:?}"),
        }
    }

    #[test]
    fn parse_response_out_of_order() {
        let raw = br#"{"desc":"test","msg":"ok","when":999,"code":0}"#;
        let r: Response<String> = parse_response(raw).unwrap();
        assert_eq!(r.code, ResponseCode::Success);
        assert_eq!(r.when, 999);
        assert_eq!(r.msg, "ok");
        assert_eq!(r.desc, "test");
    }

    #[test]
    fn parse_response_missing_field_fails() {
        let raw = br#"{"code":0,"msg":"ok","desc":"test"}"#; // missing when
        let err = parse_response::<String>(raw).unwrap_err();
        assert!(matches!(err, Error::Json(_)));
    }

    #[test]
    fn parse_response_invalid_code_fails() {
        let raw = br#"{"code":99,"when":1,"msg":"ok","desc":"test"}"#;
        let err = parse_response::<String>(raw).unwrap_err();
        assert!(matches!(err, Error::Json(_)));
    }

    #[test]
    fn parse_response_malformed_typed_payload_fails() {
        let raw = br#"{"code":0,"when":1,"msg":"not an object","desc":"test"}"#;
        let err = parse_response::<DummyPayload>(raw).unwrap_err();
        assert!(matches!(err, Error::Json(_)));
    }

    #[test]
    fn parse_response_invalid_utf8_fails() {
        let raw = &[0xFF, 0xFE, 0xFD];
        let err = parse_response::<String>(raw).unwrap_err();
        assert!(matches!(err, Error::InvalidUtf8));
    }
}