megalib 0.11.1

Rust client library for Mega.nz cloud storage
Documentation
use serde_json::Value;

use crate::api::ApiClient;
use crate::error::Result;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RequestClass {
    ReadOnly,
    Mutating,
}

impl RequestClass {
    fn extracts_seqtag(self) -> bool {
        matches!(self, Self::Mutating)
    }
}

#[derive(Debug)]
pub(crate) enum RequestEnvelope {
    Single {
        class: RequestClass,
        payload: Value,
    },
    Batch {
        class: RequestClass,
        payloads: Vec<Value>,
    },
}

impl RequestEnvelope {
    pub(crate) fn read(payload: Value) -> Self {
        Self::Single {
            class: RequestClass::ReadOnly,
            payload,
        }
    }

    pub(crate) fn mutating(payload: Value) -> Self {
        Self::Single {
            class: RequestClass::Mutating,
            payload,
        }
    }

    pub(crate) fn read_batch(payloads: Vec<Value>) -> Self {
        Self::Batch {
            class: RequestClass::ReadOnly,
            payloads,
        }
    }

    pub(crate) fn mutating_batch(payloads: Vec<Value>) -> Self {
        Self::Batch {
            class: RequestClass::Mutating,
            payloads,
        }
    }
}

#[derive(Debug)]
pub(crate) struct RequestOutcome {
    pub(crate) response: Value,
    pub(crate) seqtag: Option<String>,
}

#[derive(Debug, Default)]
pub(crate) struct RequestRuntime;

impl RequestRuntime {
    pub(crate) fn new() -> Self {
        Self
    }

    pub(crate) async fn submit(
        &mut self,
        api: &mut ApiClient,
        envelope: RequestEnvelope,
    ) -> Result<RequestOutcome> {
        match envelope {
            RequestEnvelope::Single { class, payload } => {
                let response = api.request(payload).await?;
                let seqtag = class
                    .extracts_seqtag()
                    .then(|| Self::extract_seqtag(&response))
                    .flatten();
                Ok(RequestOutcome { response, seqtag })
            }
            RequestEnvelope::Batch { class, payloads } => {
                if payloads.is_empty() {
                    return Ok(RequestOutcome {
                        response: Value::Array(vec![]),
                        seqtag: None,
                    });
                }

                let response = api.request_batch(payloads).await?;
                let seqtag = class
                    .extracts_seqtag()
                    .then(|| Self::extract_seqtag(&response))
                    .flatten();
                Ok(RequestOutcome { response, seqtag })
            }
        }
    }

    pub(crate) async fn submit_single(
        &mut self,
        api: &mut ApiClient,
        class: RequestClass,
        payload: Value,
    ) -> Result<RequestOutcome> {
        let envelope = match class {
            RequestClass::ReadOnly => RequestEnvelope::read(payload),
            RequestClass::Mutating => RequestEnvelope::mutating(payload),
        };
        self.submit(api, envelope).await
    }

    pub(crate) async fn submit_batch(
        &mut self,
        api: &mut ApiClient,
        class: RequestClass,
        payloads: Vec<Value>,
    ) -> Result<RequestOutcome> {
        let envelope = match class {
            RequestClass::ReadOnly => RequestEnvelope::read_batch(payloads),
            RequestClass::Mutating => RequestEnvelope::mutating_batch(payloads),
        };
        self.submit(api, envelope).await
    }

    fn extract_seqtag(response: &Value) -> Option<String> {
        if let Some(st) = response.get("st").and_then(|v| v.as_str()) {
            return Some(st.to_string());
        }
        if let Some(arr) = response.as_array()
            && let Some(st) = arr.first().and_then(|v| v.as_str())
        {
            return Some(st.to_string());
        }
        None
    }
}

#[cfg(test)]
mod tests {
    use serde_json::{Value, json};

    use super::{RequestClass, RequestEnvelope, RequestRuntime};
    use crate::api::ApiClient;

    #[test]
    fn extract_seqtag_from_object_response() {
        let response = json!({"st": "abc123"});
        assert_eq!(
            RequestRuntime::extract_seqtag(&response),
            Some("abc123".to_string())
        );
    }

    #[test]
    fn extract_seqtag_from_array_response() {
        let response = json!(["abc123", {"ok": 1}]);
        assert_eq!(
            RequestRuntime::extract_seqtag(&response),
            Some("abc123".to_string())
        );
    }

    #[tokio::test]
    async fn empty_batch_is_normalized() {
        let mut runtime = RequestRuntime::new();
        let mut api = ApiClient::new();

        let outcome = runtime
            .submit_batch(&mut api, RequestClass::Mutating, vec![])
            .await
            .expect("empty batch should not fail");

        assert_eq!(outcome.response, Value::Array(vec![]));
        assert_eq!(outcome.seqtag, None);
    }

    #[test]
    fn envelope_builders_assign_expected_class() {
        match RequestEnvelope::read(json!({"a": "uq"})) {
            RequestEnvelope::Single { class, .. } => assert_eq!(class, RequestClass::ReadOnly),
            _ => panic!("expected single request"),
        }

        match RequestEnvelope::mutating_batch(vec![json!({"a": "p"})]) {
            RequestEnvelope::Batch { class, .. } => assert_eq!(class, RequestClass::Mutating),
            _ => panic!("expected batch request"),
        }
    }
}