#![expect(
dead_code,
reason = "schema marker types are consumed by schemars derive"
)]
use std::collections::BTreeMap;
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::Serialize;
#[derive(JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
struct TreebootConfig {
#[serde(skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
default_ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
dangerously_allow_sources_outside_root: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
dangerously_allow_targets_outside_worktree: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
copy: Option<Vec<CopyEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
symlink: Option<Vec<SymlinkEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
sync: Option<Vec<SyncEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
files: Option<Vec<MixedFileObject>>,
#[serde(skip_serializing_if = "Option::is_none")]
file: Option<Vec<MixedFileObject>>,
#[serde(skip_serializing_if = "Option::is_none")]
commands: Option<Vec<CommandEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
command: Option<Vec<CommandObject>>,
}
#[derive(JsonSchema, Serialize)]
#[serde(untagged)]
enum CopyEntry {
Path(String),
Object(CopyObject),
}
#[derive(JsonSchema, Serialize)]
#[serde(untagged)]
enum SymlinkEntry {
Path(String),
Object(SymlinkObject),
}
#[derive(JsonSchema, Serialize)]
#[serde(untagged)]
enum SyncEntry {
Path(String),
Object(SyncObject),
}
#[derive(JsonSchema, Serialize)]
#[serde(untagged)]
enum CommandEntry {
Run(String),
Object(CommandObject),
}
#[derive(JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
struct CopyObject {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
symlinks: Option<SymlinkMode>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore_metadata: Option<Vec<MetadataField>>,
}
#[derive(JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
struct SymlinkObject {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<bool>,
}
#[derive(JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
struct SyncObject {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
compare: Option<SyncCompare>,
#[serde(skip_serializing_if = "Option::is_none")]
delete: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
symlinks: Option<SymlinkMode>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore_metadata: Option<Vec<MetadataField>>,
}
#[derive(JsonSchema, Serialize)]
#[serde(tag = "operation", rename_all = "snake_case", deny_unknown_fields)]
enum MixedFileObject {
Copy {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
symlinks: Option<SymlinkMode>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore_metadata: Option<Vec<MetadataField>>,
},
Symlink {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<bool>,
},
Sync {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
compare: Option<SyncCompare>,
#[serde(skip_serializing_if = "Option::is_none")]
delete: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
symlinks: Option<SymlinkMode>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
ignore_metadata: Option<Vec<MetadataField>>,
},
}
#[derive(JsonSchema, Serialize)]
#[serde(rename_all = "snake_case")]
enum SyncCompare {
Metadata,
Checksum,
}
#[derive(JsonSchema, Serialize)]
#[serde(rename_all = "snake_case")]
enum SymlinkMode {
Preserve,
}
#[derive(JsonSchema, Serialize)]
#[serde(rename_all = "snake_case")]
enum MetadataField {
Permissions,
Owner,
Group,
Ownership,
}
#[derive(JsonSchema, Serialize)]
#[serde(untagged)]
enum CommandObject {
Shell(ShellCommandObject),
Direct(DirectCommandObject),
}
#[derive(JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
struct ShellCommandObject {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
run: String,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
allow_failure: Option<bool>,
}
#[derive(JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
struct DirectCommandObject {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
program: String,
#[serde(skip_serializing_if = "Option::is_none")]
args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
allow_failure: Option<bool>,
}
fn main() {
let path = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("schemas/treeboot.schema.json"));
let schema = schemars::schema_for!(TreebootConfig);
let mut schema = serde_json::to_value(schema).expect("schema should serialize as JSON");
strip_null_type(&mut schema);
let content = serde_json::to_string_pretty(&schema).expect("schema should serialize as JSON");
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("schema parent directory should be created");
}
std::fs::write(&path, format!("{content}\n")).expect("schema should be written");
}
fn strip_null_type(value: &mut serde_json::Value) {
match value {
serde_json::Value::Array(items) => {
for item in items {
strip_null_type(item);
}
}
serde_json::Value::Object(object) => {
strip_null_type_array(object);
strip_null_any_of(object);
for value in object.values_mut() {
strip_null_type(value);
}
}
_ => {}
}
}
fn strip_null_type_array(object: &mut serde_json::Map<String, serde_json::Value>) {
let Some(serde_json::Value::Array(types)) = object.get_mut("type") else {
return;
};
types.retain(|item| item.as_str() != Some("null"));
if types.len() == 1 {
let only = types.pop().expect("single schema type should exist");
object.insert("type".to_owned(), only);
}
}
fn strip_null_any_of(object: &mut serde_json::Map<String, serde_json::Value>) {
let Some(serde_json::Value::Array(any_of)) = object.get_mut("anyOf") else {
return;
};
any_of.retain(|item| {
!matches!(
item,
serde_json::Value::Object(schema)
if schema.get("type").and_then(serde_json::Value::as_str) == Some("null")
)
});
if any_of.len() == 1 {
let only = any_of.pop().expect("single anyOf schema should exist");
if let serde_json::Value::Object(only) = only {
object.remove("anyOf");
object.extend(only);
}
}
}