use serde_json::Value;
use crate::settings::FileConfig;
pub const SCHEMA_URL: &str =
"https://raw.githubusercontent.com/nickderobertis/crozier/main/assets/crozier.schema.json";
pub const BUNDLED: &str = include_str!("../assets/crozier.schema.json");
pub const COMPARE_REPORT_SCHEMA_URL: &str =
"https://raw.githubusercontent.com/nickderobertis/crozier/main/assets/compare-report.schema.json";
pub const COMPARE_REPORT_BUNDLED: &str = include_str!("../assets/compare-report.schema.json");
#[must_use]
pub fn compare_report() -> Value {
let mut schema = serde_json::to_value(schemars::schema_for!(crate::compare::report::Report))
.expect("report schema serializes");
if let Value::Object(map) = &mut schema {
map.insert(
"$id".to_string(),
Value::String(COMPARE_REPORT_SCHEMA_URL.to_string()),
);
map.insert(
"title".to_string(),
Value::String("crozier compare report".to_string()),
);
}
schema
}
#[cfg(test)]
fn assert_asset_matches(relative: &str, committed: &str, generated: &Value) {
let committed: Value = serde_json::from_str(committed).expect("bundled schema is valid JSON");
if &committed != generated {
if std::env::var_os("CROZIER_UPDATE_SCHEMA").is_some() {
let path = format!("{}/{relative}", env!("CARGO_MANIFEST_DIR"));
let mut body = serde_json::to_string_pretty(generated).unwrap();
body.push('\n');
std::fs::write(path, body).unwrap();
return;
}
panic!(
"{relative} is out of sync with its Rust types; \
regenerate with `CROZIER_UPDATE_SCHEMA=1 cargo test --lib schema`"
);
}
}
#[must_use]
pub fn modeline() -> String {
format!("# yaml-language-server: $schema={SCHEMA_URL}\n")
}
#[must_use]
pub fn build() -> Value {
let mut schema =
serde_json::to_value(schemars::schema_for!(FileConfig)).expect("config schema serializes");
if let Value::Object(map) = &mut schema {
map.insert("$id".to_string(), Value::String(SCHEMA_URL.to_string()));
map.insert(
"title".to_string(),
Value::String("crozier configuration".to_string()),
);
}
schema
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_describes_the_config_shape() {
let s = build();
assert_eq!(s["$id"], SCHEMA_URL);
assert_eq!(s["type"], "object");
let props = s["properties"].as_object().unwrap();
for key in [
"spec",
"output",
"package-name",
"audiences",
"layout",
"generators",
] {
assert!(props.contains_key(key), "missing property {key}");
}
assert_eq!(s["properties"]["generators"]["type"], "object");
}
#[test]
fn modeline_targets_the_published_schema() {
let line = modeline();
assert!(line.starts_with("# yaml-language-server: $schema="));
assert!(line.contains(SCHEMA_URL));
assert!(line.ends_with('\n'));
}
#[test]
fn committed_asset_matches_generated_schema() {
assert_asset_matches("assets/crozier.schema.json", BUNDLED, &build());
}
#[test]
fn committed_compare_report_schema_matches_its_types() {
assert_asset_matches(
"assets/compare-report.schema.json",
COMPARE_REPORT_BUNDLED,
&compare_report(),
);
}
#[test]
fn compare_report_schema_states_the_contract() {
let s = compare_report();
assert_eq!(s["$id"], COMPARE_REPORT_SCHEMA_URL);
let required: Vec<&str> = s["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
for key in [
"schema_version",
"crozier_version",
"searched_paths",
"exit_code",
"counts",
"timing_totals",
"results",
] {
assert!(required.contains(&key), "{key} not required: {required:?}");
}
assert_eq!(s["properties"]["schema_version"]["const"], 2);
assert_eq!(
s["properties"]["exit_code"]["enum"],
serde_json::json!([0, 3, 4])
);
let text = s.to_string();
assert!(!text.to_lowercase().contains("fern"), "{text}");
}
}