Skip to main content

native/guppy_nif/src/etf_decode.rs

use eetf::{Atom, Binary, ByteList, List, Map, Term};
use std::collections::HashMap;
use std::io::Cursor;

pub(crate) fn decode_term(bytes: &[u8]) -> Result<Term, String> {
    Term::decode(Cursor::new(bytes)).map_err(|error| error.to_string())
}

pub(crate) fn expect_hash_map<'a>(
    term: &'a Term,
    description: &str,
) -> Result<&'a HashMap<Term, Term>, String> {
    match term {
        Term::Map(Map { map }) => Ok(map),
        other => Err(format!("expected {description}, got {other}")),
    }
}

pub(crate) fn expect_eetf_map<'a>(term: &'a Term, message: &str) -> Result<&'a Map, String> {
    match term {
        Term::Map(map) => Ok(map),
        _ => Err(message.to_owned()),
    }
}

pub(crate) fn expect_list(term: &Term) -> Result<&Vec<Term>, String> {
    match term {
        Term::List(List { elements }) => Ok(elements),
        other => Err(format!("expected list, got {other}")),
    }
}

pub(crate) fn get_atom_keyed_field<'a>(
    map: &'a HashMap<Term, Term>,
    key: &str,
) -> Option<&'a Term> {
    map.get(&Term::Atom(Atom::from(key)))
}

pub(crate) fn atom_name(term: &Term) -> Option<&str> {
    match term {
        Term::Atom(atom) => Some(atom.name.as_str()),
        _ => None,
    }
}

pub(crate) fn term_to_binary_string(term: &Term) -> Result<String, String> {
    term_to_binary_str(term).map(str::to_owned)
}

pub(crate) fn term_to_binary_str(term: &Term) -> Result<&str, String> {
    match term {
        Term::Binary(Binary { bytes }) | Term::ByteList(ByteList { bytes }) => {
            std::str::from_utf8(bytes).map_err(|error| error.to_string())
        }
        other => Err(format!("expected utf8 binary/string, got {other}")),
    }
}

pub(crate) fn ensure_atom_keyed_allowed_fields(
    map: &HashMap<Term, Term>,
    allowed: &[&str],
    context: &str,
) -> Result<(), String> {
    for key in map.keys() {
        let Some(key_name) = atom_name(key) else {
            return Err(format!("{context} has non-atom field key: {key}"));
        };

        if !allowed.contains(&key_name) {
            return Err(format!("unsupported {context} field: {key_name}"));
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{expect_hash_map, expect_list, term_to_binary_string};
    use eetf::{Atom, Term};

    #[test]
    fn shared_helpers_preserve_contextual_error_fragments() {
        let atom = Term::Atom(Atom::from("not_a_map"));

        assert_eq!(
            expect_hash_map(&atom, "menu").unwrap_err(),
            "expected menu, got 'not_a_map'"
        );
        assert_eq!(
            expect_list(&atom).unwrap_err(),
            "expected list, got 'not_a_map'"
        );
        assert_eq!(
            term_to_binary_string(&atom).unwrap_err(),
            "expected utf8 binary/string, got 'not_a_map'"
        );
    }
}