matroskin 0.1.0

whatsminer api client lib
Documentation
//! Implement `get.log.download` command
//!
//! This command requests the miner to package its logs into a `.tgz` archive
//! and stream them back in 10 KB chunks.
//!
//! The initial JSON response contains the total log size; the actual log data
//! is streamed as raw binary chunks afterward, which is outside the scope of
//! the [`Command`](crate::command::Command) trait and is handled at the actor/codec level.
//!
//! - Command: [GetLogDownload]
//! - ApiDoc: <https://apidoc.whatsminer.com/#api-Log-get_log_download>
use core::str;

use serde::Deserialize;

use crate::{error::Result, permission::PermissionLevel, request::Request};

/// Internal command representing the `get.log.download` protocol operation.
///
/// Requests the miner to package logs into a `.tgz` archive. Callers interact
/// with log streaming through [`Actor::download_log`](crate::actor::Actor::download_log)
/// rather than sending this internal command directly.
///
/// - ApiDoc: <https://apidoc.whatsminer.com/#api-Log-get_log_download>
///
/// # Example
/// ```rust,no_run
/// use matroskin::actor::Actor;
/// use matroskin::account::Account;
/// use matroskin::password::Password;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let actor = Actor::new("10.10.10.10:4433", Account::Super, Password::Super);
///     actor.connect()?;
///
///     let file = tokio::fs::File::create("miner.log").await?;
///     actor.download_log(file).await?;
///
///     actor.shutdown()?;
///     Ok(())
/// }
/// ```
#[derive(Debug, Default, Clone, Copy)]
pub struct GetLogDownload;

impl GetLogDownload {
    pub const CMD_NAME: &'static str = "get.log.download";
    pub const MINIMAL_PERMISSIONS: PermissionLevel = PermissionLevel::ReadOnly;

    pub fn encode_request() -> Result<Vec<u8>> {
        let req = Request::new(Self::CMD_NAME, None, None::<()>);
        Ok(serde_json::to_vec(&req)?)
    }
}
/// Deserializes a string-encoded `u64` value.
///
/// The WhatsMiner API returns `logsize` as a JSON string (e.g. `"12345"`)
/// rather than a number. This helper parses that string into a `u64`.
fn deserialize_u64_from_string<'de, D>(deserializer: D) -> std::result::Result<u64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::Deserialize;
    let s = String::deserialize(deserializer)?;
    s.parse::<u64>().map_err(serde::de::Error::custom)
}

/// [GetLogDownload] Response
///
/// Contains the size (in bytes) of the log archive that will be streamed.
///
/// **Note:** This is only the initial response. The actual `.tgz` log data
/// is streamed as raw binary chunks after this response, which is outside
/// the scope of the [`Command`](crate::command::Command) trait and is handled at the actor/codec level.
#[derive(Debug, Default, Deserialize)]
pub struct GetLogDownloadResponse {
    /// Size of the log archive in bytes.
    ///
    /// The API returns this as a string (e.g. `"12345"`), which is
    /// deserialized into a `u64`.
    #[serde(deserialize_with = "deserialize_u64_from_string")]
    pub logsize: u64,
}

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

    const RESPONSE_JSON: &str =
        r#"{"code":0,"when":1672281726,"msg":{"logsize":"12345"},"desc":"get.log.download"}"#;

    #[test]
    fn request_no_params_no_auth() {
        let bytes = GetLogDownload::encode_request().unwrap();
        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
        assert_eq!(json["cmd"], "get.log.download");
        assert!(json.get("param").is_none());
        assert!(json.get("token").is_none());
    }

    #[test]
    fn deserialize_response_fixture() {
        let response: crate::response::Response<GetLogDownloadResponse> =
            serde_json::from_str(RESPONSE_JSON).unwrap();
        assert!(response.code.is_success());
        assert_eq!(response.desc, "get.log.download");
        assert_eq!(response.when, 1672281726);
        assert_eq!(response.msg.logsize, 12345);
    }

    #[test]
    fn deserialize_u64_from_string_helper() {
        #[derive(Deserialize)]
        struct Wrapper {
            #[serde(deserialize_with = "deserialize_u64_from_string")]
            value: u64,
        }
        let json = r#"{"value":"9876543210"}"#;
        let w: Wrapper = serde_json::from_str(json).unwrap();
        assert_eq!(w.value, 9876543210);
    }
}