use schemars::Schema;
use super::raw::RawFlockfile;
pub const COMMITTED: &str = include_str!("../../../assets/flockfile.schema.json");
#[allow(dead_code)]
const REGENERATE: &str =
"cargo run --bin shep -- schema > crates/shep-core/assets/flockfile.schema.json";
#[track_caller]
#[must_use]
pub fn flockfile_schema_string() -> String {
let schema = flockfile_schema_json();
let mut rendered =
serde_json::to_string_pretty(&schema).expect("a schemars Schema always serializes");
rendered.push('\n');
rendered
}
#[track_caller]
#[must_use]
pub fn flockfile_schema_json() -> Schema {
schemars::schema_for!(RawFlockfile)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "schema")]
fn resolved<'a>(
root: &'a serde_json::Value,
node: &'a serde_json::Value,
) -> &'a serde_json::Value {
match node.get("$ref").and_then(serde_json::Value::as_str) {
Some(r) => {
let name = r
.strip_prefix("#/$defs/")
.expect("every $ref in this schema points into $defs");
&root["$defs"][name]
}
None => node,
}
}
#[cfg(feature = "schema")]
#[test]
fn the_committed_schema_is_current() {
assert_eq!(
flockfile_schema_string(),
COMMITTED,
"crates/shep-core/assets/flockfile.schema.json is stale. Regenerate it:\n {REGENERATE}\n\
A doc-comment edit on AppConfig counts; schemars puts doc comments \
into `description`."
);
}
#[cfg(feature = "schema")]
#[test]
fn the_schema_describes_a_document_not_one_app() {
let schema: serde_json::Value = serde_json::from_str(&flockfile_schema_string()).unwrap();
assert!(schema["properties"]["app"].is_object(), "{schema}");
assert_eq!(schema["properties"]["app"]["type"], "array", "{schema}");
assert!(
schema["properties"]["name"].is_null(),
"root must not be an app: {schema}"
);
assert!(schema["$defs"]["AppConfig"].is_object(), "{schema}");
}
#[cfg(feature = "schema")]
#[test]
fn kill_signal_stays_an_unconstrained_string() {
let schema: serde_json::Value = serde_json::from_str(&flockfile_schema_string()).unwrap();
let field = resolved(
&schema,
&schema["$defs"]["AppConfig"]["properties"]["kill_signal"],
);
let types = field["type"]
.as_array()
.unwrap_or_else(|| panic!("kill_signal must carry a type array: {field}"));
assert!(
types.iter().any(|t| t == "string"),
"kill_signal must accept a string: {field}"
);
assert!(
field.get("enum").is_none(),
"kill_signal must not become an enum of the four signal names: {field}"
);
assert!(
field.get("pattern").is_none(),
"kill_signal must not become pattern-constrained: {field}"
);
}
#[cfg(feature = "schema")]
#[test]
fn duration_and_memory_fields_are_string_shaped() {
let schema: serde_json::Value = serde_json::from_str(&flockfile_schema_string()).unwrap();
let app = &schema["$defs"]["AppConfig"]["properties"];
let min_uptime = resolved(&schema, &app["min_uptime"]);
assert_eq!(min_uptime["type"], "string", "{min_uptime}");
assert_eq!(min_uptime["pattern"], r"^\d+(ms|h|m|s)?$", "{min_uptime}");
let any_of = app["max_memory"]["anyOf"]
.as_array()
.unwrap_or_else(|| panic!("max_memory must be anyOf: {}", app["max_memory"]));
let ref_node = any_of
.iter()
.find(|v| v.get("$ref").is_some())
.unwrap_or_else(|| panic!("max_memory's anyOf must carry a $ref: {any_of:?}"));
let max_memory = resolved(&schema, ref_node);
assert_eq!(max_memory["type"], "string", "{max_memory}");
assert_eq!(max_memory["pattern"], r"^\d+(G|M|K)?$", "{max_memory}");
}
}