use std::path::Path;
use super::{from_raw::from_raw, raw::RawConfig, CartularyConfig, CURRENT_SCHEMA_VERSION};
fn probe_schema_version(toml_text: &str) -> Option<u32> {
toml::from_str::<toml::Value>(toml_text)
.ok()
.and_then(|v| v.get("version")?.as_integer())
.and_then(|i| u32::try_from(i).ok())
}
pub fn load_config(path: &Path, root: &Path) -> CartularyConfig {
match try_load_config(path, root) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
}
}
pub fn try_load_config(path: &Path, root: &Path) -> Result<CartularyConfig, String> {
let raw_text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(_) => return Ok(CartularyConfig::default_for_root(root)),
};
let raw: RawConfig = toml::from_str(&raw_text).map_err(|e| {
let suffix = match probe_schema_version(&raw_text) {
Some(v) if v < CURRENT_SCHEMA_VERSION => format!(
"\nhint: workspace is at schema version {v}; \
run `cartu migrate` to bring it to v{CURRENT_SCHEMA_VERSION}."
),
_ => "\nhint: see `cartu man config` for the current schema.".to_string(),
};
format!("invalid {}: {e}{suffix}", path.display())
})?;
from_raw(raw, root).map_err(|e| format!("invalid {}: {e}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::status::{Status, StatusCategory};
use crate::domain::model::tag_descriptor::Cardinality;
use crate::infra::driven::fs::config::DocsKind;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn write_toml(dir: &Path, content: &str) -> PathBuf {
let path = dir.join("cartulary.toml");
fs::write(&path, content).unwrap();
path
}
#[test]
fn decision_kind_loads_without_statuses_section() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["adr"]
[decisions.adr]
dir = "docs/adr"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.decision_kinds.len(), 1);
assert_eq!(config.decision_kinds[0].kind, "adr");
}
#[test]
fn no_statuses_section_uses_default_issue_statuses() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[issues]\ndir = \"docs/issues\"\n");
let config = load_config(&path, tmp.path());
let open = config.issues_statuses.resolve("open").unwrap();
assert!(config.issues_statuses.contains(&open));
assert!(open.active);
}
#[test]
fn custom_decision_statuses_table_is_ignored() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["adr"]
[decisions.adr]
dir = "docs/adr"
[decisions.adr.statuses]
draft = { next = ["review"], active = true }
review = { next = ["accepted", "draft"], active = true }
accepted = { next = [], terminal = true }
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.decision_kinds.len(), 1);
assert_eq!(config.decision_kinds[0].kind, "adr");
}
#[test]
fn parses_custom_issue_statuses() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[issues.statuses]
todo = { next = ["doing"], active = true }
doing = { next = ["todo", "done"], active = true }
done = { next = [], terminal = true }
"#,
);
let config = load_config(&path, tmp.path());
let open = Status::new("open").unwrap();
let todo = config.issues_statuses.resolve("todo").unwrap();
let done = config.issues_statuses.resolve("done").unwrap();
assert!(config.issues_statuses.contains(&todo));
assert!(
!config.issues_statuses.contains(&open),
"open should not exist in custom config"
);
assert!(todo.active);
assert!(done.terminal);
}
#[test]
fn parses_status_label_from_toml() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[issues.statuses.in-progress]
next = ["open", "closed"]
active = true
label = "In Progress"
[issues.statuses.open]
next = ["in-progress", "closed"]
active = true
[issues.statuses.closed]
next = []
terminal = true
"#,
);
let config = load_config(&path, tmp.path());
let in_progress = config.issues_statuses.resolve("in-progress").unwrap();
let open = config.issues_statuses.resolve("open").unwrap();
assert_eq!(in_progress.label, "In Progress");
assert_eq!(open.label, "open");
}
#[test]
fn missing_file_returns_default() {
let tmp = TempDir::new().unwrap();
let config = load_config(Path::new("/tmp/nonexistent-cartulary.toml"), tmp.path());
assert_eq!(config, CartularyConfig::default_for_root(tmp.path()));
}
#[test]
fn malformed_file_is_a_hard_error() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "this is not valid toml ][");
let err = try_load_config(&path, tmp.path()).expect_err("expected hard error");
assert!(err.contains("invalid"), "got: {err}");
}
#[test]
fn unknown_top_level_field_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "issuse = \"oops\"\n");
let err = try_load_config(&path, tmp.path()).expect_err("expected hard error");
assert!(
err.contains("issuse"),
"expected key name in error, got: {err}"
);
}
#[test]
fn unknown_field_on_stale_version_hints_at_migrate() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
"version = 4\n[issues]\ndir = \"docs/issues\"\n\n[issues.tags.flow]\nlevels = [\"x\"]\n",
);
let err = try_load_config(&path, tmp.path()).expect_err("expected hard error");
assert!(err.contains("cartu migrate"), "got: {err}");
assert!(err.contains("version 4"), "got: {err}");
}
#[test]
fn unknown_field_on_current_version_hints_at_doc() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
&format!("version = {CURRENT_SCHEMA_VERSION}\n[issuse]\n"),
);
let err = try_load_config(&path, tmp.path()).expect_err("expected hard error");
assert!(err.contains("cartu man config"), "got: {err}");
assert!(!err.contains("cartu migrate"), "got: {err}");
}
#[test]
fn site_theme_absent_when_section_missing() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[issues]\ndir = \"docs/issues\"\n");
let config = load_config(&path, tmp.path());
assert_eq!(config.site_theme, None);
}
#[test]
fn site_theme_resolves_relative_to_root() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[site]
theme = "themes/my-theme"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.site_theme, Some(tmp.path().join("themes/my-theme")));
}
#[test]
fn site_out_resolves_relative_to_root() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[site]
out = "public"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.site_out, Some(tmp.path().join("public")));
}
#[test]
fn site_out_absent_when_section_missing() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[issues]\ndir = \"docs/issues\"\n");
let config = load_config(&path, tmp.path());
assert_eq!(config.site_out, None);
}
#[test]
fn site_unknown_field_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[site]\nthme = \"oops\"\n");
let err = try_load_config(&path, tmp.path()).expect_err("expected hard error");
assert!(
err.contains("thme"),
"expected key name in error, got: {err}"
);
}
#[test]
fn version_1_is_parsed_and_stored() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "version = 1\n[issues]\n");
let config = load_config(&path, tmp.path());
assert_eq!(config.schema_version, 1);
}
#[test]
fn absent_version_is_treated_as_zero() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[issues]\n");
let config = load_config(&path, tmp.path());
assert_eq!(config.schema_version, 0);
}
#[test]
fn parses_decision_kinds_with_explicit_dirs() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["adr", "ddr"]
[decisions.adr]
dir = "docs/adr"
[decisions.ddr]
dir = "docs/ddr"
[issues]
dir = "docs/issues"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.decision_kinds.len(), 2);
assert_eq!(config.decision_kinds[0].kind, "adr");
assert_eq!(config.decision_kinds[0].dir, tmp.path().join("docs/adr"));
assert_eq!(config.decision_kinds[0].id_prefix, None);
assert_eq!(config.decision_kinds[1].kind, "ddr");
assert_eq!(config.decision_kinds[1].dir, tmp.path().join("docs/ddr"));
assert_eq!(config.decision_kinds[1].id_prefix, None);
assert_eq!(config.issues_dir, tmp.path().join("docs/issues"));
}
#[test]
fn parses_decision_kinds_with_id_prefix() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["adr"]
[decisions.adr]
dir = "docs/adr"
id_prefix = "ADR-"
[issues]
dir = "docs/issues"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.decision_kinds.len(), 1);
assert_eq!(config.decision_kinds[0].kind, "adr");
assert_eq!(config.decision_kinds[0].dir, tmp.path().join("docs/adr"));
assert_eq!(config.decision_kinds[0].id_prefix, Some("ADR-".to_string()));
}
#[test]
fn falls_back_to_default_dir_when_kind_has_no_dir() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["gddr"]
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.decision_kinds[0].dir, tmp.path().join("docs/gddr"));
}
#[test]
fn missing_decisions_section_yields_no_kinds() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[issues]\ndir = \"docs/issues\"\n");
let config = load_config(&path, tmp.path());
assert!(config.decision_kinds.is_empty());
}
#[test]
fn preserves_kind_order_from_types_array() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["ddr", "adr", "gddr"]
"#,
);
let config = load_config(&path, tmp.path());
let kinds: Vec<&str> = config
.decision_kinds
.iter()
.map(|k| k.kind.as_str())
.collect();
assert_eq!(kinds, vec!["ddr", "adr", "gddr"]);
}
#[test]
fn preset_scrum_for_issues_is_resolved() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
preset = "scrum"
"#,
);
let config = load_config(&path, tmp.path());
assert!(config.issues_statuses.contains_name("to-do"));
assert!(config.issues_statuses.contains_name("done"));
assert!(!config.issues_statuses.contains_name("open"));
}
#[test]
fn preset_kanban_for_issues_is_resolved() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
preset = "kanban"
"#,
);
let config = load_config(&path, tmp.path());
assert!(config.issues_statuses.contains_name("backlog"));
assert!(config.issues_statuses.contains_name("blocked"));
}
#[test]
fn decision_preset_key_is_ignored() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["adr"]
[decisions.adr]
preset = "extended"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.decision_kinds.len(), 1);
assert_eq!(config.decision_kinds[0].kind, "adr");
}
#[test]
fn explicit_statuses_table_overrides_preset() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
preset = "scrum"
[issues.statuses]
open = { next = ["closed"], active = true }
closed = { next = [], terminal = true }
"#,
);
let config = load_config(&path, tmp.path());
assert!(config.issues_statuses.contains_name("open"));
assert!(!config.issues_statuses.contains_name("to-do"));
}
#[test]
fn unknown_preset_falls_back_to_default() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
preset = "no-such-preset"
"#,
);
let config = load_config(&path, tmp.path());
assert!(config.issues_statuses.contains_name("open"));
}
#[test]
fn parses_category_field_from_status_table() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[issues.statuses.open]
next = ["in-progress", "closed"]
active = true
category = "queued"
[issues.statuses.in-progress]
next = ["open", "closed"]
active = true
category = "active"
[issues.statuses.review]
next = ["in-progress", "closed"]
active = true
category = "stalled"
[issues.statuses.closed]
next = []
terminal = true
category = "resolved"
[issues.statuses.rejected]
next = []
terminal = true
category = "cancelled"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(
config.issues_statuses.resolve("open").unwrap().category,
StatusCategory::Queued
);
assert_eq!(
config
.issues_statuses
.resolve("in-progress")
.unwrap()
.category,
StatusCategory::Active
);
assert_eq!(
config.issues_statuses.resolve("review").unwrap().category,
StatusCategory::Stalled
);
assert_eq!(
config.issues_statuses.resolve("closed").unwrap().category,
StatusCategory::Resolved
);
assert_eq!(
config.issues_statuses.resolve("rejected").unwrap().category,
StatusCategory::Cancelled
);
}
#[test]
fn legacy_pending_token_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[issues.statuses.open]
next = ["closed"]
active = true
category = "pending"
[issues.statuses.closed]
next = []
terminal = true
category = "resolved"
"#,
);
let err = try_load_config(&path, tmp.path()).unwrap_err();
assert!(err.contains("\"pending\""), "got: {err}");
assert!(err.contains("queued"), "got: {err}");
assert!(err.contains("stalled"), "got: {err}");
}
#[test]
fn tag_descriptor_aggregate_max_without_ordered_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.priority]
levels = ["high", "medium", "low"]
aggregate = "max"
applies_to = ["issues"]
"#,
);
let err = try_load_config(&path, tmp.path()).unwrap_err();
assert!(err.contains("ordered = true"), "got: {err}");
assert!(err.contains("priority"), "got: {err}");
}
#[test]
fn tag_descriptor_unknown_aggregate_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.state]
levels = ["blocked"]
aggregate = "xor"
applies_to = ["issues"]
"#,
);
let err = try_load_config(&path, tmp.path()).unwrap_err();
assert!(err.contains("xor"), "got: {err}");
assert!(err.contains("or, and, max, min"), "got: {err}");
}
#[test]
fn legacy_ongoing_token_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[issues.statuses.in-progress]
next = ["closed"]
active = true
category = "ongoing"
[issues.statuses.closed]
next = []
terminal = true
category = "resolved"
"#,
);
let err = try_load_config(&path, tmp.path()).unwrap_err();
assert!(err.contains("\"ongoing\""), "got: {err}");
assert!(err.contains("\"active\""), "got: {err}");
}
#[test]
fn absent_category_field_yields_unknown() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[issues.statuses.open]
next = ["closed"]
active = true
[issues.statuses.closed]
next = []
terminal = true
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(
config.issues_statuses.resolve("open").unwrap().category,
StatusCategory::Unknown
);
}
#[test]
fn invalid_category_field_yields_unknown() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[issues.statuses.open]
next = ["closed"]
active = true
category = "not-a-valid-category"
[issues.statuses.closed]
next = []
terminal = true
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(
config.issues_statuses.resolve("open").unwrap().category,
StatusCategory::Unknown
);
}
#[test]
fn no_tags_section_yields_empty_descriptors() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[issues]\ndir = \"docs/issues\"\n");
let config = load_config(&path, tmp.path());
assert!(config.tag_descriptors_for("issues").is_empty());
}
#[test]
fn parses_flow_descriptor_with_exactly_one() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.flow]
levels = ["feature", "defect", "risk", "debt"]
cardinality = "exactly-one"
applies_to = ["issues"]
"#,
);
let config = load_config(&path, tmp.path());
let issue_descriptors = config.tag_descriptors_for("issues");
let flow = issue_descriptors.get("flow").unwrap();
assert_eq!(flow.levels, vec!["feature", "defect", "risk", "debt"]);
assert_eq!(flow.cardinality, Cardinality::ExactlyOne);
assert!(!flow.ordered);
assert!(flow.weights.is_none());
}
#[test]
fn parses_size_descriptor_with_weights_and_ordered() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.size]
levels = ["xs", "s", "m", "l", "xl"]
weights = [1, 2, 3, 5, 8]
cardinality = "at-most-one"
ordered = true
applies_to = ["issues"]
"#,
);
let config = load_config(&path, tmp.path());
let issue_descriptors = config.tag_descriptors_for("issues");
let size = issue_descriptors.get("size").unwrap();
assert_eq!(size.weights, Some(vec![1, 2, 3, 5, 8]));
assert!(size.ordered);
assert_eq!(size.weight_of("m"), Some(3));
}
#[test]
fn parses_multiple_descriptors() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.flow]
levels = ["feature", "defect"]
cardinality = "exactly-one"
applies_to = ["issues"]
[tags.level]
levels = ["epic", "feature", "story"]
ordered = true
applies_to = ["issues"]
"#,
);
let config = load_config(&path, tmp.path());
let issue_descriptors = config.tag_descriptors_for("issues");
assert_eq!(issue_descriptors.len(), 2);
assert!(issue_descriptors.get("flow").is_some());
assert!(issue_descriptors.get("level").is_some());
}
#[test]
fn weights_dropped_when_length_mismatches_levels() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.size]
levels = ["xs", "s", "m"]
weights = [1, 2]
applies_to = ["issues"]
"#,
);
let config = load_config(&path, tmp.path());
let issue_descriptors = config.tag_descriptors_for("issues");
let size = issue_descriptors.get("size").unwrap();
assert!(size.weights.is_none());
}
#[test]
fn unknown_cardinality_falls_back_to_any() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.area]
cardinality = "garbage"
applies_to = ["issues"]
"#,
);
let config = load_config(&path, tmp.path());
let issue_descriptors = config.tag_descriptors_for("issues");
let area = issue_descriptors.get("area").unwrap();
assert_eq!(area.cardinality, Cardinality::Any);
}
#[test]
fn open_vocabulary_descriptor_has_empty_levels() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[issues]
dir = "docs/issues"
[tags.area]
cardinality = "any"
applies_to = ["issues"]
"#,
);
let config = load_config(&path, tmp.path());
let issue_descriptors = config.tag_descriptors_for("issues");
let area = issue_descriptors.get("area").unwrap();
assert!(area.levels.is_empty());
assert!(area.accepts("backend"));
}
#[test]
fn source_status_map_is_parsed_when_present() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["adr"]
[decisions.adr]
dir = "docs/adr"
[issues]
dir = "docs/issues"
[sources.mygitlab]
type = "gitlab"
url = "https://gitlab.com"
project = "g/p"
token_env = "GITLAB_TOKEN"
[sources.mygitlab.status_map]
opened = "in-progress"
closed = "done"
"#,
);
let config = load_config(&path, tmp.path());
let src = &config.sources[0];
assert_eq!(
src.status_map.get("opened").map(String::as_str),
Some("in-progress")
);
assert_eq!(
src.status_map.get("closed").map(String::as_str),
Some("done")
);
}
#[test]
fn source_status_map_defaults_to_empty_when_absent() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[decisions]
types = ["adr"]
[decisions.adr]
dir = "docs/adr"
[issues]
dir = "docs/issues"
[sources.mygitlab]
type = "gitlab"
url = "https://gitlab.com"
project = "g/p"
token_env = "GITLAB_TOKEN"
"#,
);
let config = load_config(&path, tmp.path());
assert!(config.sources[0].status_map.is_empty());
}
#[test]
fn docs_section_absent_yields_empty_vec() {
let tmp = TempDir::new().unwrap();
let path = write_toml(tmp.path(), "[issues]\ndir = \"docs/issues\"\n");
let config = load_config(&path, tmp.path());
assert!(config.docs.is_empty());
}
#[test]
fn docs_entry_with_all_fields_loads() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[docs.pages]
type = "markdown"
source = "docs/pages"
publish = "/pages"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.docs.len(), 1);
let entry = &config.docs[0];
assert_eq!(entry.name, "pages");
assert_eq!(entry.kind, DocsKind::Markdown);
assert_eq!(entry.source, tmp.path().join("docs/pages"));
assert_eq!(entry.publish, "/pages");
}
#[test]
fn docs_entry_missing_type_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[docs.pages]
source = "docs/pages"
publish = "/pages"
"#,
);
let err = try_load_config(&path, tmp.path()).expect_err("expected error");
assert!(err.contains("type"), "got: {err}");
}
#[test]
fn docs_entry_missing_source_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[docs.pages]
type = "markdown"
publish = "/pages"
"#,
);
let err = try_load_config(&path, tmp.path()).expect_err("expected error");
assert!(err.contains("source"), "got: {err}");
}
#[test]
fn docs_entry_missing_publish_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[docs.pages]
type = "markdown"
source = "docs/pages"
"#,
);
let err = try_load_config(&path, tmp.path()).expect_err("expected error");
assert!(err.contains("publish"), "got: {err}");
}
#[test]
fn docs_entry_with_unknown_type_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[docs.pages]
type = "html"
source = "docs/pages"
publish = "/pages"
"#,
);
let err = try_load_config(&path, tmp.path()).expect_err("expected error");
assert!(err.contains("markdown"), "got: {err}");
assert!(err.contains("pages"), "got: {err}");
}
#[test]
fn docs_entry_with_unknown_field_is_rejected() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[docs.pages]
type = "markdown"
source = "docs/pages"
publish = "/pages"
extra = "nope"
"#,
);
let err = try_load_config(&path, tmp.path()).expect_err("expected error");
assert!(err.contains("extra"), "got: {err}");
}
#[test]
fn multiple_docs_entries_load() {
let tmp = TempDir::new().unwrap();
let path = write_toml(
tmp.path(),
r#"
[docs.guides]
type = "markdown"
source = "docs/guides"
publish = "/guides"
[docs.reference]
type = "markdown"
source = "docs/reference"
publish = "/reference"
"#,
);
let config = load_config(&path, tmp.path());
assert_eq!(config.docs.len(), 2);
let names: Vec<&str> = config.docs.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"guides"));
assert!(names.contains(&"reference"));
}
}