irkki-core 0.1.0

The irkki core library.
Documentation
use log::info;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::net::TcpStream;

pub struct IRCClient {
    nickname: String,
    server: String,
    port: u16,
    channel: String,
    reader: Option<BufReader<TcpStream>>,
    writer: Option<BufWriter<TcpStream>>,
}

impl IRCClient {
    pub fn connect(
        nickname: impl Into<String>,
        server: impl Into<String>,
        port: u16,
    ) -> io::Result<Self> {
        let mut client = Self::new(nickname, server, port);
        client.connect_inner()?;
        Ok(client)
    }

    fn new(nickname: impl Into<String>, server: impl Into<String>, port: u16) -> Self {
        Self {
            nickname: nickname.into(),
            server: server.into(),
            port,
            channel: "#testchannel".to_string(),
            reader: None,
            writer: None,
        }
    }

    fn connect_inner(&mut self) -> io::Result<()> {
        let stream = TcpStream::connect((self.server.as_str(), self.port))?;
        let reader = BufReader::new(stream.try_clone()?);
        let writer = BufWriter::new(stream);

        self.reader = Some(reader);
        self.writer = Some(writer);

        self.send_line(&format!("NICK {}", self.nickname))?;
        self.send_line(&format!("USER {} 0 * :{}", self.nickname, self.nickname))?;
        self.send_line(&format!("JOIN {}", self.channel))?;

        Ok(())
    }

    pub fn listen<F>(&mut self, mut message_handler: F) -> io::Result<()>
    where
        F: FnMut(String) -> io::Result<()>,
    {
        loop {
            let mut line = String::new();
            let read_result = {
                let reader = self.reader.as_mut().ok_or_else(|| {
                    io::Error::new(io::ErrorKind::NotConnected, "Client is not connected.")
                })?;
                reader.read_line(&mut line)
            };

            match read_result {
                Ok(0) => break,
                Ok(_) => {
                    info!("Received line: {}", line.trim_end());
                    let line = line.trim_end_matches(&['\r', '\n'][..]).to_string();
                    if line.starts_with("PING") {
                        let response = line.replacen("PING", "PONG", 1);
                        self.send_line(&response)?;
                        continue;
                    }

                    message_handler(line)?;
                }
                Err(_) => break,
            }
        }

        Ok(())
    }

    pub fn quit(&mut self) -> io::Result<()> {
        if self.writer.is_none() {
            return Ok(());
        }

        self.send_line(&format!("PART {} :Goodbye!", self.channel))?;
        self.send_line("QUIT :Client closed")?;

        Ok(())
    }

    fn send_line(&mut self, line: &str) -> io::Result<()> {
        let writer = self.writer.as_mut().ok_or_else(|| {
            io::Error::new(io::ErrorKind::NotConnected, "Client is not connected.")
        })?;

        writer.write_all(line.as_bytes())?;
        writer.write_all(b"\r\n")?;
        writer.flush()
    }
}