use serde_json::Value;
use std::env;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
enum SchemaType {
String,
Boolean,
Integer,
Number,
Array,
Object,
Unknown,
}
impl SchemaType {
fn as_str(&self) -> &'static str {
match self {
SchemaType::String => "string",
SchemaType::Boolean => "boolean",
SchemaType::Integer => "integer",
SchemaType::Number => "number",
SchemaType::Array => "array",
SchemaType::Object => "object",
SchemaType::Unknown => "unknown",
}
}
}
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("schema_sections.rs");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let local_schema = manifest_dir.join("mise.json");
let repo_schema = manifest_dir
.parent()
.unwrap()
.parent()
.unwrap()
.join("schema/mise.json");
let schema_path = if local_schema.exists() {
local_schema
} else {
repo_schema
};
println!("cargo:rerun-if-changed={}", schema_path.display());
let schema_content = fs::read_to_string(&schema_path)
.unwrap_or_else(|e| panic!("Failed to read schema at {}: {}", schema_path.display(), e));
let schema: Value = serde_json::from_str(&schema_content)
.unwrap_or_else(|e| panic!("Failed to parse schema JSON: {}", e));
let defs = schema.get("$defs");
let mut sections = Vec::new();
let mut entries = Vec::new();
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
for (name, prop) in properties {
if prop
.get("deprecated")
.and_then(|d| d.as_bool())
.unwrap_or(false)
{
continue;
}
if name == "_" {
continue;
}
let description = prop
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("");
let is_section = is_section_property(prop, defs);
if is_section {
sections.push((name.clone(), description.to_string()));
} else {
let schema_type = get_schema_type(prop, defs);
entries.push((name.clone(), description.to_string(), schema_type));
}
}
}
let mut settings_keys = Vec::new(); if let Some(settings_def) = defs.and_then(|d| d.get("settings")) {
extract_settings_keys(settings_def, defs, "", &mut settings_keys);
}
let mut task_config_keys = Vec::new();
if let Some(task_config_def) = defs.and_then(|d| d.get("task_config")) {
extract_simple_properties(task_config_def, defs, &mut task_config_keys);
}
let mut monorepo_keys = Vec::new();
if let Some(monorepo_def) = defs.and_then(|d| d.get("monorepo")) {
extract_simple_properties(monorepo_def, defs, &mut monorepo_keys);
}
sections.sort_by(|a, b| a.0.cmp(&b.0));
entries.sort_by(|a, b| a.0.cmp(&b.0));
settings_keys.sort_by(|a, b| a.0.cmp(&b.0));
task_config_keys.sort_by(|a, b| a.0.cmp(&b.0));
monorepo_keys.sort_by(|a, b| a.0.cmp(&b.0));
let mut code = String::new();
code.push_str("/// Type of a schema property\n");
code.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n");
code.push_str("pub enum SchemaType {\n");
code.push_str(" String,\n");
code.push_str(" Boolean,\n");
code.push_str(" Integer,\n");
code.push_str(" Number,\n");
code.push_str(" Array,\n");
code.push_str(" Object,\n");
code.push_str(" Unknown,\n");
code.push_str("}\n\n");
code.push_str("/// Valid top-level sections in mise.toml (tables with user-defined keys)\n");
code.push_str("pub const SCHEMA_SECTIONS: &[(&str, &str)] = &[\n");
for (name, description) in §ions {
let escaped_desc = escape_string(description);
code.push_str(&format!(" (\"{}\", \"{}\"),\n", name, escaped_desc));
}
code.push_str("];\n\n");
code.push_str(
"/// Valid top-level entries in mise.toml (scalar values, not sections) with type info\n",
);
code.push_str("pub const SCHEMA_ENTRIES: &[(&str, &str, SchemaType)] = &[\n");
for (name, description, schema_type) in &entries {
let escaped_desc = escape_string(description);
code.push_str(&format!(
" (\"{}\", \"{}\", SchemaType::{}),\n",
name,
escaped_desc,
capitalize_first(schema_type.as_str())
));
}
code.push_str("];\n\n");
code.push_str("/// Valid settings keys in mise.toml [settings] section with type info\n");
code.push_str("pub const SCHEMA_SETTINGS: &[(&str, &str, SchemaType)] = &[\n");
for (name, description, schema_type) in &settings_keys {
let escaped_desc = escape_string(description);
code.push_str(&format!(
" (\"{}\", \"{}\", SchemaType::{}),\n",
name,
escaped_desc,
capitalize_first(schema_type.as_str())
));
}
code.push_str("];\n\n");
code.push_str("/// Common hook names in mise.toml [hooks] section\n");
code.push_str("pub const SCHEMA_HOOKS: &[(&str, &str)] = &[\n");
code.push_str(" (\"enter\", \"Run when entering a directory with this mise.toml\"),\n");
code.push_str(" (\"leave\", \"Run when leaving a directory with this mise.toml\"),\n");
code.push_str(" (\"cd\", \"Run on any directory change\"),\n");
code.push_str(" (\"preinstall\", \"Run before installing a tool\"),\n");
code.push_str(" (\"postinstall\", \"Run after installing a tool\"),\n");
code.push_str("];\n\n");
code.push_str("/// Valid keys in mise.toml [task_config] section with type info\n");
code.push_str("pub const SCHEMA_TASK_CONFIG: &[(&str, &str, SchemaType)] = &[\n");
for (name, description, schema_type) in &task_config_keys {
let escaped_desc = escape_string(description);
code.push_str(&format!(
" (\"{}\", \"{}\", SchemaType::{}),\n",
name,
escaped_desc,
capitalize_first(schema_type.as_str())
));
}
code.push_str("];\n\n");
code.push_str("/// Valid keys in mise.toml [monorepo] section with type info\n");
code.push_str("pub const SCHEMA_MONOREPO: &[(&str, &str, SchemaType)] = &[\n");
for (name, description, schema_type) in &monorepo_keys {
let escaped_desc = escape_string(description);
code.push_str(&format!(
" (\"{}\", \"{}\", SchemaType::{}),\n",
name,
escaped_desc,
capitalize_first(schema_type.as_str())
));
}
code.push_str("];\n");
fs::write(&dest_path, code).unwrap();
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
fn get_schema_type(prop: &Value, defs: Option<&Value>) -> SchemaType {
if let Some(type_val) = prop.get("type").and_then(|t| t.as_str()) {
return match type_val {
"string" => SchemaType::String,
"boolean" => SchemaType::Boolean,
"integer" => SchemaType::Integer,
"number" => SchemaType::Number,
"array" => SchemaType::Array,
"object" => SchemaType::Object,
_ => SchemaType::Unknown,
};
}
if let Some(ref_val) = prop.get("$ref").and_then(|r| r.as_str())
&& let Some(def_name) = ref_val.strip_prefix("#/$defs/")
&& let Some(def) = defs.and_then(|d| d.get(def_name))
{
return get_schema_type(def, defs);
}
if let Some(one_of) = prop.get("oneOf").and_then(|o| o.as_array()) {
for option in one_of {
let t = get_schema_type(option, defs);
if t != SchemaType::Unknown && t != SchemaType::Object {
return t;
}
}
}
SchemaType::Unknown
}
fn extract_simple_properties(
prop: &Value,
defs: Option<&Value>,
keys: &mut Vec<(String, String, SchemaType)>,
) {
if let Some(properties) = prop.get("properties").and_then(|p| p.as_object()) {
for (name, prop_value) in properties {
if prop_value
.get("deprecated")
.and_then(|d| d.as_bool())
.unwrap_or(false)
{
continue;
}
let description = prop_value
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("");
let schema_type = get_schema_type(prop_value, defs);
keys.push((name.clone(), description.to_string(), schema_type));
}
}
}
fn extract_settings_keys(
prop: &Value,
defs: Option<&Value>,
prefix: &str,
keys: &mut Vec<(String, String, SchemaType)>,
) {
if let Some(properties) = prop.get("properties").and_then(|p| p.as_object()) {
for (name, prop_value) in properties {
if prop_value
.get("deprecated")
.and_then(|d| d.as_bool())
.unwrap_or(false)
{
continue;
}
let full_name = if prefix.is_empty() {
name.clone()
} else {
format!("{}.{}", prefix, name)
};
let description = prop_value
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("");
let is_nested_object = prop_value.get("type").and_then(|t| t.as_str())
== Some("object")
&& prop_value.get("properties").is_some()
&& prop_value
.get("additionalProperties")
.and_then(|a| a.as_bool())
== Some(false);
if is_nested_object {
extract_settings_keys(prop_value, defs, &full_name, keys);
} else {
let schema_type = get_schema_type(prop_value, defs);
keys.push((full_name, description.to_string(), schema_type));
}
}
}
}
fn escape_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn is_section_property(prop: &Value, defs: Option<&Value>) -> bool {
if prop.get("additionalProperties").is_some() {
return true;
}
if let Some(type_val) = prop.get("type").and_then(|t| t.as_str()) {
match type_val {
"array" => return false, "string" | "number" | "boolean" | "integer" => return false, "object" => {
if prop.get("additionalProperties").is_some() {
return true;
}
if prop.get("properties").is_some() && prop.get("additionalProperties").is_none() {
return false;
}
return true;
}
_ => {}
}
}
if let Some(ref_val) = prop.get("$ref").and_then(|r| r.as_str())
&& let Some(def_name) = ref_val.strip_prefix("#/$defs/")
&& let Some(def) = defs.and_then(|d| d.get(def_name))
{
return is_section_property(def, defs);
}
if let Some(one_of) = prop.get("oneOf").and_then(|o| o.as_array()) {
for option in one_of {
if let Some(type_val) = option.get("type").and_then(|t| t.as_str())
&& matches!(type_val, "string" | "number" | "boolean" | "integer")
{
return false;
}
}
for option in one_of {
if is_section_property(option, defs) {
return true;
}
}
return false;
}
true
}