use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use hyalo_core::schema::{RawSchemaConfig, SchemaConfig};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct SearchConfig {
language: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigFile {
dir: Option<String>,
format: Option<String>,
hints: Option<bool>,
site_prefix: Option<String>,
#[allow(dead_code)]
views: Option<HashMap<String, toml::Value>>,
search: Option<SearchConfig>,
#[serde(default)]
schema: Option<toml::Value>,
}
#[derive(Debug)]
pub(crate) struct ResolvedDefaults {
pub(crate) dir: PathBuf,
pub(crate) format: String,
pub(crate) hints: bool,
pub(crate) site_prefix: Option<String>,
pub(crate) search_language: Option<String>,
pub(crate) schema: SchemaConfig,
}
impl PartialEq for ResolvedDefaults {
fn eq(&self, other: &Self) -> bool {
self.dir == other.dir
&& self.format == other.format
&& self.hints == other.hints
&& self.site_prefix == other.site_prefix
&& self.search_language == other.search_language
}
}
impl ResolvedDefaults {
fn hardcoded() -> Self {
Self {
dir: PathBuf::from("."),
format: "json".to_owned(),
hints: true,
site_prefix: None,
search_language: None,
schema: SchemaConfig::default(),
}
}
}
pub(crate) fn load_config() -> ResolvedDefaults {
match std::env::current_dir() {
Ok(cwd) => load_config_from(&cwd),
Err(e) => {
crate::warn::warn(format!(
"could not determine current directory to locate .hyalo.toml: {e}"
));
ResolvedDefaults::hardcoded()
}
}
}
pub(crate) fn load_config_from(dir: &Path) -> ResolvedDefaults {
let path = dir.join(".hyalo.toml");
let contents = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return ResolvedDefaults::hardcoded();
}
Err(e) => {
crate::warn::warn(format!("could not read .hyalo.toml: {e}"));
return ResolvedDefaults::hardcoded();
}
};
let cfg: ConfigFile = match toml::from_str(&contents) {
Ok(c) => c,
Err(e) => {
crate::warn::warn(format!("malformed .hyalo.toml: {e}"));
return ResolvedDefaults::hardcoded();
}
};
let defaults = ResolvedDefaults::hardcoded();
let schema = parse_schema_from_toml(cfg.schema.as_ref());
ResolvedDefaults {
dir: cfg.dir.map(PathBuf::from).unwrap_or(defaults.dir),
format: cfg.format.unwrap_or(defaults.format),
hints: cfg.hints.unwrap_or(defaults.hints),
site_prefix: cfg.site_prefix,
search_language: cfg.search.and_then(|s| s.language),
schema,
}
}
fn parse_schema_from_toml(raw: Option<&toml::Value>) -> SchemaConfig {
let Some(val) = raw else {
return SchemaConfig::default();
};
match val.clone().try_into::<RawSchemaConfig>() {
Ok(raw_cfg) => SchemaConfig::from(raw_cfg),
Err(e) => {
crate::warn::warn(format!("malformed [schema] in .hyalo.toml: {e}"));
SchemaConfig::default()
}
}
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
fn make_temp() -> TempDir {
tempfile::tempdir().expect("failed to create temp dir")
}
#[test]
fn missing_config_returns_defaults() {
let dir = make_temp();
let resolved = load_config_from(dir.path());
assert_eq!(resolved, ResolvedDefaults::hardcoded());
}
#[test]
fn valid_full_config() {
let dir = make_temp();
fs::write(
dir.path().join(".hyalo.toml"),
r#"
dir = "notes"
format = "text"
hints = true
"#,
)
.unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved.dir, PathBuf::from("notes"));
assert_eq!(resolved.format, "text");
assert!(resolved.hints);
assert_eq!(resolved.site_prefix, None);
}
#[test]
fn site_prefix_config() {
let dir = make_temp();
fs::write(
dir.path().join(".hyalo.toml"),
r#"dir = "docs"
site_prefix = "docs"
"#,
)
.unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved.dir, PathBuf::from("docs"));
assert_eq!(resolved.site_prefix, Some("docs".to_owned()));
}
#[test]
fn partial_config_merges_with_defaults() {
let dir = make_temp();
fs::write(dir.path().join(".hyalo.toml"), "hints = false\n").unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved.dir, PathBuf::from("."));
assert_eq!(resolved.format, "json");
assert!(
!resolved.hints,
"config should override the default (true) to false"
);
}
#[test]
fn malformed_toml_returns_defaults() {
let dir = make_temp();
fs::write(dir.path().join(".hyalo.toml"), "this is not { valid toml").unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved, ResolvedDefaults::hardcoded());
}
#[test]
fn unknown_fields_returns_defaults() {
let dir = make_temp();
fs::write(dir.path().join(".hyalo.toml"), "unknown_key = \"value\"\n").unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved, ResolvedDefaults::hardcoded());
}
#[test]
fn invalid_format_value_passed_through() {
let dir = make_temp();
fs::write(dir.path().join(".hyalo.toml"), "format = \"xml\"\n").unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved.format, "xml");
assert_eq!(resolved.dir, PathBuf::from("."));
assert!(resolved.hints);
}
#[test]
fn search_language_config() {
let dir = make_temp();
fs::write(
dir.path().join(".hyalo.toml"),
"[search]\nlanguage = \"french\"\n",
)
.unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved.search_language, Some("french".to_owned()));
}
#[test]
fn search_language_absent() {
let dir = make_temp();
fs::write(dir.path().join(".hyalo.toml"), "dir = \"notes\"\n").unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved.search_language, None);
}
#[test]
fn search_language_empty_section() {
let dir = make_temp();
fs::write(dir.path().join(".hyalo.toml"), "[search]\n").unwrap();
let resolved = load_config_from(dir.path());
assert_eq!(resolved.search_language, None);
}
}