ts_json/
config.rs

1//! Helpers for application config.
2
3use std::{fs, io, path::PathBuf};
4
5use schemars::{JsonSchema, Schema, SchemaGenerator, generate::SchemaSettings};
6use serde::{Serialize, de::DeserializeOwned};
7use ts_error::diagnostic::Diagnostics;
8use ts_io::{ReadFileError, read_file_to_string};
9
10use crate::{ValidationError, validate_json};
11
12/// Trait defining a struct as representing a config file.
13pub trait ConfigFile: DeserializeOwned + Serialize + JsonSchema {
14    /// The path to the config file.
15    fn config_file_path() -> PathBuf;
16
17    /// Delete the config file.
18    fn delete(&self) -> io::Result<()> {
19        fs::remove_file(Self::config_file_path())
20    }
21
22    /// Get the schema for the config file.
23    fn get_schema() -> Schema {
24        let schema_generator = SchemaGenerator::from(SchemaSettings::draft07());
25        schema_generator.into_root_schema_for::<Self>()
26    }
27
28    /// Try load the config file, linting it against its JSON schema.
29    fn try_load() -> Result<Self, LoadConfigError> {
30        let _ = Self::write_schema();
31
32        let source = read_file_to_string(&Self::config_file_path())
33            .map_err(|source| LoadConfigError::ReadConfig { source })?;
34
35        let schema = Self::get_schema();
36        let schema = serde_json::to_string(&schema)
37            .map_err(|source| LoadConfigError::SerailizeSchema { source })?;
38
39        let diagnostics =
40            validate_json(&source, &schema, Some(Self::config_file_path()).as_deref())
41                .map_err(|source| LoadConfigError::ValidationFailure { source })?;
42
43        if !diagnostics.is_empty() {
44            Err(LoadConfigError::InvalidConfig {
45                source: diagnostics,
46            })
47        } else {
48            serde_json::from_str(&source)
49                .map_err(|source| LoadConfigError::DeserializeConfig { source })
50        }
51    }
52
53    /// Write the config file.
54    fn write(&self) -> io::Result<()> {
55        let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
56        fs::write(Self::config_file_path(), json)
57    }
58
59    /// Write the schema to the schema file.
60    fn write_schema() -> io::Result<()> {
61        let schema_file_path = Self::config_file_path().with_file_name("config.schema.json");
62        let schema = Self::get_schema();
63        let json = serde_json::to_string_pretty(&schema).map_err(io::Error::other)?;
64        fs::write(schema_file_path, json)
65    }
66}
67
68/// Error variants for loading config.
69#[derive(Debug)]
70#[non_exhaustive]
71#[allow(missing_docs)]
72pub enum LoadConfigError {
73    #[non_exhaustive]
74    SerailizeSchema { source: serde_json::Error },
75
76    #[non_exhaustive]
77    ValidationFailure { source: ValidationError },
78
79    #[non_exhaustive]
80    InvalidConfig { source: Diagnostics },
81
82    #[non_exhaustive]
83    DeserializeConfig { source: serde_json::Error },
84
85    #[non_exhaustive]
86    ReadConfig { source: ReadFileError },
87}
88impl core::fmt::Display for LoadConfigError {
89    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90        match &self {
91            Self::SerailizeSchema { .. } => {
92                write!(f, "JSON schema for the config could not be serialized")
93            }
94            Self::ValidationFailure { .. } => write!(f, "could not validate config file"),
95            Self::InvalidConfig { .. } => write!(f, "config file is invalid"),
96            Self::DeserializeConfig { .. } => write!(f, "config file could not be deserialized"),
97            Self::ReadConfig { .. } => write!(f, "could not read config file"),
98        }
99    }
100}
101impl core::error::Error for LoadConfigError {
102    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
103        match &self {
104            Self::DeserializeConfig { source, .. } | Self::SerailizeSchema { source, .. } => {
105                Some(source)
106            }
107            Self::ValidationFailure { source, .. } => Some(source),
108            Self::InvalidConfig { source, .. } => Some(source),
109            Self::ReadConfig { source, .. } => Some(source),
110        }
111    }
112}