soup-sdk 0.3.1

채팅 이벤트 수신 SDK
Documentation
use chrono::{DateTime, Utc};
use std::sync::Arc;
use thiserror::Error;

use crate::chat::constants::{SEPARATOR_U8, message_codes::MessageCode};

#[derive(Debug, Clone)]
pub struct RawMessage {
    pub code: MessageCode,
    pub ret_code: u32,
    pub body: Vec<String>,
    pub received_time: DateTime<Utc>,
    pub raw: Arc<[u8]>,
}

struct MessageHeader {
    code: MessageCode,
    ret_code: u32,
}

#[derive(Debug, Clone, Error)]
pub enum ParseError {
    #[error("프레임이 너무 짧습니다: {len} bytes")]
    FrameTooShort { len: usize },

    #[error("헤더 길이가 잘못되었습니다: {len} bytes")]
    InvalidHeaderLength { len: usize },

    #[error("필드가 없습니다: code={code}, index={index}, len={len}")]
    MissingField {
        code: MessageCode,
        index: usize,
        len: usize,
    },

    #[error("숫자 파싱 실패: code={code}, index={index}, value={value:?}, target={target}")]
    InvalidNumber {
        code: MessageCode,
        index: usize,
        value: String,
        target: &'static str,
    },

    #[error("JSON 파싱 실패: code={code}, index={index}, reason={reason}")]
    InvalidJson {
        code: MessageCode,
        index: usize,
        reason: String,
    },

    #[error("프로토콜 파싱 실패: {0}")]
    Protocol(String),
}

pub type ParseResult<T> = std::result::Result<T, ParseError>;

impl RawMessage {
    pub fn field(&self, index: usize) -> ParseResult<&str> {
        self.body
            .get(index)
            .map(String::as_str)
            .ok_or(ParseError::MissingField {
                code: self.code,
                index,
                len: self.body.len(),
            })
    }

    pub fn field_string(&self, index: usize) -> ParseResult<String> {
        Ok(self.field(index)?.to_string())
    }

    pub fn parse_u32(&self, index: usize) -> ParseResult<u32> {
        let value = self.field(index)?;
        value.parse::<u32>().map_err(|_| ParseError::InvalidNumber {
            code: self.code,
            index,
            value: value.to_string(),
            target: "u32",
        })
    }

    pub fn parse_usize_or_default(&self, index: usize, default: usize) -> usize {
        self.field(index)
            .ok()
            .and_then(|value| value.parse::<usize>().ok())
            .unwrap_or(default)
    }

    pub fn parse_u32_or_default(&self, index: usize, default: u32) -> u32 {
        self.field(index)
            .ok()
            .and_then(|value| value.parse::<u32>().ok())
            .unwrap_or(default)
    }
}

pub fn parse_message(data: Arc<[u8]>) -> ParseResult<RawMessage> {
    let now = Utc::now();

    if data.len() < 14 {
        return Err(ParseError::FrameTooShort { len: data.len() });
    }

    let header_bytes = &data[0..14];

    let header = parse_header(header_bytes)?;

    let body = &data[14..];

    Ok(RawMessage {
        code: header.code,
        ret_code: header.ret_code,
        body: parse_body(body),
        received_time: now,
        raw: data,
    })
}

fn parse_header(header: &[u8]) -> ParseResult<MessageHeader> {
    if header.len() != 14 {
        return Err(ParseError::InvalidHeaderLength { len: header.len() });
    }

    Ok(MessageHeader {
        code: parse_bytes_to_u32(&header[2..6]),
        ret_code: parse_bytes_to_u32(&header[12..14]),
    })
}

fn parse_body(body: &[u8]) -> Vec<String> {
    // 입력 데이터가 너무 짧으면(헤더만 있거나 비어있으면) 빈 벡터를 반환합니다.
    if body.len() < 2 {
        return Vec::new();
    }

    // 첫 번째 바이트를 건너뛴 슬라이스를 가져옵니다.
    let data_to_process = &body[1..];

    // 구분자 개수를 미리 세어서 Vec 용량을 할당합니다.
    let separator_count = data_to_process
        .iter()
        .filter(|&&b| b == SEPARATOR_U8)
        .count();
    let mut result = Vec::with_capacity(separator_count + 1);

    // 데이터를 구분자로 분할하고, 각 조각을 문자열로 변환한 뒤, 벡터로 수집합니다.
    for byte_part in data_to_process.split(|&byte| byte == SEPARATOR_U8) {
        // UTF-8 유효성을 먼저 확인하여 불필요한 할당을 피합니다.
        match std::str::from_utf8(byte_part) {
            Ok(s) => result.push(s.to_string()),
            Err(_) => {
                // 유효하지 않은 UTF-8인 경우에만 lossy conversion 사용
                let cow_str = String::from_utf8_lossy(byte_part);
                result.push(cow_str.into_owned());
            }
        }
    }

    result
}

fn parse_bytes_to_u32(bytes: &[u8]) -> u32 {
    // UTF-8 바이트를 직접 파싱하여 불필요한 String 할당을 피합니다.
    match std::str::from_utf8(bytes) {
        Ok(s) => s.parse::<u32>().unwrap_or(0),
        Err(_) => {
            // 유효하지 않은 UTF-8인 경우에만 lossy conversion 사용
            String::from_utf8_lossy(bytes).parse::<u32>().unwrap_or(0)
        }
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use super::*;

    fn frame(code: u32, body: &[u8]) -> Arc<[u8]> {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&[27, 9]);
        bytes.extend_from_slice(format!("{code:04}").as_bytes());
        bytes.extend_from_slice(format!("{:06}", body.len()).as_bytes());
        bytes.extend_from_slice(b"00");
        bytes.extend_from_slice(body);
        bytes.into()
    }

    #[test]
    fn decodes_header_and_body_fields() {
        let raw = parse_message(frame(5, b"\x0chello\x0cuser")).unwrap();

        assert_eq!(raw.code, 5);
        assert_eq!(raw.ret_code, 0);
        assert_eq!(raw.body, vec!["hello".to_string(), "user".to_string()]);
    }

    #[test]
    fn rejects_short_header() {
        let short: Arc<[u8]> = vec![1_u8, 2, 3].into();
        let err = parse_message(short).unwrap_err();

        assert!(matches!(err, ParseError::FrameTooShort { len: 3 }));
    }

    #[test]
    fn keeps_unknown_code_decodable() {
        let raw = parse_message(frame(9999, b"\x0cnew-payload")).unwrap();

        assert_eq!(raw.code, 9999);
        assert_eq!(raw.body, vec!["new-payload".to_string()]);
    }

    #[test]
    fn invalid_utf8_body_is_lossy_not_fatal() {
        let raw = parse_message(frame(5, b"\x0c\xff")).unwrap();

        assert_eq!(raw.body.len(), 1);
        assert!(raw.body[0].contains('\u{fffd}'));
    }
}