modbot 0.9.0

Discord bot for https://mod.io. ModBot provides commands to search for mods and notifications about added & edited mods.
use std::fmt;
use std::fs;
use std::net::SocketAddr;
use std::path::Path;

use serde::de::{Deserialize, Deserializer, Error, Visitor};
use serde_derive::Deserialize;

use crate::Result;

const DEFAULT_METRICS_SOCKET_ADDR: ([u8; 4], u16) = ([127, 0, 0, 1], 8080);

#[derive(Deserialize)]
pub struct Config {
    pub bot: BotConfig,
    pub modio: ModioConfig,
    #[serde(default)]
    pub metrics: MetricsConfig,
}

#[derive(Deserialize)]
pub struct MetricsConfig {
    #[serde(default = "default_metrics_socket_addr")]
    pub addr: SocketAddr,
}

#[derive(Deserialize)]
pub struct BotConfig {
    pub token: String,
    pub database_url: String,
}

#[derive(Deserialize)]
pub struct ModioConfig {
    pub host: Option<Host>,
    pub api_key: String,
    pub token: Option<String>,
}

#[derive(Debug)]
pub enum Host {
    Default,
    Test,
    Dynamic,
    DynamicWithCustom(String),
    Custom(String),
}

impl<'de> Deserialize<'de> for Host {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
        struct StrVisitor;

        impl Visitor<'_> for StrVisitor {
            type Value = Host;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str("string")
            }

            fn visit_str<E: Error>(self, v: &str) -> std::result::Result<Self::Value, E> {
                let host = match v {
                    "default" => Host::Default,
                    "test" => Host::Test,
                    "dynamic" => Host::Dynamic,
                    _ => {
                        if let Some(host) = v.strip_prefix("dynamic:") {
                            Host::DynamicWithCustom(host.to_owned())
                        } else {
                            Host::Custom(v.to_owned())
                        }
                    }
                };

                Ok(host)
            }
        }

        deserializer.deserialize_str(StrVisitor)
    }
}

pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Config> {
    let data = fs::read_to_string(path)?;
    Ok(toml::from_str(&data)?)
}

impl Default for MetricsConfig {
    fn default() -> Self {
        Self {
            addr: default_metrics_socket_addr(),
        }
    }
}

fn default_metrics_socket_addr() -> SocketAddr {
    SocketAddr::from(DEFAULT_METRICS_SOCKET_ADDR)
}