slim_protocol/
lib.rs

1pub use self::slim_deserialize::{FromSlimReader, FromSlimReaderError};
2pub use self::slim_serialize::ToSlimString;
3use std::{
4    fmt::Display,
5    io::{BufReader, Read, Write},
6};
7use thiserror::Error;
8use ulid::Ulid;
9
10mod slim_deserialize;
11mod slim_serialize;
12
13pub struct SlimConnection<R, W>
14where
15    R: Read,
16    W: Write,
17{
18    reader: BufReader<R>,
19    writer: W,
20    _version: SlimVersion,
21    closed: bool,
22}
23
24#[derive(Debug, Error)]
25pub enum NewSlimConnectionError {
26    #[error(transparent)]
27    IoError(#[from] std::io::Error),
28    #[error(transparent)]
29    SlimVersionReadError(#[from] SlimVersionReadError),
30}
31
32#[derive(Debug, Error)]
33pub enum SendInstructionsError {
34    #[error(transparent)]
35    IoError(#[from] std::io::Error),
36    #[error(transparent)]
37    FromSlimReaderError(#[from] FromSlimReaderError),
38}
39
40impl<R, W> SlimConnection<R, W>
41where
42    R: Read,
43    W: Write,
44{
45    pub fn new(mut reader: R, writer: W) -> Result<Self, NewSlimConnectionError> {
46        let mut buf = [0_u8; 13];
47        reader.read_exact(&mut buf)?;
48        let version = SlimVersion::from_str(String::from_utf8_lossy(&buf))?;
49        Ok(Self {
50            reader: BufReader::new(reader),
51            writer,
52            _version: version,
53            closed: false,
54        })
55    }
56
57    pub fn send_instructions(
58        &mut self,
59        data: &[Instruction],
60    ) -> Result<Vec<InstructionResult>, SendInstructionsError> {
61        self.writer.write_all(data.to_slim_string().as_bytes())?;
62        Ok(Vec::from_reader(&mut self.reader)?)
63    }
64
65    pub fn close(mut self) -> Result<(), std::io::Error> {
66        self.say_goodbye()?;
67        self.closed = true;
68        Ok(())
69    }
70
71    fn say_goodbye(&mut self) -> Result<(), std::io::Error> {
72        self.writer.write_all("bye".to_slim_string().as_bytes())?;
73        self.writer.flush()?;
74        Ok(())
75    }
76}
77
78impl<R, W> Drop for SlimConnection<R, W>
79where
80    R: Read,
81    W: Write,
82{
83    fn drop(&mut self) {
84        if !self.closed {
85            self.say_goodbye().expect("Error sending goodbye");
86        }
87    }
88}
89
90#[derive(Debug, PartialEq, Eq)]
91pub enum SlimVersion {
92    V0_3,
93    V0_4,
94    V0_5,
95}
96
97#[derive(Debug, Error)]
98pub enum SlimVersionReadError {
99    #[error("Invalid slim version string")]
100    Invalid,
101    #[error("Version {0} not recognized")]
102    NotRecognized(String),
103}
104
105impl SlimVersion {
106    fn from_str(string: impl AsRef<str>) -> Result<Self, SlimVersionReadError> {
107        let (_, version) = string
108            .as_ref()
109            .split_once(" -- ")
110            .ok_or(SlimVersionReadError::Invalid)?;
111        Ok(match version.trim() {
112            "V0.3" => SlimVersion::V0_3,
113            "V0.4" => SlimVersion::V0_4,
114            "V0.5" => SlimVersion::V0_5,
115            v => return Err(SlimVersionReadError::NotRecognized(v.into())),
116        })
117    }
118}
119
120#[derive(Debug, PartialEq, Eq, Clone)]
121pub struct Id(String);
122
123impl Id {
124    pub fn new() -> Self {
125        Self::default()
126    }
127}
128
129impl From<String> for Id {
130    fn from(value: String) -> Self {
131        Self(value)
132    }
133}
134
135impl From<&str> for Id {
136    fn from(value: &str) -> Self {
137        Self(value.into())
138    }
139}
140
141impl Default for Id {
142    fn default() -> Self {
143        Self(Ulid::new().to_string())
144    }
145}
146
147impl Display for Id {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(f, "{}", self.0)
150    }
151}
152
153#[derive(Debug, PartialEq, Eq, Clone)]
154pub enum Instruction {
155    Import {
156        id: Id,
157        path: String,
158    },
159    Make {
160        id: Id,
161        instance: String,
162        class: String,
163        args: Vec<String>,
164    },
165    Call {
166        id: Id,
167        instance: String,
168        function: String,
169        args: Vec<String>,
170    },
171    #[allow(dead_code)]
172    CallAndAssign {
173        id: Id,
174        symbol: String,
175        instance: String,
176        function: String,
177        args: Vec<String>,
178    },
179    #[allow(dead_code)]
180    Assign {
181        id: Id,
182        symbol: String,
183        value: String,
184    },
185}
186
187#[derive(Debug, PartialEq, Eq)]
188pub struct InstructionResult {
189    pub id: Id,
190    pub value: InstructionResultValue,
191}
192
193impl InstructionResult {
194    pub fn new(id: Id, value: InstructionResultValue) -> Self {
195        Self { id, value }
196    }
197
198    pub fn ok(id: Id) -> Self {
199        Self {
200            id,
201            value: InstructionResultValue::Ok,
202        }
203    }
204    pub fn void(id: Id) -> Self {
205        Self {
206            id,
207            value: InstructionResultValue::Void,
208        }
209    }
210    pub fn exception(id: Id, message: ExceptionMessage) -> Self {
211        Self {
212            id,
213            value: InstructionResultValue::Exception(message),
214        }
215    }
216    pub fn string(id: Id, string: String) -> Self {
217        Self {
218            id,
219            value: InstructionResultValue::String(string),
220        }
221    }
222    pub fn list(id: Id, values: Vec<InstructionResultValue>) -> Self {
223        Self {
224            id,
225            value: InstructionResultValue::List(values),
226        }
227    }
228}
229
230#[derive(Debug, PartialEq, Eq, Clone)]
231pub enum InstructionResultValue {
232    Ok,
233    Void,
234    Exception(ExceptionMessage),
235    String(String),
236    List(Vec<InstructionResultValue>),
237}
238
239impl Display for InstructionResultValue {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        match self {
242            InstructionResultValue::Ok => write!(f, "OK")?,
243            InstructionResultValue::Void => write!(f, "VOID")?,
244            InstructionResultValue::Exception(message) => write!(
245                f,
246                "Exception `{}`",
247                message.pretty_message().unwrap_or(message.raw_message())
248            )?,
249            InstructionResultValue::String(value) => write!(f, "`{}`", value)?,
250            InstructionResultValue::List(value) => write!(
251                f,
252                "[{}]",
253                value
254                    .iter()
255                    .map(|v| v.to_string())
256                    .collect::<Vec<String>>()
257                    .join(",")
258            )?,
259        }
260        Ok(())
261    }
262}
263
264#[derive(PartialEq, Eq, Debug, Clone)]
265pub struct ExceptionMessage(String);
266
267#[derive(Debug, Error)]
268#[error("Failed processing exception {0}")]
269pub struct ExceptionPrettyMessageError(String);
270
271impl ExceptionMessage {
272    pub fn new(message: String) -> Self {
273        Self(message)
274    }
275
276    pub fn raw_message(&self) -> &str {
277        &self.0
278    }
279
280    pub fn pretty_message(&self) -> Result<&str, ExceptionPrettyMessageError> {
281        if let Some(pos) = self.0.find("message:<<") {
282            let (_, rest) = self.0.split_at(pos + 10);
283            let Some((message, _)) = rest.split_once(">>") else {
284                return Err(ExceptionPrettyMessageError(self.0.clone()));
285            };
286            Ok(message)
287        } else {
288            Ok(&self.0)
289        }
290    }
291}
292
293#[derive(Debug, PartialEq, Eq)]
294pub enum ByeOrSlimInstructions {
295    Bye,
296    Instructions(Vec<Instruction>),
297}
298
299#[cfg(test)]
300mod test {
301    use std::error::Error;
302    use std::io::Cursor;
303
304    use super::*;
305
306    #[test]
307    fn test_simple_connection() -> Result<(), Box<dyn Error>> {
308        let mut writer = Vec::new();
309        let connection =
310            SlimConnection::new(Cursor::new(b"Slim -- V0.5\n"), Cursor::new(&mut writer))?;
311        assert_eq!(SlimVersion::V0_5, connection._version);
312        drop(connection);
313        assert_eq!("000003:bye".to_string(), String::from_utf8_lossy(&writer));
314        Ok(())
315    }
316
317    #[test]
318    fn test_send_instructions_connection() -> Result<(), Box<dyn Error>> {
319        let mut writer = Vec::new();
320        let mut connection = SlimConnection::new(
321            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:]:]"),
322            Cursor::new(&mut writer),
323        )?;
324        let id = Id::from("01HFM0NQM3ZS6BBX0ZH6VA6DJX");
325        let result = connection.send_instructions(&[
326            Instruction::Import {
327                id: id.clone(),
328                path: "Path".into(),
329            },
330            Instruction::Call {
331                id: id.clone(),
332                instance: "Instance".into(),
333                function: "Function".into(),
334                args: Vec::new(),
335            },
336            Instruction::Call {
337                id: id.clone(),
338                instance: "Instance".into(),
339                function: "Function".into(),
340                args: Vec::new(),
341            },
342        ])?;
343        drop(connection);
344        assert_eq!(
345            vec![
346                InstructionResult {
347                    id: id.clone(),
348                    value: InstructionResultValue::Ok
349                },
350                InstructionResult {
351                    id: id.clone(),
352                    value: InstructionResultValue::String("null".into()),
353                },
354                InstructionResult {
355                    id: id.clone(),
356                    value: InstructionResultValue::String("Hello".into()),
357                }
358            ],
359            result
360        );
361        assert_eq!(
362            "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(),
363            String::from_utf8_lossy(&writer)
364        );
365        Ok(())
366    }
367}