wacore-binary 0.5.0

Binary data and constants for WhatsApp protocol
Documentation
use std::borrow::Cow;
use std::str::FromStr;

use crate::error::{BinaryError, Result};
use crate::jid::Jid;
use crate::node::{Attrs, Node, NodeRef, NodeValue, ValueRef};

pub struct AttrParser<'a> {
    pub attrs: &'a Attrs,
    pub errors: Vec<BinaryError>,
}

pub struct AttrParserRef<'a> {
    pub attrs: &'a [(Cow<'a, str>, ValueRef<'a>)],
    pub errors: Vec<BinaryError>,
}

impl<'a> AttrParserRef<'a> {
    pub fn new(node: &'a NodeRef<'a>) -> Self {
        Self {
            attrs: node.attrs.as_slice(),
            errors: Vec::new(),
        }
    }

    pub fn ok(&self) -> bool {
        self.errors.is_empty()
    }

    pub fn finish(&self) -> Result<()> {
        if self.ok() {
            Ok(())
        } else {
            Err(BinaryError::AttrList(self.errors.clone()))
        }
    }

    fn get_raw(&mut self, key: &str, require: bool) -> Option<&'a ValueRef<'a>> {
        let val = self
            .attrs
            .iter()
            .find(|(k, _)| k.as_ref() == key)
            .map(|(_, v)| v);

        if require && val.is_none() {
            self.errors.push(BinaryError::AttrParse(format!(
                "Required attribute '{key}' not found"
            )));
        }

        val
    }

    /// Get string from the value. Works for both String and JID variants.
    /// - String variant: Cow::Borrowed — zero copy
    /// - JID variant: Cow::Owned — allocates only when needed
    pub fn optional_string(&mut self, key: &str) -> Option<Cow<'a, str>> {
        self.get_raw(key, false).map(|v| v.to_string_cow())
    }

    /// Get a required string attribute, returning an error if missing.
    ///
    /// Prefer this over `string()` for required attributes as it makes
    /// the error explicit rather than silently defaulting to empty string.
    pub fn required_string(&mut self, key: &str) -> Result<Cow<'a, str>> {
        self.optional_string(key)
            .ok_or_else(|| BinaryError::MissingAttr(key.to_string()))
    }

    /// Get JID from the value.
    /// If the value is a JidRef, returns it directly without parsing (zero allocation).
    /// If the value is a string, parses it as a JID.
    pub fn optional_jid(&mut self, key: &str) -> Option<Jid> {
        self.get_raw(key, false).and_then(|v| match v.to_jid() {
            Some(jid) => Some(jid),
            None => {
                // to_jid() only returns None if it's a String that failed to parse
                if let ValueRef::String(s) = v {
                    self.errors
                        .push(BinaryError::AttrParse(format!("Invalid JID: {s}")));
                }
                None
            }
        })
    }

    pub fn jid(&mut self, key: &str) -> Jid {
        self.get_raw(key, true);
        self.optional_jid(key).unwrap_or_default()
    }

    pub fn non_ad_jid(&mut self, key: &str) -> Jid {
        self.jid(key).to_non_ad()
    }

    fn get_string_value(&mut self, key: &str, require: bool) -> Option<Cow<'a, str>> {
        self.get_raw(key, require).map(|v| v.to_string_cow())
    }

    fn get_bool(&mut self, key: &str, require: bool) -> Option<bool> {
        self.get_string_value(key, require)
            .and_then(|s| match s.parse::<bool>() {
                Ok(val) => Some(val),
                Err(e) => {
                    self.errors.push(BinaryError::AttrParse(format!(
                        "Failed to parse bool from '{s}' for key '{key}': {e}"
                    )));
                    None
                }
            })
    }

    pub fn optional_bool(&mut self, key: &str) -> bool {
        self.get_bool(key, false).unwrap_or(false)
    }

    pub fn bool(&mut self, key: &str) -> bool {
        self.get_bool(key, true).unwrap_or(false)
    }

    pub fn optional_u64(&mut self, key: &str) -> Option<u64> {
        self.get_string_value(key, false)
            .and_then(|s| match s.parse::<u64>() {
                Ok(val) => Some(val),
                Err(e) => {
                    self.errors.push(BinaryError::AttrParse(format!(
                        "Failed to parse u64 from '{s}' for key '{key}': {e}"
                    )));
                    None
                }
            })
    }

    pub fn unix_time(&mut self, key: &str) -> i64 {
        self.get_raw(key, true);
        self.optional_unix_time(key).unwrap_or_default()
    }

    pub fn optional_unix_time(&mut self, key: &str) -> Option<i64> {
        self.get_i64(key, false)
    }

    pub fn unix_milli(&mut self, key: &str) -> i64 {
        self.get_raw(key, true);
        self.optional_unix_milli(key).unwrap_or_default()
    }

    pub fn optional_unix_milli(&mut self, key: &str) -> Option<i64> {
        self.get_i64(key, false)
    }

    fn get_i64(&mut self, key: &str, require: bool) -> Option<i64> {
        self.get_string_value(key, require)
            .and_then(|s| match s.parse::<i64>() {
                Ok(val) => Some(val),
                Err(e) => {
                    self.errors.push(BinaryError::AttrParse(format!(
                        "Failed to parse i64 from '{s}' for key '{key}': {e}"
                    )));
                    None
                }
            })
    }
}

impl<'a> AttrParser<'a> {
    pub fn new(node: &'a Node) -> Self {
        Self {
            attrs: &node.attrs,
            errors: Vec::new(),
        }
    }

    pub fn ok(&self) -> bool {
        self.errors.is_empty()
    }

    pub fn finish(&self) -> Result<()> {
        if self.ok() {
            Ok(())
        } else {
            Err(BinaryError::AttrList(self.errors.clone()))
        }
    }

    fn get_raw(&mut self, key: &str, require: bool) -> Option<&'a NodeValue> {
        let val = self.attrs.get(key);
        if require && val.is_none() {
            self.errors.push(BinaryError::AttrParse(format!(
                "Required attribute '{key}' not found"
            )));
        }
        val
    }

    /// Get the string representation of the value (for numeric parsing, etc.)
    fn get_string_value(&mut self, key: &str, require: bool) -> Option<Cow<'a, str>> {
        self.get_raw(key, require).map(|v| match v {
            NodeValue::String(s) => Cow::Borrowed(s.as_str()),
            NodeValue::Jid(j) => Cow::Owned(j.to_string()),
        })
    }

    // --- String ---
    /// Get string from the value. Works for both String and JID variants.
    /// - String variant: Cow::Borrowed — zero copy
    /// - JID variant: Cow::Owned — allocates only when needed
    pub fn optional_string(&mut self, key: &str) -> Option<Cow<'a, str>> {
        self.get_raw(key, false).map(|v| v.as_str())
    }

    /// Get a required string attribute, returning an error if missing.
    ///
    /// Prefer this over `string()` for required attributes as it makes
    /// the error explicit rather than silently defaulting to empty string.
    pub fn required_string(&mut self, key: &str) -> Result<Cow<'a, str>> {
        self.optional_string(key)
            .ok_or_else(|| BinaryError::MissingAttr(key.to_string()))
    }

    // --- JID ---
    /// Get JID from the value.
    /// If the value is a JID variant, returns it directly without parsing (zero allocation clone).
    /// If the value is a string, parses it as a JID.
    pub fn optional_jid(&mut self, key: &str) -> Option<Jid> {
        self.get_raw(key, false).and_then(|v| match v {
            NodeValue::Jid(j) => Some(j.clone()),
            NodeValue::String(s) => match Jid::from_str(s) {
                Ok(jid) => Some(jid),
                Err(e) => {
                    self.errors.push(BinaryError::from(e));
                    None
                }
            },
        })
    }

    pub fn jid(&mut self, key: &str) -> Jid {
        self.get_raw(key, true); // Push "not found" error if needed.
        self.optional_jid(key).unwrap_or_default()
    }

    pub fn non_ad_jid(&mut self, key: &str) -> Jid {
        self.jid(key).to_non_ad()
    }

    // --- Boolean ---
    fn get_bool(&mut self, key: &str, require: bool) -> Option<bool> {
        self.get_string_value(key, require)
            .and_then(|s| match s.parse::<bool>() {
                Ok(val) => Some(val),
                Err(e) => {
                    self.errors.push(BinaryError::AttrParse(format!(
                        "Failed to parse bool from '{s}' for key '{key}': {e}"
                    )));
                    None
                }
            })
    }

    pub fn optional_bool(&mut self, key: &str) -> bool {
        self.get_bool(key, false).unwrap_or(false)
    }

    pub fn bool(&mut self, key: &str) -> bool {
        self.get_bool(key, true).unwrap_or(false)
    }

    // --- u64 ---
    pub fn optional_u64(&mut self, key: &str) -> Option<u64> {
        self.get_string_value(key, false)
            .and_then(|s| match s.parse::<u64>() {
                Ok(val) => Some(val),
                Err(e) => {
                    self.errors.push(BinaryError::AttrParse(format!(
                        "Failed to parse u64 from '{s}' for key '{key}': {e}"
                    )));
                    None
                }
            })
    }

    pub fn unix_time(&mut self, key: &str) -> i64 {
        self.get_raw(key, true);
        self.optional_unix_time(key).unwrap_or_default()
    }

    pub fn optional_unix_time(&mut self, key: &str) -> Option<i64> {
        self.get_i64(key, false)
    }

    pub fn unix_milli(&mut self, key: &str) -> i64 {
        self.get_raw(key, true);
        self.optional_unix_milli(key).unwrap_or_default()
    }

    pub fn optional_unix_milli(&mut self, key: &str) -> Option<i64> {
        self.get_i64(key, false)
    }

    fn get_i64(&mut self, key: &str, require: bool) -> Option<i64> {
        self.get_string_value(key, require)
            .and_then(|s| match s.parse::<i64>() {
                Ok(val) => Some(val),
                Err(e) => {
                    self.errors.push(BinaryError::AttrParse(format!(
                        "Failed to parse i64 from '{s}' for key '{key}': {e}"
                    )));
                    None
                }
            })
    }
}