1pub mod config;
19pub mod md;
20pub mod validate;
21
22pub use config::*;
23pub use md::{parse_md, render_md};
24pub use validate::{validate, Issue, Severity, ValidationReport};
25
26use std::path::Path;
27
28#[derive(Debug, thiserror::Error)]
29pub enum CoreError {
30 #[error("io error reading {path}: {source}")]
31 Io {
32 path: String,
33 #[source]
34 source: std::io::Error,
35 },
36 #[error("could not parse {path} as YAML or JSON:\n yaml: {yaml}\n json: {json}")]
37 Parse {
38 path: String,
39 yaml: String,
40 json: String,
41 },
42 #[error("config is invalid:\n{0}")]
43 Invalid(String),
44}
45
46pub fn load(path: impl AsRef<Path>) -> Result<LoopConfig, CoreError> {
53 let path = path.as_ref();
54 let text = std::fs::read_to_string(path).map_err(|source| CoreError::Io {
55 path: path.display().to_string(),
56 source,
57 })?;
58 let origin = path.display().to_string();
59 if is_markdown(path) {
60 return md::parse_md(&text, &origin);
61 }
62 parse_str(&text, &origin)
63}
64
65pub fn is_markdown(path: &Path) -> bool {
67 path.extension()
68 .and_then(|e| e.to_str())
69 .map(|e| e.eq_ignore_ascii_case("md") || e.eq_ignore_ascii_case("markdown"))
70 .unwrap_or(false)
71}
72
73pub fn parse_str(text: &str, origin: &str) -> Result<LoopConfig, CoreError> {
76 let yaml_err = match serde_yaml::from_str::<LoopConfig>(text) {
77 Ok(cfg) => return Ok(cfg),
78 Err(e) => e.to_string(),
79 };
80 let json_err = match serde_json::from_str::<LoopConfig>(text) {
81 Ok(cfg) => return Ok(cfg),
82 Err(e) => e.to_string(),
83 };
84 Err(CoreError::Parse {
85 path: origin.to_string(),
86 yaml: yaml_err,
87 json: json_err,
88 })
89}
90
91pub fn load_validated(path: impl AsRef<Path>) -> Result<LoopConfig, CoreError> {
93 let cfg = load(path)?;
94 let report = validate(&cfg);
95 if report.has_errors() {
96 return Err(CoreError::Invalid(report.render()));
97 }
98 Ok(cfg)
99}