galvan-files 0.0.3

Locates galvan files in a directory, checks naming conventions and generates artifact names.
Documentation
use std::fs;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::GalvanFileExtension;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum FileError {
    #[error("Error when trying to read source file {0}: {1}")]
    Io(PathBuf, #[source] std::io::Error),
    #[error("File name {0} is not valid UTF-8")]
    Utf8(String),
    #[error("File name {0} is not allowed. Only lowercase letters and _ are allowed in galvan file names")]
    Naming(String),
    #[error("File {0} has no extension")]
    MissingExtension(PathBuf),
}

impl FileError {
    pub fn io(path: impl AsRef<Path>, error: std::io::Error) -> Self {
        Self::Io(path.as_ref().to_owned(), error)
    }

    pub fn utf8(file_name: impl Into<String>) -> Self {
        Self::Utf8(file_name.into())
    }

    pub fn naming(file_name: impl Into<String>) -> Self {
        Self::Naming(file_name.into())
    }

    pub fn missing_extension(path: impl AsRef<Path>) -> Self {
        Self::MissingExtension(path.as_ref().to_owned())
    }
}

pub type SourceResult = Result<Source, FileError>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
    File {
        path: Arc<Path>,
        content: Arc<str>,
        canonical_name: Arc<str>,
    },
    Str(Arc<str>),
    Missing,
    Builtin,
}

impl Source {
    pub fn from_string(string: impl Into<Arc<str>>) -> Source {
        Self::Str(string.into())
    }

    pub fn read(path: impl AsRef<Path>) -> SourceResult {
        let path = path.as_ref();
        if !path.has_galvan_extension() {
            Err(FileError::missing_extension(path))?
        }
        let stem = path
            .file_stem()
            .ok_or_else(|| FileError::missing_extension(path))?;

        let stem = stem
            .to_str()
            .ok_or_else(|| FileError::utf8(stem.to_string_lossy()))?;
        if !stem.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
            Err(FileError::naming(stem))?
        }
        let canonical_name = stem.replace(".", "_").into();
        let content = fs::read_to_string(path)
            .map_err(|e| FileError::io(path, e))?
            .into();
        let path = path.into();

        Ok(Self::File {
            path,
            content,
            canonical_name,
        })
    }

    pub fn content(&self) -> &str {
        match self {
            Self::File { content, .. } => content.as_ref(),
            Self::Str(content) => content.as_ref(),
            Self::Missing => "",
            Self::Builtin => "",
        }
    }

    pub fn origin(&self) -> Option<&Path> {
        match self {
            Self::File { path, .. } => Some(path),
            Self::Str(_) => None,
            Self::Missing => None,
            Self::Builtin => None,
        }
    }

    pub fn canonical_name(&self) -> Option<&str> {
        match self {
            Self::File {
                path: _,
                content: _,
                canonical_name,
            } => Some(canonical_name),
            Self::Str(_) => None,
            Self::Missing => None,
            Self::Builtin => Some("galvan_std"),
        }
    }
}

impl<T> From<T> for Source
where
    T: Into<Arc<str>>,
{
    fn from(value: T) -> Self {
        Self::from_string(value)
    }
}

impl Deref for Source {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        self.content()
    }
}