sip-header 1.0.0-beta.2

SIP header field parsers: Via, Warning, Auth, Accept, Contact, Call-Info, History-Info, Geolocation, Security, and full IANA header catalog
Documentation
use std::fmt;
use std::str::FromStr;

/// Notification state for partial updates (RFC 4575 Section 4.1).
///
/// Used on `ConferenceInfo`, `Users`, `User`, `Endpoint`, `SidebarsByRef`,
/// and `SidebarsByVal` to indicate whether the document is a full snapshot,
/// a partial delta, or a deletion notice.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum State {
    /// Complete state — replaces any previously cached document.
    #[cfg_attr(feature = "serde", serde(rename = "full"))]
    Full,
    /// Incremental update — merge with previously cached state.
    #[cfg_attr(feature = "serde", serde(rename = "partial"))]
    Partial,
    /// Entity has been removed.
    #[cfg_attr(feature = "serde", serde(rename = "deleted"))]
    Deleted,
}

impl fmt::Display for State {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::Full => "full",
            Self::Partial => "partial",
            Self::Deleted => "deleted",
        })
    }
}

/// Error returned when parsing an invalid [`State`] string.
#[derive(Debug, Clone)]
pub struct ParseStateError(pub String);

impl fmt::Display for ParseStateError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "invalid conference-info state ({} bytes)",
            self.0
                .len()
        )
    }
}

impl std::error::Error for ParseStateError {}

impl FromStr for State {
    type Err = ParseStateError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "full" => Ok(Self::Full),
            "partial" => Ok(Self::Partial),
            "deleted" => Ok(Self::Deleted),
            other => Err(ParseStateError(other.to_owned())),
        }
    }
}

/// Execution context shared by `joining-info`, `disconnection-info`, and
/// `referred` elements (RFC 4575 Section 5.6).
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ExecutionInfo {
    /// ISO 8601 timestamp (e.g. `2026-02-24T14:26:16.217046Z`).
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub when: Option<String>,
    /// Human-readable or protocol-defined reason string.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub reason: Option<String>,
    /// URI of the actor that triggered this event.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub by: Option<String>,
}

impl ExecutionInfo {
    /// Create an empty execution info.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the timestamp.
    pub fn with_when(mut self, when: impl Into<String>) -> Self {
        self.when = Some(when.into());
        self
    }

    /// Set the reason.
    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
        self.reason = Some(reason.into());
        self
    }

    /// Set the actor URI.
    pub fn with_by(mut self, by: impl Into<String>) -> Self {
        self.by = Some(by.into());
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(feature = "serde")]
    use crate::conference_info::assert_serde_wire_names;
    use crate::conference_info::assert_wire_names;

    const STATES: &[State] = &[State::Full, State::Partial, State::Deleted];
    const _: fn(State) = |v| match v {
        State::Full | State::Partial | State::Deleted => {}
    };

    #[test]
    fn state_wire_names() {
        assert_wire_names(STATES);
        #[cfg(feature = "serde")]
        assert_serde_wire_names(STATES);
    }

    #[test]
    fn state_invalid() {
        assert!("Full"
            .parse::<State>()
            .is_err());
        assert!("FULL"
            .parse::<State>()
            .is_err());
        assert!(""
            .parse::<State>()
            .is_err());
    }

    #[test]
    fn state_error_display_omits_input() {
        let e = "secret"
            .parse::<State>()
            .unwrap_err();
        assert_eq!(e.0, "secret");
        assert_eq!(e.to_string(), "invalid conference-info state (6 bytes)");
    }
}