nestor 0.1.0

irc bot framework for nightly
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;

use crate::config::Config;
use crate::request::Request;
use crate::response::{Outcome, Response};

use failure::Error;

pub(crate) struct CommandRouter {
    commands: HashMap<&'static str, &'static dyn CommandHandler>,
    default: Option<&'static dyn CommandHandler>,
}

impl CommandRouter {
    pub fn new() -> Self {
        CommandRouter {
            commands: HashMap::new(),
            default: None,
        }
    }

    pub fn add_handlers(
        &mut self,
        handlers: Vec<(Option<&'static str>, &'static dyn CommandHandler)>,
    ) {
        for (label, handler) in handlers {
            if let Some(label) = label {
                self.commands.insert(label, handler);
            } else if self.default.is_none() {
                self.default = Some(handler);
            }
        }
    }

    pub async fn route<'r>(&'r self, request: &'r Request<'r>) -> Outcome {
        if request
            .config
            .bot_settings
            .blacklisted_users
            .contains(&request.command.source_nick.into())
        {
            return Outcome::Success(Response::None);
        }

        let c: &str = request.command.command_str.as_ref();
        if let Some(handler) = self.commands.get(c) {
            match handler.handle(&request) {
                Ok(fut) => await!(fut),
                Err(err) => return Outcome::Failure(err),
            }
        } else if let Some(handler) = &self.default {
            match handler.handle(&request) {
                Ok(fut) => await!(fut),
                Err(err) => return Outcome::Failure(err),
            }
        } else {
            Outcome::Success(Response::None)
        }
    }
}

pub trait CommandHandler {
    fn route_id(&self) -> Option<&'static str>;

    fn handle<'a, 'r>(
        &'a self,
        request: &'a Request<'r>,
    ) -> Result<Pin<Box<Future<Output = Outcome> + 'a>>, Error>;
}

pub struct Command<'a> {
    pub source_nick: &'a str,
    pub command_str: String,
    pub arguments: Vec<String>,
}

impl<'a> Command<'a> {
    pub fn try_parse<'u>(
        our_nick: &'u str,
        source_nick: &'a str,
        message: &'a str,
        config: &Config,
    ) -> Option<Command<'a>> {
        let command_str = config
            .bot_settings
            .command_indicator
            .iter()
            .chain(std::iter::once(&format!("{}:", our_nick)))
            .filter_map(|indicator| {
                if !message.starts_with(indicator) {
                    message.find(&format!("{{{}", indicator)).and_then(|start| {
                        let start = start + indicator.len() + 1;
                        let end = message.split_at(start).1.find('}')?;

                        Some(&message[start..(start + end)])
                    })
                } else {
                    Some(message.split_at(indicator.len()).1)
                }
            })
            .nth(0)?;

        let mut parts = command_str.trim().split(' ').map(String::from);
        let command = parts.next()?;
        let args = parts.collect();
        Some(Command {
            source_nick,
            command_str: command,
            arguments: args,
        })
    }

    pub fn from_command_str(source_nick: &'a str, command_str: &str) -> Option<Command<'a>> {
        let mut parts = command_str.trim().split(' ').map(String::from);
        let command = parts.next()?;
        let args = parts.collect();
        Some(Command {
            source_nick,
            command_str: command,
            arguments: args,
        })
    }
}