indi 5.1.1

Client library for interfacing with the Instrument Neutral Distributed Interface (INDI) protocol.
Documentation
use quick_xml::{events::Event, NsReader};
use tokio::{
    io::{AsyncRead, AsyncWriteExt},
    net::{
        tcp::{OwnedReadHalf, OwnedWriteHalf},
        TcpStream,
    },
};
use tracing::error;

use crate::{client::Connectable, Command};
use tokio::io::BufReader;

use super::{AsyncClientConnection, AsyncReadConnection, AsyncWriteConnection};

impl AsyncClientConnection for TcpStream {
    type Reader = AsyncIndiReader<OwnedReadHalf>;
    type Writer = AsyncIndiWriter;

    fn to_indi(self) -> (Self::Writer, Self::Reader) {
        let (reader, writer) = self.into_split();
        let reader = NsReader::from_reader(BufReader::new(reader));

        (AsyncIndiWriter { writer }, AsyncIndiReader::new(reader))
    }
}
impl Connectable for TcpStream {
    type ConnectionError = std::io::Error;
    async fn connect(addr: String) -> Result<Self, Self::ConnectionError> {
        Self::connect(addr).await
    }
}

pub struct AsyncIndiReader<T> {
    reader: NsReader<BufReader<T>>,
}

impl<T: AsyncRead + Unpin> AsyncIndiReader<T> {
    fn new(reader: quick_xml::reader::NsReader<BufReader<T>>) -> AsyncIndiReader<T> {
        AsyncIndiReader { reader }
    }

    async fn read_xml_documents(&mut self) -> Option<Result<String, crate::DeError>> {
        let mut buffer = Vec::new();
        let mut document = Vec::new();
        let mut depth = 0;
        loop {
            let event = match self.reader.read_event_into_async(&mut buffer).await {
                Ok(e) => e,
                Err(e) => return Some(Err(e.into())),
            };
            match event {
                Event::Start(e) => {
                    depth += 1;
                    document.extend_from_slice(b"<");
                    document.extend_from_slice(e.name().as_ref());
                    for attr in e.attributes() {
                        let attr = match attr {
                            Ok(d) => d,
                            Err(e) => return Some(Err(e.into())),
                        };
                        document.extend_from_slice(b" ");
                        document.extend_from_slice(attr.key.as_ref());
                        document.extend_from_slice(b"=\"");
                        document.extend_from_slice(&attr.value);
                        document.extend_from_slice(b"\"");
                    }
                    document.extend_from_slice(b">");
                }
                Event::End(e) => {
                    depth -= 1;
                    document.extend_from_slice(b"</");
                    document.extend_from_slice(e.name().as_ref());
                    document.extend_from_slice(b">");
                    if depth == 0 {
                        let doc = match String::from_utf8(document) {
                            Ok(d) => d,
                            Err(e) => return Some(Err(e.into())),
                        };
                        return Some(Ok(doc));
                    }
                }
                Event::Text(e) => {
                    document.extend_from_slice(&e.into_inner());
                }
                Event::Eof => return None,
                Event::Empty(e) => {
                    document.extend_from_slice(b"<");
                    document.extend_from_slice(e.name().as_ref());
                    for attr in e.attributes() {
                        let attr = match attr {
                            Ok(d) => d,
                            Err(e) => return Some(Err(e.into())),
                        };
                        document.extend_from_slice(b" ");
                        document.extend_from_slice(attr.key.as_ref());
                        document.extend_from_slice(b"=\"");
                        document.extend_from_slice(&attr.value);
                        document.extend_from_slice(b"\"");
                    }

                    document.extend_from_slice(b">");
                    document.extend_from_slice(b"</");
                    document.extend_from_slice(e.name().as_ref());
                    document.extend_from_slice(b">");
                    if depth == 0 {
                        let doc = match String::from_utf8(document) {
                            Ok(d) => d,
                            Err(e) => return Some(Err(e.into())),
                        };
                        return Some(Ok(doc));
                    }
                }
                _ => {}
            }
            buffer.clear();
        }
    }
}

impl<T: AsyncRead + Unpin + Send> AsyncReadConnection for AsyncIndiReader<T> {
    async fn read(&mut self) -> Option<Result<crate::Command, crate::DeError>> {
        let doc = match self.read_xml_documents().await? {
            Ok(doc) => doc,
            Err(e) => return Some(Err(e.into())),
        };
        let cmd = quick_xml::de::from_str::<crate::Command>(&doc).map_err(|x| x.into());

        if let Err(e) = &cmd {
            error!("Failed to parse ( {:?} ):\n{}", e, &doc);
        }
        return Some(cmd);
    }
}

pub struct AsyncIndiWriter {
    writer: OwnedWriteHalf,
}

impl AsyncWriteConnection for AsyncIndiWriter {
    async fn write(&mut self, cmd: Command) -> Result<(), crate::DeError> {
        let buffer = quick_xml::se::to_string(&cmd)?;
        self.writer.write(buffer.as_bytes()).await?;

        self.writer.write(b"\n").await?;
        self.writer.flush().await?;
        Ok(())
    }

    async fn shutdown(&mut self) -> Result<(), crate::DeError> {
        Ok(self.writer.shutdown().await?)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use tokio::time::Duration;

    #[tokio::test]
    async fn test_threads_stop_on_shutdown() {
        let connection = TcpStream::connect("indi:7624")
            .await
            .expect("connecting to indi");
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
        let mut client = crate::client::Client::new(Some(tx));
        let client_task: tokio::task::JoinHandle<()> =
            tokio::task::spawn(crate::client::start(client.devices.clone(), rx, connection));

        client.shutdown();
        tokio::time::timeout(Duration::from_secs(1), client_task)
            .await
            .unwrap()
            .unwrap();
    }
}