use std::fs;
use std::path::{Path, PathBuf};
use jsonschema::Validator;
use schemars::generate::SchemaSettings;
use schemars::transform::RestrictFormats;
use serde_json::Value;
use super::RawConfig;
const SCHEMA_ID: &str = "https://fatou.dev/fatou.schema.json";
const SCHEMA_PATH: &str = "fatou.schema.json";
fn generate_schema_json() -> Value {
let generator = SchemaSettings::draft07()
.with_transform(RestrictFormats::default())
.into_generator();
let schema = generator.into_root_schema_for::<RawConfig>();
let mut json = serde_json::to_value(schema).expect("serialize configuration schema");
let Value::Object(root) = &mut json else {
panic!("root configuration schema must be an object");
};
root.insert("$id".into(), SCHEMA_ID.into());
root.insert("title".into(), "Fatou configuration".into());
root.insert(
"description".into(),
"Schema for fatou.toml. Generated from Fatou's configuration types; do not hand-edit—run `UPDATE_EXPECTED=1 cargo test config_schema` instead."
.into(),
);
json
}
fn schema_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join(SCHEMA_PATH)
}
fn render_schema(schema: &Value) -> String {
let mut rendered = serde_json::to_string_pretty(schema).expect("render schema");
rendered.push('\n');
rendered
}
fn validator() -> Validator {
Validator::new(&generate_schema_json()).expect("compile generated configuration schema")
}
fn toml_to_json(source: &str) -> Value {
let value: toml::Value = toml::from_str(source).expect("parse test configuration as TOML");
serde_json::to_value(value).expect("convert test configuration to JSON")
}
fn assert_accepts(source: &str) {
toml::from_str::<RawConfig>(source).expect("runtime configuration should accept fixture");
let value = toml_to_json(source);
let errors: Vec<_> = validator()
.iter_errors(&value)
.map(|error| format!("{error} at {}", error.instance_path()))
.collect();
assert!(
errors.is_empty(),
"schema rejected a runtime-valid configuration:\n{}",
errors.join("\n")
);
}
fn assert_rejects(source: &str) {
toml::from_str::<RawConfig>(source).expect_err("runtime configuration should reject fixture");
let value = toml_to_json(source);
assert!(
validator().iter_errors(&value).next().is_some(),
"schema accepted a runtime-invalid configuration: {source}"
);
}
#[test]
fn config_schema_is_in_sync() {
let generated = render_schema(&generate_schema_json());
let path = schema_path();
if std::env::var_os("UPDATE_EXPECTED").is_some() {
fs::write(&path, generated).expect("write generated configuration schema");
return;
}
let checked_in = fs::read_to_string(&path).unwrap_or_else(|error| {
panic!(
"missing {}: {error}. Run `UPDATE_EXPECTED=1 cargo test config_schema` to create it.",
path.display()
)
});
assert_eq!(
checked_in,
generated,
"{} is out of date; run `UPDATE_EXPECTED=1 cargo test config_schema`",
path.display()
);
}
#[test]
fn config_schema_uses_the_public_draft_7_identity() {
let schema = generate_schema_json();
assert_eq!(schema["$id"], SCHEMA_ID);
assert_eq!(schema["$schema"], "http://json-schema.org/draft-07/schema#");
assert!(
!render_schema(&schema).contains("\"format\": \"uint32\""),
"schema must not expose Rust-specific formats"
);
validator();
}
#[test]
fn config_schema_accepts_supported_configuration() {
assert_accepts("");
assert_accepts(
r#"
exclude = ["vendored/"]
extend-exclude = ["generated/"]
[format]
line-width = 100
indent-width = 2
line-ending = "native"
[lint]
select = ["future-rule"]
extend-select = ["undefined-name", "another-future-rule"]
ignore = ["unused-binding"]
[lint.severity]
one = "error"
two = "warning"
three = "info"
four = "hint"
[lint.rules.discouraged-function]
functions = { sleep = "use a timer" }
extend-functions = { exit = "return an error" }
[julia]
version = "1.6, 1.10 - 1.11"
"#,
);
assert_accepts("[format]\nline_width = 100\nindent_width = 2\n");
for line_ending in ["auto", "lf", "crlf", "native"] {
assert_accepts(&format!("[format]\nline-ending = \"{line_ending}\"\n"));
}
assert_accepts("[julia]\nversion = \"not a version\"\n");
}
#[test]
fn config_schema_rejects_invalid_configuration() {
for source in [
"[project]\nentry-point = []\n",
"[project]\nentry-points = [1]\n",
"[project]\nentry-points = \"main.jl\"\n",
"unknown = true\n",
"[format]\nline-width = \"wide\"\n",
"[format]\nline-ending = \"mac\"\n",
"[lint]\nunknown = true\n",
"[lint]\nextend-select = \"undefined-name\"\n",
"[lint]\nextend-select = [1]\n",
"[lint]\nextend_select = []\n",
"[lint.severity]\nunused-binding = \"fatal\"\n",
"[lint.rules.unknown-rule]\n",
"[lint.rules.discouraged-function]\nfuncs = {}\n",
"[julia]\ntarget = \"1.6\"\n",
] {
assert_rejects(source);
}
}
#[test]
fn config_schema_accepts_script_entries() {
assert_accepts("[project]\nentry-points = [\"scripts/main.jl\", \"other.jl\"]\n");
assert_accepts("[project]\nentry-points = []\n");
}