ts-json 0.2.0

JSON config file linting for my applications
Documentation
//! Helpers for application config.

use std::{fs, io, path::PathBuf};

use schemars::{JsonSchema, Schema, SchemaGenerator, generate::SchemaSettings};
use serde::{Serialize, de::DeserializeOwned};
use ts_error::diagnostic::Diagnostics;
use ts_io::{ReadFileError, read_file_to_string};

use crate::{ValidationError, validate_json};

/// Trait defining a struct as representing a config file.
pub trait ConfigFile: DeserializeOwned + Serialize + JsonSchema {
    /// The path to the config file.
    fn config_file_path() -> PathBuf;

    /// Delete the config file.
    fn delete(&self) -> io::Result<()> {
        fs::remove_file(Self::config_file_path())
    }

    /// Get the schema for the config file.
    fn get_schema() -> Schema {
        let schema_generator = SchemaGenerator::from(SchemaSettings::draft07());
        schema_generator.into_root_schema_for::<Self>()
    }

    /// Try load the config file, linting it against its JSON schema.
    fn try_load() -> Result<Self, LoadConfigError> {
        let _ = Self::write_schema();

        let source = read_file_to_string(&Self::config_file_path())
            .map_err(|source| LoadConfigError::ReadConfig { source })?;

        let schema = Self::get_schema();
        let schema = serde_json::to_string(&schema)
            .map_err(|source| LoadConfigError::SerailizeSchema { source })?;

        let diagnostics =
            validate_json(&source, &schema, Some(Self::config_file_path()).as_deref())
                .map_err(|source| LoadConfigError::ValidationFailure { source })?;

        if !diagnostics.is_empty() {
            Err(LoadConfigError::InvalidConfig {
                source: diagnostics,
            })
        } else {
            serde_json::from_str(&source)
                .map_err(|source| LoadConfigError::DeserializeConfig { source })
        }
    }

    /// Write the config file.
    fn write(&self) -> io::Result<()> {
        let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
        fs::write(Self::config_file_path(), json)
    }

    /// Write the schema to the schema file.
    fn write_schema() -> io::Result<()> {
        let schema_file_path = Self::config_file_path().with_file_name("config.schema.json");
        let schema = Self::get_schema();
        let json = serde_json::to_string_pretty(&schema).map_err(io::Error::other)?;
        fs::write(schema_file_path, json)
    }
}

/// Error variants for loading config.
#[derive(Debug)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum LoadConfigError {
    #[non_exhaustive]
    SerailizeSchema { source: serde_json::Error },

    #[non_exhaustive]
    ValidationFailure { source: ValidationError },

    #[non_exhaustive]
    InvalidConfig { source: Diagnostics },

    #[non_exhaustive]
    DeserializeConfig { source: serde_json::Error },

    #[non_exhaustive]
    ReadConfig { source: ReadFileError },
}
impl core::fmt::Display for LoadConfigError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match &self {
            Self::SerailizeSchema { .. } => {
                write!(f, "JSON schema for the config could not be serialized")
            }
            Self::ValidationFailure { .. } => write!(f, "could not validate config file"),
            Self::InvalidConfig { .. } => write!(f, "config file is invalid"),
            Self::DeserializeConfig { .. } => write!(f, "config file could not be deserialized"),
            Self::ReadConfig { .. } => write!(f, "could not read config file"),
        }
    }
}
impl core::error::Error for LoadConfigError {
    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
        match &self {
            Self::DeserializeConfig { source, .. } | Self::SerailizeSchema { source, .. } => {
                Some(source)
            }
            Self::ValidationFailure { source, .. } => Some(source),
            Self::InvalidConfig { source, .. } => Some(source),
            Self::ReadConfig { source, .. } => Some(source),
        }
    }
}