generate the settings docs (#5740)

* generate the settings docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update docs/kcl/settings.md

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-03-10 18:59:10 -07:00
committed by GitHub
parent 310932dc5a
commit 546b4ea3b8
9 changed files with 812 additions and 6 deletions

View File

@ -0,0 +1,232 @@
use std::{fs, path::PathBuf};
use schemars::{gen::SchemaGenerator, JsonSchema};
use serde_json::{json, Value};
use crate::settings::types::{project::ProjectConfiguration, Configuration};
// Project settings example in TOML format
const PROJECT_SETTINGS_EXAMPLE: &str = r#"[settings.app]
# Set the appearance of the application
name = "My Awesome Project"
[settings.app.appearance]
# Use dark mode theme
theme = "dark"
# Set the app color to blue (240.0 = blue, 0.0 = red, 120.0 = green)
color = 240.0
[settings.modeling]
# Use inches as the default measurement unit
base_unit = "in"
"#;
// User settings example in TOML format
const USER_SETTINGS_EXAMPLE: &str = r#"[settings.app]
# Set the appearance of the application
[settings.app.appearance]
# Use dark mode theme
theme = "dark"
# Set the app color to blue (240.0 = blue, 0.0 = red, 120.0 = green)
color = 240.0
[settings.modeling]
# Use millimeters as the default measurement unit
base_unit = "mm"
[settings.text_editor]
# Disable text wrapping in the editor
text_wrapping = false
"#;
const PROJECT_SETTINGS_DOC_PATH: &str = "../../docs/kcl/settings/project.toml.md";
const USER_SETTINGS_DOC_PATH: &str = "../../docs/kcl/settings/user.toml.md";
fn init_handlebars() -> handlebars::Handlebars<'static> {
let mut hbs = handlebars::Handlebars::new();
// Register helper to pretty-format enum values
hbs.register_helper(
"pretty_enum",
Box::new(
|h: &handlebars::Helper,
_: &handlebars::Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output|
-> handlebars::HelperResult {
if let Some(enum_value) = h.param(0) {
if let Some(array) = enum_value.value().as_array() {
let pretty_options = array
.iter()
.filter_map(|v| v.as_str())
.map(|s| format!("`{}`", s))
.collect::<Vec<_>>()
.join(", ");
out.write(&pretty_options)?;
return Ok(());
}
}
out.write("No options available")?;
Ok(())
},
),
);
// Helper to format default values better
hbs.register_helper(
"format_default",
Box::new(
|h: &handlebars::Helper,
_: &handlebars::Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output|
-> handlebars::HelperResult {
if let Some(default) = h.param(0) {
let val = default.value();
match val {
Value::Null => out.write("None")?,
Value::Bool(b) => out.write(&b.to_string())?,
Value::Number(n) => out.write(&n.to_string())?,
Value::String(s) => out.write(&format!("`{}`", s))?,
Value::Array(arr) => {
let formatted = arr
.iter()
.map(|v| match v {
Value::String(s) => format!("`{}`", s),
_ => format!("{}", v),
})
.collect::<Vec<_>>()
.join(", ");
out.write(&format!("[{}]", formatted))?;
}
Value::Object(_) => out.write("(complex default)")?,
}
return Ok(());
}
out.write("None")?;
Ok(())
},
),
);
// Register the settings template
hbs.register_template_string("settings", include_str!("templates/settings.hbs"))
.expect("Failed to register settings template");
hbs
}
fn ensure_settings_dir() {
let settings_dir = PathBuf::from("../../docs/kcl/settings");
if !settings_dir.exists() {
fs::create_dir_all(&settings_dir).expect("Failed to create settings directory");
}
}
pub fn generate_settings_docs() {
ensure_settings_dir();
let hbs = init_handlebars();
// Generate project settings documentation
let mut settings = schemars::gen::SchemaSettings::default();
settings.inline_subschemas = true;
settings.meta_schema = None; // We don't need the meta schema for docs
settings.option_nullable = false; // Important - makes Option fields show properly
settings.option_add_null_type = false;
let mut generator = SchemaGenerator::new(settings.clone());
let project_schema = ProjectConfiguration::json_schema(&mut generator);
// For debugging the schema:
// fs::write("/tmp/project_schema.json", serde_json::to_string_pretty(&project_schema).unwrap())
// .expect("Failed to write debug schema");
// Extract the description from the schema metadata
let project_description = if let schemars::schema::Schema::Object(obj) = &project_schema {
if let Some(metadata) = &obj.metadata {
metadata.description.clone().unwrap_or_default()
} else {
"Project specific settings for the KittyCAD modeling app.".to_string()
}
} else {
"Project specific settings for the KittyCAD modeling app.".to_string()
};
// Convert the schema to our template format
let project_data = json!({
"title": "Project Settings",
"description": project_description,
"config_type": "Project Configuration",
"file_name": "project.toml",
"settings": json!(project_schema),
"example": PROJECT_SETTINGS_EXAMPLE
});
let project_output = hbs
.render("settings", &project_data)
.expect("Failed to render project settings documentation");
expectorate::assert_contents(PROJECT_SETTINGS_DOC_PATH, &project_output);
// Generate user settings documentation
let mut generator = SchemaGenerator::new(settings);
let user_schema = Configuration::json_schema(&mut generator);
// For debugging the schema:
// fs::write("/tmp/user_schema.json", serde_json::to_string_pretty(&user_schema).unwrap())
// .expect("Failed to write debug schema");
// Extract the description from the schema metadata
let user_description = if let schemars::schema::Schema::Object(obj) = &user_schema {
if let Some(metadata) = &obj.metadata {
metadata.description.clone().unwrap_or_default()
} else {
"User-specific configuration options for the KittyCAD modeling app.".to_string()
}
} else {
"User-specific configuration options for the KittyCAD modeling app.".to_string()
};
// Trim any trailing periods to avoid double periods
let user_data = json!({
"title": "User Settings",
"description": user_description,
"config_type": "User Configuration",
"file_name": "user.toml",
"settings": json!(user_schema),
"example": USER_SETTINGS_EXAMPLE
});
let user_output = hbs
.render("settings", &user_data)
.expect("Failed to render user settings documentation");
expectorate::assert_contents(USER_SETTINGS_DOC_PATH, &user_output);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_settings_docs() {
// First verify that our TOML examples are valid and match the expected types
let _project_config: ProjectConfiguration = toml::from_str(PROJECT_SETTINGS_EXAMPLE)
.expect("Project settings example is not valid according to ProjectConfiguration");
let _user_config: Configuration = toml::from_str(USER_SETTINGS_EXAMPLE)
.expect("User settings example is not valid according to Configuration");
// Expectorate will verify the output matches what we expect,
// or update it if run with EXPECTORATE=overwrite
generate_settings_docs();
// Verify files exist
let project_path = PathBuf::from(PROJECT_SETTINGS_DOC_PATH);
let user_path = PathBuf::from(USER_SETTINGS_DOC_PATH);
assert!(project_path.exists(), "Project settings documentation not generated");
assert!(user_path.exists(), "User settings documentation not generated");
}
}