Skip to main content

schema/
loader.rs

1use std::path::{Path, PathBuf};
2
3use schema_config_toml::ConfigToml;
4use schema_core::common::IndexName;
5use schema_core::{IndexSchema, ParseFrom};
6use schema_index_yaml::SchemaYaml;
7
8use crate::{Config, Index};
9
10#[derive(thiserror::Error, Debug)]
11pub enum LoadError {
12    #[error("failed to read config `{path}`: {source}")]
13    ReadConfig {
14        path: PathBuf,
15        #[source]
16        source: std::io::Error,
17    },
18    #[error("failed to parse config `{path}`: {source}")]
19    ParseConfig {
20        path: PathBuf,
21        #[source]
22        source: schema_config_toml::ParseError,
23    },
24    #[error("failed to read schema `{name}` from `{path}`: {source}")]
25    ReadSchema {
26        name: IndexName,
27        path: PathBuf,
28        #[source]
29        source: std::io::Error,
30    },
31    #[error("failed to parse schema `{name}` from `{path}`: {source}")]
32    ParseSchema {
33        name: IndexName,
34        path: PathBuf,
35        #[source]
36        source: schema_index_yaml::ParseError,
37    },
38    #[error("failed to convert schema `{name}` from `{path}`: {source}")]
39    ConvertSchema {
40        name: IndexName,
41        path: PathBuf,
42        #[source]
43        source: schema_index_yaml::ConversionError,
44    },
45    #[error("duplicate index name `{0}`")]
46    DuplicateIndex(IndexName),
47}
48
49/// Loads a full [`Config`] from a TOML config file at `config_path`.
50///
51/// Source and sinks come from the TOML itself; each `[[index]]` entry
52/// references a YAML schema file, resolved relative to the config file's
53/// directory, which is parsed and converted into [`Index`] entries.
54pub fn load(config_path: impl AsRef<Path>) -> Result<Config, LoadError> {
55    let config_path = config_path.as_ref();
56
57    let raw = std::fs::read_to_string(config_path).map_err(|source| LoadError::ReadConfig {
58        path: config_path.to_path_buf(),
59        source,
60    })?;
61
62    let config_toml = ConfigToml::try_parse(&raw).map_err(|source| LoadError::ParseConfig {
63        path: config_path.to_path_buf(),
64        source,
65    })?;
66
67    let indexes = config_toml.index.clone();
68
69    // Infallible: secrets are deferred and URLs validated at resolution time.
70    let mut config = Config::from(config_toml);
71
72    let base_dir = config_path.parent().unwrap_or(Path::new("."));
73
74    for entry in indexes {
75        let schema_path = resolve(base_dir, entry.schema.as_path());
76
77        let raw =
78            std::fs::read_to_string(&schema_path).map_err(|source| LoadError::ReadSchema {
79                name: entry.name.clone(),
80                path: schema_path.clone(),
81                source,
82            })?;
83
84        let schema_yaml = SchemaYaml::try_parse(&raw).map_err(|source| LoadError::ParseSchema {
85            name: entry.name.clone(),
86            path: schema_path.clone(),
87            source,
88        })?;
89
90        let schema =
91            IndexSchema::try_from(schema_yaml).map_err(|source| LoadError::ConvertSchema {
92                name: entry.name.clone(),
93                path: schema_path.clone(),
94                source,
95            })?;
96
97        let index = Index {
98            enabled: entry.enabled,
99            schema,
100            on_error: entry.on_error,
101        };
102
103        if config.indexes.insert(entry.name.clone(), index).is_some() {
104            return Err(LoadError::DuplicateIndex(entry.name));
105        }
106    }
107
108    Ok(config)
109}
110
111/// Resolves a schema path against the config's directory. Absolute paths are
112/// used as-is.
113fn resolve(base_dir: &Path, path: &Path) -> PathBuf {
114    if path.is_absolute() {
115        path.to_path_buf()
116    } else {
117        base_dir.join(path)
118    }
119}