im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use serde_json::Value;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MessageRetryTarget {
    DirectText,
    GroupText,
    DirectPayload,
    GroupPayload,
}

pub(crate) fn send_state_from_delivery(
    delivery: &crate::messages::DeliveryState,
    operation_id: Option<String>,
    message_id: Option<crate::ids::MessageId>,
    updated_at: Option<String>,
    retry_target: Option<MessageRetryTarget>,
) -> (
    crate::messages::MessageSendState,
    Option<crate::messages::MessageRetryPlan>,
) {
    let reason = match delivery {
        crate::messages::DeliveryState::Failed { reason } => non_empty_string(reason),
        _ => None,
    };
    let state = match delivery {
        crate::messages::DeliveryState::Accepted => crate::messages::MessageSendStateKind::Accepted,
        crate::messages::DeliveryState::Sent => crate::messages::MessageSendStateKind::Sent,
        crate::messages::DeliveryState::StoredLocally => {
            crate::messages::MessageSendStateKind::StoredLocally
        }
        crate::messages::DeliveryState::Failed { .. } => {
            crate::messages::MessageSendStateKind::Failed
        }
    };
    let retry_plan = retry_plan_for_state(
        &state,
        retry_target,
        operation_id.clone(),
        message_id.clone(),
        reason.clone(),
    );
    (
        crate::messages::MessageSendState {
            state,
            operation_id,
            message_id,
            reason,
            updated_at,
        },
        retry_plan,
    )
}

pub(crate) fn send_state_label(state: &crate::messages::MessageSendStateKind) -> &'static str {
    match state {
        crate::messages::MessageSendStateKind::Pending => "pending",
        crate::messages::MessageSendStateKind::Accepted => "accepted",
        crate::messages::MessageSendStateKind::Sent => "sent",
        crate::messages::MessageSendStateKind::StoredLocally => "stored_locally",
        crate::messages::MessageSendStateKind::Failed => "failed",
    }
}

pub(crate) fn send_state_from_metadata(
    metadata: &str,
    fallback_message_id: &str,
) -> Option<crate::messages::MessageSendState> {
    let metadata = parse_metadata(metadata)?;
    if let Some(state) = metadata
        .get("send_state")
        .and_then(|value| send_state_from_value(value, fallback_message_id))
    {
        return Some(state);
    }
    let state = metadata
        .get("delivery_state")
        .and_then(Value::as_str)
        .and_then(send_state_kind_from_str)?;
    let operation_id = string_value(metadata.get("operation_id"));
    let message_id = string_value(metadata.get("message_id"))
        .or_else(|| non_empty_string(fallback_message_id))
        .and_then(parse_message_id);
    let reason = metadata
        .get("failure_reason")
        .and_then(Value::as_str)
        .and_then(non_empty_string);
    let updated_at = metadata
        .get("send_state_updated_at")
        .or_else(|| metadata.get("accepted_at"))
        .and_then(Value::as_str)
        .and_then(non_empty_string);
    Some(crate::messages::MessageSendState {
        state,
        operation_id,
        message_id,
        reason,
        updated_at,
    })
}

pub(crate) fn retry_plan_from_metadata(
    metadata: &str,
    send_state: Option<&crate::messages::MessageSendState>,
    retry_target: Option<MessageRetryTarget>,
) -> Option<crate::messages::MessageRetryPlan> {
    let metadata = parse_metadata(metadata)?;
    if let Some(plan) = metadata.get("retry_plan").and_then(retry_plan_from_value) {
        return Some(plan);
    }
    let state = send_state?;
    retry_plan_for_state(
        &state.state,
        retry_target,
        state.operation_id.clone(),
        state.message_id.clone(),
        state.reason.clone(),
    )
}

pub(crate) fn send_state_from_value(
    value: &Value,
    fallback_message_id: &str,
) -> Option<crate::messages::MessageSendState> {
    if let Some(value) = value.as_str() {
        return Some(crate::messages::MessageSendState {
            state: send_state_kind_from_str(value)?,
            operation_id: None,
            message_id: non_empty_string(fallback_message_id).and_then(parse_message_id),
            reason: None,
            updated_at: None,
        });
    }
    let object = value.as_object()?;
    let state = object
        .get("state")
        .and_then(Value::as_str)
        .and_then(send_state_kind_from_str)?;
    let operation_id = object
        .get("operation_id")
        .and_then(Value::as_str)
        .and_then(non_empty_string);
    let message_id = object
        .get("message_id")
        .and_then(Value::as_str)
        .and_then(non_empty_string)
        .or_else(|| non_empty_string(fallback_message_id))
        .and_then(parse_message_id);
    let reason = object
        .get("reason")
        .or_else(|| object.get("failure_reason"))
        .and_then(Value::as_str)
        .and_then(non_empty_string);
    let updated_at = object
        .get("updated_at")
        .and_then(Value::as_str)
        .and_then(non_empty_string);
    Some(crate::messages::MessageSendState {
        state,
        operation_id,
        message_id,
        reason,
        updated_at,
    })
}

pub(crate) fn retry_plan_for_state(
    state: &crate::messages::MessageSendStateKind,
    retry_target: Option<MessageRetryTarget>,
    operation_id: Option<String>,
    message_id: Option<crate::ids::MessageId>,
    reason: Option<String>,
) -> Option<crate::messages::MessageRetryPlan> {
    if state != &crate::messages::MessageSendStateKind::Failed {
        return None;
    }
    let action = match retry_target? {
        MessageRetryTarget::DirectText => crate::messages::MessageRetryAction::RetryDirectText,
        MessageRetryTarget::GroupText => crate::messages::MessageRetryAction::RetryGroupText,
        MessageRetryTarget::DirectPayload => {
            crate::messages::MessageRetryAction::RetryDirectPayload
        }
        MessageRetryTarget::GroupPayload => crate::messages::MessageRetryAction::RetryGroupPayload,
    };
    Some(crate::messages::MessageRetryPlan {
        retryable: true,
        action,
        operation_id,
        message_id,
        reason,
    })
}

fn retry_plan_from_value(value: &Value) -> Option<crate::messages::MessageRetryPlan> {
    let object = value.as_object()?;
    let retryable = object
        .get("retryable")
        .and_then(Value::as_bool)
        .unwrap_or(false);
    let action = object
        .get("action")
        .and_then(Value::as_str)
        .and_then(retry_action_from_str)
        .unwrap_or(crate::messages::MessageRetryAction::None);
    let operation_id = object
        .get("operation_id")
        .and_then(Value::as_str)
        .and_then(non_empty_string);
    let message_id = object
        .get("message_id")
        .and_then(Value::as_str)
        .and_then(non_empty_string)
        .and_then(parse_message_id);
    let reason = object
        .get("reason")
        .and_then(Value::as_str)
        .and_then(non_empty_string);
    Some(crate::messages::MessageRetryPlan {
        retryable,
        action,
        operation_id,
        message_id,
        reason,
    })
}

fn retry_action_from_str(value: &str) -> Option<crate::messages::MessageRetryAction> {
    match normalize_state(value).as_str() {
        "none" => Some(crate::messages::MessageRetryAction::None),
        "retry_direct_text" | "direct_text" => {
            Some(crate::messages::MessageRetryAction::RetryDirectText)
        }
        "retry_group_text" | "group_text" => {
            Some(crate::messages::MessageRetryAction::RetryGroupText)
        }
        "retry_direct_payload" | "direct_payload" => {
            Some(crate::messages::MessageRetryAction::RetryDirectPayload)
        }
        "retry_group_payload" | "group_payload" => {
            Some(crate::messages::MessageRetryAction::RetryGroupPayload)
        }
        _ => None,
    }
}

fn send_state_kind_from_str(value: &str) -> Option<crate::messages::MessageSendStateKind> {
    match normalize_state(value).as_str() {
        "pending" => Some(crate::messages::MessageSendStateKind::Pending),
        "accepted" => Some(crate::messages::MessageSendStateKind::Accepted),
        "sent" => Some(crate::messages::MessageSendStateKind::Sent),
        "stored_locally" => Some(crate::messages::MessageSendStateKind::StoredLocally),
        "failed" => Some(crate::messages::MessageSendStateKind::Failed),
        _ => None,
    }
}

fn parse_metadata(metadata: &str) -> Option<Value> {
    if metadata.trim().is_empty() {
        return None;
    }
    serde_json::from_str(metadata).ok()
}

fn string_value(value: Option<&Value>) -> Option<String> {
    value.and_then(Value::as_str).and_then(non_empty_string)
}

fn non_empty_string(value: &str) -> Option<String> {
    let value = value.trim();
    if value.is_empty() {
        None
    } else {
        Some(value.to_string())
    }
}

fn parse_message_id(value: String) -> Option<crate::ids::MessageId> {
    crate::ids::MessageId::parse(value).ok()
}

fn normalize_state(value: &str) -> String {
    value.trim().to_ascii_lowercase().replace('-', "_")
}

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

    #[test]
    fn message_state_metadata_projects_legacy_delivery_state() {
        let metadata = r#"{"operation_id":"op-1","delivery_state":"stored-locally"}"#;

        let state = send_state_from_metadata(metadata, "msg-1").unwrap();

        assert_eq!(
            state.state,
            crate::messages::MessageSendStateKind::StoredLocally
        );
        assert_eq!(state.operation_id.as_deref(), Some("op-1"));
        assert_eq!(state.message_id.unwrap().as_str(), "msg-1");
    }

    #[test]
    fn message_state_failed_generates_direct_retry_plan() {
        let (state, plan) = send_state_from_delivery(
            &crate::messages::DeliveryState::Failed {
                reason: "service unavailable".to_string(),
            },
            Some("op-2".to_string()),
            crate::ids::MessageId::parse("msg-2").ok(),
            None,
            Some(MessageRetryTarget::DirectText),
        );

        assert_eq!(state.state, crate::messages::MessageSendStateKind::Failed);
        let plan = plan.unwrap();
        assert!(plan.retryable);
        assert_eq!(
            plan.action,
            crate::messages::MessageRetryAction::RetryDirectText
        );
        assert_eq!(plan.operation_id.as_deref(), Some("op-2"));
        assert_eq!(plan.message_id.unwrap().as_str(), "msg-2");
        assert_eq!(plan.reason.as_deref(), Some("service unavailable"));
    }
}