localize 0.2.0

Localization library
Documentation
#![doc = include_str!("../README.md")]

use std::{
    borrow::Cow,
    collections::HashMap,
    fs,
    marker::PhantomPinned,
    path::{Path, PathBuf},
};

macro_rules! include_mods {
    ($($mod:ident),*) => {
        $(
            mod $mod;
            #[allow(unused_imports)]
            pub use $mod::*;
        )*
    };
}

include_mods!(cache, error);

pub type Locale = json::Value;
pub(crate) type Sources = HashMap<String, PathBuf>;

/// Heart and soul of this crated. Intended to be initialized once.
pub struct Localizer<'this> {
    locales: Sources,
    // cache contains references to `locales`
    cache: LocaleCache<'this>,
    default: Option<&'static str>,
    debug: bool,
    _pin: PhantomPinned,
}

impl<'this> Localizer<'this> {
    /// Creates new [`Localizer`] and searches for available locales in specified folder.
    pub fn new(locales_path: impl AsRef<Path>) -> Self {
        let locales = walkdir::WalkDir::new(locales_path)
            .into_iter()
            .filter_map(|de| match de {
                Ok(de)
                    if de.file_type().is_file()
                        && de.file_name().to_string_lossy().ends_with(".json") =>
                {
                    log::trace!("Found locale: `{:?}`", de.path());
                    Some((
                        de.file_name()
                            .to_str()
                            .unwrap()
                            .split_once(".json")
                            .unwrap()
                            .0
                            .into(),
                        de.path().to_path_buf(),
                    ))
                }
                _ => None,
            })
            .collect();

        Self {
            locales,
            cache: LocaleCache::new(),
            debug: false,
            _pin: PhantomPinned::default(),
            default: None,
        }
    }

    /// Loads all available locales into internal cache.
    #[must_use]
    pub fn precache_all(mut self) -> Self {
        for (s, _) in unsafe { std::mem::transmute::<_, &'_ Sources>(&self.locales) }.iter() {
            self.cache.prefer_recache(s, &self.locales).unwrap();
        }
        self
    }

    /// Enables debug mode.
    /// This mostly means that [`Localizer`] will prefer fetching locales from disk rather than cache.
    #[must_use]
    #[inline]
    pub fn debug(mut self, enable: bool) -> Self {
        self.debug = enable;
        self
    }

    pub fn default_locale(mut self, default_locale: &'static str) -> Self {
        self.default = Some(default_locale);
        self.cache.1 = Some(default_locale);
        self
    }

    /// # Normal mode
    /// Looks up cache for tag. If not found fetches locale from disk and **overwrites** old value in cache,
    /// returns a reference to new locale afterwards. Otherwise returns cached value.
    /// # Debug mode
    /// Looks up available locales on disk for specified tag. If found, overwrites locale in cache with value from disk and returns a reference to it.
    /// Otherwise tries to fetch the locale from cache.
    pub fn localize<'w>(&mut self, tag: &'w str) -> Result<&Locale>
    where
        'w: 'this,
    {
        if self.debug {
            self.cache.prefer_recache(tag, &self.locales)
        } else {
            self.cache.look_up_or_cache(tag, &self.locales)
        }
    }

    /// # Normal mode
    /// Looks up cache for tag. If not found fetches locale from disk and returns borrowed value of it.
    /// Otherwise returns cached value.
    /// # Debug mode
    /// Looks up available locales for specified tag. If found fetches locale from disk and returns owned value of it.
    /// Otherwise returns an error.
    pub fn localize_no_cache<'w>(&self, tag: &'w str) -> Result<Cow<Locale>>
    where
        'w: 'this,
    {
        if self.debug {
            log::trace!("Running debug mode, fetching locale `{}` from disk", tag);

            if let Some(locale) = self.locales.get(tag) {
                log::trace!("Found locale `{}` in sources, fetching from disk.", tag);
                
                Ok(
                    Cow::Owned(
                        json::from_reader(fs::File::open(locale)?)?
                    )
                )
            } else if let Some(default_tag) = self.default {
                log::trace!("Locale `{}` missing in sources, falling back to default locale `{}`", tag,default_tag);

                Ok(
                    Cow::Owned(
                        json::from_reader(fs::File::open(
                            self.locales.get(default_tag).ok_or(
                                LocaleError::FallbackToDefaultFailed
                            )?
                        )?)?
                    )
                )
            } else {
                log::error!("Locale `{}` wasn't found in sources, and default was `None`. Aborted.", tag);

                Err(LocaleError::SourceLookupFailed)
            }
        } else {
            self.cache.look_up_or_fetch(tag, &self.locales)
        }
    }
}