xbbg_core 1.1.2

Safe Rust wrappers for BLPAPI session, request, and response primitives
//! Message type for Bloomberg BLPAPI
//!
//! Messages are the primary data containers in Bloomberg responses.
//! Each message contains a root element with field data.
//!
//! **Zero allocation**: Messages are borrowed from Events and provide
//! zero-cost access to their contents.

use crate::{ffi, CorrelationId, Element, Name};
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::ptr::NonNull;
use std::rc::Rc;

/// Bloomberg message wrapper.
///
/// Borrowed from Event, valid only while Event is alive.
/// NOT thread-safe - must be consumed on receiving thread.
///
/// # Lifetime
/// The lifetime `'a` ties this Message to its parent Event.
/// Do not store Messages - extract data immediately.
///
/// # Thread Safety
/// Messages are `!Send + !Sync` because:
/// - Bloomberg's API is not thread-safe
/// - Messages must be processed on the thread that received them
///
/// # Performance
/// All methods are `#[inline(always)]` for zero-cost abstraction.
#[repr(transparent)]
pub struct Message<'a> {
    ptr: *mut ffi::blpapi_Message_t,
    _life: PhantomData<&'a ()>,
    _marker: PhantomData<Rc<()>>, // Makes !Send + !Sync
}

impl<'a> Message<'a> {
    /// Construct from raw pointer (internal use only).
    ///
    /// # Safety
    /// Caller must ensure:
    /// - `ptr` is a valid `blpapi_Message_t` pointer
    /// - The lifetime `'a` does not outlive the parent Event
    /// - The pointer remains valid for the lifetime `'a`
    #[inline]
    pub(crate) unsafe fn from_raw(ptr: *mut ffi::blpapi_Message_t) -> Self {
        Self {
            ptr,
            _life: PhantomData,
            _marker: PhantomData,
        }
    }

    /// Get root element of this message.
    ///
    /// The root element contains all field data for this message.
    /// Use this to navigate the message structure.
    ///
    /// # Performance
    /// This is a hot path method - returns immediately with no allocation.
    #[inline(always)]
    pub fn elements(&self) -> Element<'a> {
        // SAFETY: blpapi_Message_elements returns a valid Element pointer.
        // The Element borrows from this Message, so lifetime 'a is correct.
        // Bloomberg guarantees the element pointer is valid for the message's lifetime.
        // Element::new is safe to call with a valid pointer.
        let ptr = unsafe { ffi::blpapi_Message_elements(self.ptr) };
        Element::new(ptr)
    }

    /// Message type name.
    ///
    /// Returns the schema type of this message (e.g., "ReferenceDataResponse").
    /// This is an owned Name because it's duplicated from Bloomberg's internal storage.
    ///
    /// # Performance
    /// This allocates a new Name (increments refcount). Cache if called repeatedly.
    #[inline(always)]
    pub fn message_type(&self) -> Name {
        self.name()
    }

    /// Message name (alias for `message_type()`).
    ///
    /// Returns the schema type of this message (e.g., "ReferenceDataResponse").
    /// This is an owned Name because it's duplicated from Bloomberg's internal storage.
    ///
    /// # Performance
    /// This allocates a new Name (increments refcount). Cache if called repeatedly.
    #[inline(always)]
    pub fn name(&self) -> Name {
        // SAFETY: blpapi_Message_messageType returns a valid Name pointer.
        // We duplicate it to get an owned Name that we can return.
        // The duplicate increments Bloomberg's internal refcount.
        let ptr = unsafe { ffi::blpapi_Message_messageType(self.ptr) };
        // SAFETY: blpapi_Name_duplicate returns a valid pointer
        unsafe { Name::from_raw(NonNull::new(ffi::blpapi_Name_duplicate(ptr)).unwrap()) }
    }

    /// Get the time this message was received by the SDK, as microseconds since Unix epoch.
    ///
    /// Returns `None` if receive-time recording was not enabled via
    /// `SessionOptions::set_record_subscription_receive_times(true)`.
    ///
    /// This is more accurate than `SystemTime::now()` because it records the time
    /// at the SDK's network layer, before any queuing or processing delays.
    #[inline]
    pub fn time_received_us(&self) -> Option<i64> {
        let mut tp = MaybeUninit::<ffi::blpapi_TimePoint_t>::uninit();
        // SAFETY: self.ptr is valid for the lifetime 'a. timeReceived writes to tp on success.
        let rc = unsafe { ffi::blpapi_Message_timeReceived(self.ptr as *const _, tp.as_mut_ptr()) };
        if rc != 0 {
            return None;
        }
        let tp = unsafe { tp.assume_init() };

        let mut dt = MaybeUninit::<ffi::blpapi_HighPrecisionDatetime_t>::uninit();
        // SAFETY: tp is initialized above. offset=0 gives UTC datetime.
        let rc =
            unsafe { ffi::blpapi_HighPrecisionDatetime_fromTimePoint(dt.as_mut_ptr(), &tp, 0) };
        if rc != 0 {
            return None;
        }

        let hpdt = crate::HighPrecisionDatetime(unsafe { dt.assume_init() });
        Some(hpdt.to_micros())
    }

    /// Get raw pointer for FFI calls (internal use).
    ///
    /// This is used internally by other xbbg-core types that need to call
    /// Bloomberg C API functions.
    #[inline(always)]
    #[allow(dead_code)] // Used in integration, not unit tests
    pub(crate) fn as_ptr(&self) -> *mut ffi::blpapi_Message_t {
        self.ptr
    }

    /// Get the number of correlation IDs on this message.
    ///
    /// Most messages have exactly one correlation ID, but multi-correlation
    /// responses (MCM) can have multiple.
    ///
    /// # Performance
    /// This is a hot path method - returns immediately with no allocation.
    #[inline(always)]
    pub fn num_correlation_ids(&self) -> usize {
        // SAFETY: self.ptr is valid for the lifetime 'a
        let count = unsafe { ffi::blpapi_Message_numCorrelationIds(self.ptr) };
        count.max(0) as usize
    }

    /// Get the correlation ID at the specified index.
    ///
    /// Returns `None` if the index is out of bounds.
    ///
    /// # Arguments
    /// * `index` - Zero-based index of the correlation ID
    ///
    /// # Performance
    /// This is a hot path method - minimal allocation (just the CorrelationId).
    #[inline]
    pub fn correlation_id(&self, index: usize) -> Option<CorrelationId> {
        if index >= self.num_correlation_ids() {
            return None;
        }

        // SAFETY: We've verified index is in bounds, and self.ptr is valid.
        // blpapi_Message_correlationId returns the CorrelationId by value.
        unsafe {
            let cid = ffi::blpapi_Message_correlationId(self.ptr, index);
            Some(CorrelationId::from_ffi(&cid))
        }
    }
}

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

    #[test]
    fn test_message_size() {
        // Message should be pointer-sized (transparent wrapper)
        assert_eq!(
            std::mem::size_of::<Message>(),
            std::mem::size_of::<*mut ()>()
        );
    }

    #[test]
    fn test_message_alignment() {
        // Message should have pointer alignment
        assert_eq!(
            std::mem::align_of::<Message>(),
            std::mem::align_of::<*mut ()>()
        );
    }
}