ltk_meta 0.9.1

Metadata formats and utilities for League Toolkit
Documentation
//! Bin object types and builders.

mod builder;
pub use builder::Builder;
use ltk_hash::{BinHash, WriteBytesExt as _};

use std::io;

use indexmap::IndexMap;
use ltk_io_ext::{measure, window_at};

use crate::{
    stream::{layout::Numbering, owned},
    traits::WriterExt as _,
};

use super::super::{Error, PropertyValueEnum};
use byteorder::{WriteBytesExt as _, LE};

/// A node/object in the bin tree.
///
/// Each object has a path hash (unique identifier), a class hash (type identifier),
/// and a collection of properties.
///
/// # Construction
///
/// Use [`BinObject::new`] for simple cases or [`BinObject::builder`] for
/// adding properties inline:
///
/// ```
/// use ltk_meta::BinObject;
/// use ltk_meta::property::values;
///
/// // Simple construction
/// let obj = BinObject::new(0x1234, 0x5678);
///
/// // Builder pattern with properties
/// let obj = BinObject::builder(0x1234, 0x5678)
///     .property(0xAAAA, values::I32::new(42))
///     .property(0xBBBB, values::String::from("hello"))
///     .build();
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct BinObject {
    /// The unique path hash identifying this object.
    pub path_hash: BinHash,

    /// The class hash identifying the type/schema of this object.
    pub class_hash: BinHash,

    /// The properties of this object, keyed by their name hash.
    pub properties: IndexMap<BinHash, PropertyValueEnum>,
}

impl BinObject {
    /// Creates a new `BinObject` with the given path and class hashes.
    ///
    /// The object starts with no properties.
    ///
    /// # Examples
    ///
    /// ```
    /// use ltk_meta::BinObject;
    ///
    /// let obj = BinObject::new(0x12345678, 0xABCDEF00);
    /// assert!(obj.properties.is_empty());
    /// ```
    pub fn new(path_hash: impl Into<BinHash>, class_hash: impl Into<BinHash>) -> Self {
        Self {
            path_hash: path_hash.into(),
            class_hash: class_hash.into(),
            properties: IndexMap::default(),
        }
    }

    /// Creates a new builder for constructing a `BinObject`.
    ///
    /// # Examples
    ///
    /// ```
    /// use ltk_meta::BinObject;
    /// use ltk_meta::property::values;
    ///
    /// let obj = BinObject::builder(0x12345678, 0xABCDEF00)
    ///     .property(0x1111, values::I32::new(42))
    ///     .property(0x2222, values::String::from("hello"))
    ///     .property(0x3333, values::Bool::new(true))
    ///     .build();
    ///
    /// assert_eq!(obj.properties.len(), 3);
    /// ```
    pub fn builder(
        path_hash: impl Into<BinHash>,
        class_hash: impl Into<BinHash>,
    ) -> builder::Builder {
        builder::Builder::new(path_hash, class_hash)
    }

    /// Reads a BinObject from a reader.
    ///
    /// # Arguments
    ///
    /// * `reader` - A reader that implements [`io::Read`] and [`io::Seek`].
    /// * `class_hash` - The hash of the class of the object.
    /// * `legacy` - Whether to read in legacy format.
    ///
    /// # Errors
    ///
    /// [`Error::InvalidSize`] if the object's declared size disagrees with what its property
    /// counts consumed, or whatever the value model raises for a property it refuses.
    pub fn from_reader<R: io::Read + io::Seek + ?Sized>(
        reader: &mut R,
        class_hash: BinHash,
        legacy: bool,
    ) -> Result<Self, Error> {
        // The object's size field bounds its bytes, so they are taken in one read and decoded
        // through the layout core - the same code path the stream's `read()` and `into_bin()`
        // take for the same object.
        owned::read_object_from(reader, class_hash, Numbering::from_legacy(legacy))
    }

    /// Writes this object to a writer.
    ///
    /// # Arguments
    ///
    /// * `writer` - A writer that implements io::Write and io::Seek.
    pub fn to_writer<W: io::Write + io::Seek + ?Sized>(&self, writer: &mut W) -> io::Result<()> {
        let size_pos = writer.stream_position()?;
        writer.write_u32::<LE>(0)?;

        let (size, _) = measure(writer, |writer| {
            writer.write_bin_hash::<LE>(self.path_hash)?;
            writer.write_u16::<LE>(self.properties.len() as _)?;
            for (name_hash, value) in self.properties.iter() {
                writer.write_property(*name_hash, value)?;
            }
            Ok::<_, io::Error>(())
        })?;

        window_at(writer, size_pos, |writer| writer.write_u32::<LE>(size as _))?;
        Ok(())
    }

    /// Returns the number of properties in this object.
    #[must_use]
    #[inline(always)]
    pub fn len(&self) -> usize {
        self.properties.len()
    }

    /// Returns `true` if this object has no properties.
    #[must_use]
    #[inline(always)]
    pub fn is_empty(&self) -> bool {
        self.properties.is_empty()
    }

    /// Returns a reference to the property with the given name hash, if it exists.
    #[must_use]
    #[inline(always)]
    pub fn get_property(&self, name_hash: BinHash) -> Option<&PropertyValueEnum> {
        self.properties.get(&name_hash)
    }

    /// Returns a mutable reference to the property with the given name hash, if it exists.
    #[must_use]
    #[inline(always)]
    pub fn get_property_mut(&mut self, name_hash: BinHash) -> Option<&mut PropertyValueEnum> {
        self.properties.get_mut(&name_hash)
    }

    /// Returns `true` if this object has a property with the given name hash.
    #[must_use]
    #[inline(always)]
    pub fn contains_property(&self, name_hash: BinHash) -> bool {
        self.properties.contains_key(&name_hash)
    }

    /// Adds or replaces a property.
    ///
    /// If a property with the same name hash already exists, it is replaced
    /// and the old property is returned.
    #[inline(always)]
    pub fn insert(
        &mut self,
        name_hash: BinHash,
        value: impl Into<PropertyValueEnum>,
    ) -> Option<PropertyValueEnum> {
        self.properties.insert(name_hash, value.into())
    }

    /// Removes and returns the property with the given name hash, if it exists.
    pub fn remove_property(&mut self, name_hash: BinHash) -> Option<PropertyValueEnum> {
        self.properties.shift_remove(&name_hash)
    }

    /// Returns an iterator over the properties in this object.
    #[inline]
    pub fn iter(&self) -> impl Iterator<Item = (&BinHash, &PropertyValueEnum)> {
        self.properties.iter()
    }

    /// Returns a mutable iterator over the properties in this object.
    #[inline]
    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&BinHash, &mut PropertyValueEnum)> {
        self.properties.iter_mut()
    }
}

impl<'a> IntoIterator for &'a BinObject {
    type Item = (&'a BinHash, &'a PropertyValueEnum);
    type IntoIter = indexmap::map::Iter<'a, BinHash, PropertyValueEnum>;

    fn into_iter(self) -> Self::IntoIter {
        self.properties.iter()
    }
}

impl<'a> IntoIterator for &'a mut BinObject {
    type Item = (&'a BinHash, &'a mut PropertyValueEnum);
    type IntoIter = indexmap::map::IterMut<'a, BinHash, PropertyValueEnum>;

    fn into_iter(self) -> Self::IntoIter {
        self.properties.iter_mut()
    }
}

impl IntoIterator for BinObject {
    type Item = (BinHash, PropertyValueEnum);
    type IntoIter = indexmap::map::IntoIter<BinHash, PropertyValueEnum>;

    fn into_iter(self) -> Self::IntoIter {
        self.properties.into_iter()
    }
}