slim_protocol 0.2.0

General Slim Protocol stuff. Used internally by rust_slim and Temoc
Documentation
pub use self::slim_deserialize::{FromSlimReader, FromSlimReaderError};
pub use self::slim_serialize::ToSlimString;
use std::{
    fmt::Display,
    io::{BufReader, Read, Write},
};
use thiserror::Error;
use ulid::Ulid;

mod slim_deserialize;
mod slim_serialize;

pub struct SlimConnection<R, W>
where
    R: Read,
    W: Write,
{
    reader: BufReader<R>,
    writer: W,
    _version: SlimVersion,
    closed: bool,
}

#[derive(Debug, Error)]
pub enum NewSlimConnectionError {
    #[error(transparent)]
    IoError(#[from] std::io::Error),
    #[error(transparent)]
    SlimVersionReadError(#[from] SlimVersionReadError),
}

#[derive(Debug, Error)]
pub enum SendInstructionsError {
    #[error(transparent)]
    IoError(#[from] std::io::Error),
    #[error(transparent)]
    FromSlimReaderError(#[from] FromSlimReaderError),
}

impl<R, W> SlimConnection<R, W>
where
    R: Read,
    W: Write,
{
    pub fn new(mut reader: R, writer: W) -> Result<Self, NewSlimConnectionError> {
        let mut buf = [0_u8; 13];
        reader.read_exact(&mut buf)?;
        let version = SlimVersion::from_str(String::from_utf8_lossy(&buf))?;
        Ok(Self {
            reader: BufReader::new(reader),
            writer,
            _version: version,
            closed: false,
        })
    }

    pub fn send_instructions(
        &mut self,
        data: &[Instruction],
    ) -> Result<Vec<InstructionResult>, SendInstructionsError> {
        self.writer.write_all(data.to_slim_string().as_bytes())?;
        Ok(Vec::from_reader(&mut self.reader)?)
    }

    pub fn close(mut self) -> Result<(), std::io::Error> {
        self.say_goodbye()?;
        self.closed = true;
        Ok(())
    }

    fn say_goodbye(&mut self) -> Result<(), std::io::Error> {
        self.writer.write_all("bye".to_slim_string().as_bytes())?;
        self.writer.flush()?;
        Ok(())
    }
}

impl<R, W> Drop for SlimConnection<R, W>
where
    R: Read,
    W: Write,
{
    fn drop(&mut self) {
        if !self.closed {
            self.say_goodbye().expect("Error sending goodbye");
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum SlimVersion {
    V0_3,
    V0_4,
    V0_5,
}

#[derive(Debug, Error)]
pub enum SlimVersionReadError {
    #[error("Invalid slim version string")]
    Invalid,
    #[error("Version {0} not recognized")]
    NotRecognized(String),
}

impl SlimVersion {
    fn from_str(string: impl AsRef<str>) -> Result<Self, SlimVersionReadError> {
        let (_, version) = string
            .as_ref()
            .split_once(" -- ")
            .ok_or(SlimVersionReadError::Invalid)?;
        Ok(match version.trim() {
            "V0.3" => SlimVersion::V0_3,
            "V0.4" => SlimVersion::V0_4,
            "V0.5" => SlimVersion::V0_5,
            v => return Err(SlimVersionReadError::NotRecognized(v.into())),
        })
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Id(String);

impl Id {
    pub fn new() -> Self {
        Self::default()
    }
}

impl From<String> for Id {
    fn from(value: String) -> Self {
        Self(value)
    }
}

impl From<&str> for Id {
    fn from(value: &str) -> Self {
        Self(value.into())
    }
}

impl Default for Id {
    fn default() -> Self {
        Self(Ulid::new().to_string())
    }
}

impl Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Instruction {
    Import {
        id: Id,
        path: String,
    },
    Make {
        id: Id,
        instance: String,
        class: String,
        args: Vec<String>,
    },
    Call {
        id: Id,
        instance: String,
        function: String,
        args: Vec<String>,
    },
    #[allow(dead_code)]
    CallAndAssign {
        id: Id,
        symbol: String,
        instance: String,
        function: String,
        args: Vec<String>,
    },
    #[allow(dead_code)]
    Assign {
        id: Id,
        symbol: String,
        value: String,
    },
}

#[derive(Debug, PartialEq, Eq)]
pub struct InstructionResult {
    pub id: Id,
    pub value: InstructionResultValue,
}

impl InstructionResult {
    pub fn new(id: Id, value: InstructionResultValue) -> Self {
        Self { id, value }
    }

    pub fn ok(id: Id) -> Self {
        Self {
            id,
            value: InstructionResultValue::Ok,
        }
    }
    pub fn void(id: Id) -> Self {
        Self {
            id,
            value: InstructionResultValue::Void,
        }
    }
    pub fn exception(id: Id, message: ExceptionMessage) -> Self {
        Self {
            id,
            value: InstructionResultValue::Exception(message),
        }
    }
    pub fn string(id: Id, string: String) -> Self {
        Self {
            id,
            value: InstructionResultValue::String(string),
        }
    }
    pub fn list(id: Id, values: Vec<InstructionResultValue>) -> Self {
        Self {
            id,
            value: InstructionResultValue::List(values),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum InstructionResultValue {
    Ok,
    Void,
    Exception(ExceptionMessage),
    String(String),
    List(Vec<InstructionResultValue>),
}

impl Display for InstructionResultValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            InstructionResultValue::Ok => write!(f, "OK")?,
            InstructionResultValue::Void => write!(f, "VOID")?,
            InstructionResultValue::Exception(message) => write!(
                f,
                "Exception `{}`",
                message.pretty_message().unwrap_or(message.raw_message())
            )?,
            InstructionResultValue::String(value) => write!(f, "`{}`", value)?,
            InstructionResultValue::List(value) => write!(
                f,
                "[{}]",
                value
                    .iter()
                    .map(|v| v.to_string())
                    .collect::<Vec<String>>()
                    .join(",")
            )?,
        }
        Ok(())
    }
}

#[derive(PartialEq, Eq, Debug, Clone)]
pub struct ExceptionMessage(String);

#[derive(Debug, Error)]
#[error("Failed processing exception {0}")]
pub struct ExceptionPrettyMessageError(String);

impl ExceptionMessage {
    pub fn new(message: String) -> Self {
        Self(message)
    }

    pub fn raw_message(&self) -> &str {
        &self.0
    }

    pub fn pretty_message(&self) -> Result<&str, ExceptionPrettyMessageError> {
        if let Some(pos) = self.0.find("message:<<") {
            let (_, rest) = self.0.split_at(pos + 10);
            let Some((message, _)) = rest.split_once(">>") else {
                return Err(ExceptionPrettyMessageError(self.0.clone()));
            };
            Ok(message)
        } else {
            Ok(&self.0)
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum ByeOrSlimInstructions {
    Bye,
    Instructions(Vec<Instruction>),
}

#[cfg(test)]
mod test {
    use std::error::Error;
    use std::io::Cursor;

    use super::*;

    #[test]
    fn test_simple_connection() -> Result<(), Box<dyn Error>> {
        let mut writer = Vec::new();
        let connection =
            SlimConnection::new(Cursor::new(b"Slim -- V0.5\n"), Cursor::new(&mut writer))?;
        assert_eq!(SlimVersion::V0_5, connection._version);
        drop(connection);
        assert_eq!("000003:bye".to_string(), String::from_utf8_lossy(&writer));
        Ok(())
    }

    #[test]
    fn test_send_instructions_connection() -> Result<(), Box<dyn Error>> {
        let mut writer = Vec::new();
        let mut connection = SlimConnection::new(
            Cursor::new(b"Slim -- V0.5\n000197:[000003:000053:[000002:000026:01HFM0NQM3ZS6BBX0ZH6VA6DJX:000002:OK:]:000055:[000002:000026:01HFM0NQM3ZS6BBX0ZH6VA6DJX:000004:null:]:000056:[000002:000026:01HFM0NQM3ZS6BBX0ZH6VA6DJX:000005:Hello:]:]"),
            Cursor::new(&mut writer),
        )?;
        let id = Id::from("01HFM0NQM3ZS6BBX0ZH6VA6DJX");
        let result = connection.send_instructions(&[
            Instruction::Import {
                id: id.clone(),
                path: "Path".into(),
            },
            Instruction::Call {
                id: id.clone(),
                instance: "Instance".into(),
                function: "Function".into(),
                args: Vec::new(),
            },
            Instruction::Call {
                id: id.clone(),
                instance: "Instance".into(),
                function: "Function".into(),
                args: Vec::new(),
            },
        ])?;
        drop(connection);
        assert_eq!(
            vec![
                InstructionResult {
                    id: id.clone(),
                    value: InstructionResultValue::Ok
                },
                InstructionResult {
                    id: id.clone(),
                    value: InstructionResultValue::String("null".into()),
                },
                InstructionResult {
                    id: id.clone(),
                    value: InstructionResultValue::String("Hello".into()),
                }
            ],
            result
        );
        assert_eq!(
            "000276:[000003:000069:[000003:000026:01HFM0NQM3ZS6BBX0ZH6VA6DJX:000006:import:000004:Path:]:000087:[000004:000026:01HFM0NQM3ZS6BBX0ZH6VA6DJX:000004:call:000008:Instance:000008:Function:]:000087:[000004:000026:01HFM0NQM3ZS6BBX0ZH6VA6DJX:000004:call:000008:Instance:000008:Function:]:]000003:bye".to_string(),
            String::from_utf8_lossy(&writer)
        );
        Ok(())
    }
}