use serde_json::{Value, json};
use crate::syntax::Spec;
pub const SCHEMA_URL: &str = "https://www.isoloom.com/schema/v1.json";
pub const MODELINE: &str = "# yaml-language-server: $schema=https://www.isoloom.com/schema/v1.json";
#[must_use]
pub fn schema() -> Value {
let mut s = Value::from(schemars::schema_for!(Spec));
drop_null(&mut s);
set(&mut s, &[], "$id", json!(SCHEMA_URL));
set(&mut s, &[], "title", json!("isoloom.yml"));
set(
&mut s,
&[],
"description",
json!("An Isoloom environment spec, version 1: https://www.isoloom.com/en/docs/spec-reference"),
);
names(&mut s);
networks(&mut s);
machines(&mut s);
s
}
fn set(s: &mut Value, path: &[&str], key: &str, v: Value) {
let node = path.iter().try_fold(s, |node, p| node.get_mut(*p));
if let Some(Value::Object(node)) = node {
node.insert(key.to_owned(), v);
}
}
fn names(s: &mut Value) {
let kebab = "^[a-z][a-z0-9]*(-[a-z0-9]+)*$";
set(s, &["properties", "name"], "pattern", json!("^[a-z0-9]+(-[a-z0-9]+)*$"));
set(s, &["properties", "networks"], "propertyNames", json!({ "pattern": kebab }));
set(s, &["properties", "machines"], "propertyNames", json!({ "pattern": kebab }));
}
fn networks(s: &mut Value) {
let private = r"^(10\.\d{1,3}|172\.(1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}";
let network = format!("{private}/(2[4-9])$");
set(
s,
&["$defs", "Network", "properties", "cidr"],
"pattern",
json!(format!(r"{private}/([89]|1\d|2[0-9])$")),
);
set(s, &["$defs", "Network"], "if", json!({ "not": { "required": ["vlans"] } }));
set(s, &["$defs", "Network"], "then", json!({ "properties": { "cidr": { "pattern": network } } }));
set(
s,
&["$defs", "Network", "properties", "vlans"],
"propertyNames",
json!({ "pattern": "^[1-9][0-9]{0,3}$" }),
);
set(s, &["$defs", "Vlan", "properties", "cidr"], "pattern", json!(network));
set(s, &["$defs", "Bridged", "properties", "vlan"], "minimum", json!(1));
set(s, &["$defs", "Bridged", "properties", "vlan"], "maximum", json!(4094));
set(
s,
&["$defs", "DockerNetwork", "properties", "cidr"],
"pattern",
json!(r"^10\.\d{1,3}\.\d{1,3}\.\d{1,3}/(2[4-9])$"),
);
}
fn machines(s: &mut Value) {
set(s, &["$defs", "MountPart", "properties", "path"], "pattern", json!("^/"));
}
fn drop_null(v: &mut Value) {
match v {
Value::Object(map) => {
if let Some(Value::Array(any)) = map.get("anyOf")
&& any.len() == 2
&& any.iter().any(|b| b.get("type") == Some(&json!("null")))
&& let Some(keep) = any.iter().find(|b| b.get("type") != Some(&json!("null"))).cloned()
{
map.remove("anyOf");
if let Value::Object(k) = keep {
for (key, val) in k {
map.entry(key).or_insert(val);
}
}
}
if let Some(Value::Array(types)) = map.get("type")
&& types.len() == 2
&& types.contains(&json!("null"))
&& let Some(t) = types.iter().find(|t| **t != json!("null")).cloned()
{
map.insert("type".into(), t);
}
if map.get("default") == Some(&Value::Null) {
map.remove("default");
}
for val in map.values_mut() {
drop_null(val);
}
}
Value::Array(items) => items.iter_mut().for_each(drop_null),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn null_is_dropped_from_every_alternative_inside_a_list() {
let mut v = json!({ "anyOf": [{ "type": ["string", "null"] }, { "type": "integer" }, { "type": "boolean" }], "default": null });
drop_null(&mut v);
assert_eq!(v, json!({ "anyOf": [{ "type": "string" }, { "type": "integer" }, { "type": "boolean" }] }));
}
#[test]
fn an_optional_field_keeps_only_its_real_type() {
let mut v = json!({ "description": "d", "anyOf": [{ "$ref": "#/$defs/X" }, { "type": "null" }] });
drop_null(&mut v);
assert_eq!(v, json!({ "description": "d", "$ref": "#/$defs/X" }));
}
}