Merge remote-tracking branch 'origin/main' into paultag/import
This commit is contained in:
@ -21,7 +21,7 @@ use crate::{
|
||||
};
|
||||
|
||||
const TYPES_DIR: &str = "../../docs/kcl/types";
|
||||
const LANG_TOPICS: [&str; 4] = ["Types", "Modules", "Settings", "Known Issues"];
|
||||
const LANG_TOPICS: [&str; 5] = ["Types", "Modules", "Settings", "Known Issues", "Constants"];
|
||||
// These types are declared in std.
|
||||
const DECLARED_TYPES: [&str; 7] = ["number", "string", "tag", "bool", "Sketch", "Solid", "Plane"];
|
||||
|
||||
@ -298,6 +298,7 @@ fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
|
||||
hbs.register_template_string("propertyType", include_str!("templates/propertyType.hbs"))?;
|
||||
hbs.register_template_string("schema", include_str!("templates/schema.hbs"))?;
|
||||
hbs.register_template_string("index", include_str!("templates/index.hbs"))?;
|
||||
hbs.register_template_string("consts-index", include_str!("templates/consts-index.hbs"))?;
|
||||
hbs.register_template_string("function", include_str!("templates/function.hbs"))?;
|
||||
hbs.register_template_string("const", include_str!("templates/const.hbs"))?;
|
||||
hbs.register_template_string("type", include_str!("templates/type.hbs"))?;
|
||||
@ -312,6 +313,9 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &[Doc
|
||||
let mut functions = HashMap::new();
|
||||
functions.insert("std".to_owned(), Vec::new());
|
||||
|
||||
let mut constants = HashMap::new();
|
||||
constants.insert("std".to_owned(), Vec::new());
|
||||
|
||||
for key in combined.keys() {
|
||||
let internal_fn = combined
|
||||
.get(key)
|
||||
@ -337,6 +341,13 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &[Doc
|
||||
DocData::Const(c) => (c.name.clone(), d.file_name()),
|
||||
DocData::Ty(t) => (t.name.clone(), d.file_name()),
|
||||
});
|
||||
|
||||
if let DocData::Const(c) = d {
|
||||
constants
|
||||
.entry(d.mod_name())
|
||||
.or_default()
|
||||
.push((c.name.clone(), d.file_name()));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO we should sub-divide into types, constants, and functions.
|
||||
@ -362,7 +373,7 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &[Doc
|
||||
.map(|name| {
|
||||
json!({
|
||||
"name": name,
|
||||
"file_name": name.to_lowercase().replace(' ', "-"),
|
||||
"file_name": name.to_lowercase().replace(' ', "-").replace("constants", "consts"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@ -375,6 +386,31 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &[Doc
|
||||
|
||||
expectorate::assert_contents("../../docs/kcl/index.md", &output);
|
||||
|
||||
// Generate the index for the constants.
|
||||
let mut sorted_consts: Vec<_> = constants
|
||||
.into_iter()
|
||||
.map(|(m, mut consts)| {
|
||||
consts.sort();
|
||||
let val = json!({
|
||||
"name": m,
|
||||
"consts": consts.into_iter().map(|(n, f)| json!({
|
||||
"name": n,
|
||||
"file_name": f,
|
||||
})).collect::<Vec<_>>(),
|
||||
});
|
||||
(m, val)
|
||||
})
|
||||
.collect();
|
||||
sorted_consts.sort_by(|t1, t2| t1.0.cmp(&t2.0));
|
||||
let data: Vec<_> = sorted_consts.into_iter().map(|(_, val)| val).collect();
|
||||
let data = json!({
|
||||
"consts": data,
|
||||
});
|
||||
|
||||
let output = hbs.render("consts-index", &data)?;
|
||||
|
||||
expectorate::assert_contents("../../docs/kcl/consts.md", &output);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -405,7 +441,7 @@ fn generate_example(index: usize, src: &str, props: &ExampleProperties, file_nam
|
||||
}))
|
||||
}
|
||||
|
||||
fn generate_type_from_kcl(ty: &TyData, file_name: String) -> Result<()> {
|
||||
fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String) -> Result<()> {
|
||||
if ty.properties.doc_hidden {
|
||||
return Ok(());
|
||||
}
|
||||
@ -416,7 +452,7 @@ fn generate_type_from_kcl(ty: &TyData, file_name: String) -> Result<()> {
|
||||
.examples
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, example)| generate_example(index, &example.0, &example.1, &file_name))
|
||||
.filter_map(|(index, example)| generate_example(index, &example.0, &example.1, &example_name))
|
||||
.collect();
|
||||
|
||||
let data = json!({
|
||||
@ -428,7 +464,7 @@ fn generate_type_from_kcl(ty: &TyData, file_name: String) -> Result<()> {
|
||||
});
|
||||
|
||||
let output = hbs.render("kclType", &data)?;
|
||||
expectorate::assert_contents(format!("../../docs/kcl/types/{}.md", file_name), &output);
|
||||
expectorate::assert_contents(format!("../../docs/kcl/{}.md", file_name), &output);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -480,7 +516,7 @@ fn generate_function_from_kcl(function: &FnData, file_name: String) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_const_from_kcl(cnst: &ConstData, file_name: String) -> Result<()> {
|
||||
fn generate_const_from_kcl(cnst: &ConstData, file_name: String, example_name: String) -> Result<()> {
|
||||
if cnst.properties.doc_hidden {
|
||||
return Ok(());
|
||||
}
|
||||
@ -490,7 +526,7 @@ fn generate_const_from_kcl(cnst: &ConstData, file_name: String) -> Result<()> {
|
||||
.examples
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, example)| generate_example(index, &example.0, &example.1, &file_name))
|
||||
.filter_map(|(index, example)| generate_example(index, &example.0, &example.1, &example_name))
|
||||
.collect();
|
||||
|
||||
let data = json!({
|
||||
@ -1028,8 +1064,8 @@ fn test_generate_stdlib_markdown_docs() {
|
||||
for d in &kcl_std {
|
||||
match d {
|
||||
DocData::Fn(f) => generate_function_from_kcl(f, d.file_name()).unwrap(),
|
||||
DocData::Const(c) => generate_const_from_kcl(c, d.file_name()).unwrap(),
|
||||
DocData::Ty(t) => generate_type_from_kcl(t, d.file_name()).unwrap(),
|
||||
DocData::Const(c) => generate_const_from_kcl(c, d.file_name(), d.example_name()).unwrap(),
|
||||
DocData::Ty(t) => generate_type_from_kcl(t, d.file_name(), d.example_name()).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1061,7 +1097,8 @@ fn test_generate_stdlib_json_schema() {
|
||||
async fn test_code_in_topics() {
|
||||
let mut join_set = JoinSet::new();
|
||||
for name in LANG_TOPICS {
|
||||
let filename = format!("../../docs/kcl/{}.md", name.to_lowercase().replace(' ', "-"));
|
||||
let filename =
|
||||
format!("../../docs/kcl/{}.md", name.to_lowercase().replace(' ', "-")).replace("constants", "consts");
|
||||
let mut file = File::open(&filename).unwrap();
|
||||
let mut text = String::new();
|
||||
file.read_to_string(&mut text).unwrap();
|
||||
|
@ -116,10 +116,18 @@ impl DocData {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn file_name(&self) -> String {
|
||||
match self {
|
||||
DocData::Fn(f) => f.qual_name.replace("::", "-"),
|
||||
DocData::Const(c) => format!("consts/{}", c.qual_name.replace("::", "-")),
|
||||
DocData::Ty(t) => format!("types/{}", t.name.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn example_name(&self) -> String {
|
||||
match self {
|
||||
DocData::Fn(f) => f.qual_name.replace("::", "-"),
|
||||
DocData::Const(c) => format!("const_{}", c.qual_name.replace("::", "-")),
|
||||
// TODO might want to change this
|
||||
DocData::Ty(t) => t.name.clone(),
|
||||
}
|
||||
}
|
||||
@ -872,7 +880,7 @@ mod test {
|
||||
Ok(img) => img,
|
||||
};
|
||||
twenty_twenty::assert_image(
|
||||
format!("tests/outputs/serial_test_example_{}{i}.png", d.file_name()),
|
||||
format!("tests/outputs/serial_test_example_{}{i}.png", d.example_name()),
|
||||
&result,
|
||||
0.99,
|
||||
);
|
||||
|
17
rust/kcl-lib/src/docs/templates/consts-index.hbs
vendored
Normal file
17
rust/kcl-lib/src/docs/templates/consts-index.hbs
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: "KCL Constants"
|
||||
excerpt: "Documentation for the KCL constants."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
{{#each consts}}
|
||||
|
||||
### `{{name}}`
|
||||
|
||||
{{#each consts}}
|
||||
- [`{{name}}`](/docs/kcl/{{file_name}})
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
@ -64,8 +64,6 @@ pub struct ExecOutcome {
|
||||
/// Operations that have been performed in execution order, for display in
|
||||
/// the Feature Tree.
|
||||
pub operations: Vec<Operation>,
|
||||
/// Output map of UUIDs to artifacts.
|
||||
pub artifacts: IndexMap<ArtifactId, Artifact>,
|
||||
/// Output commands to allow building the artifact graph by the caller.
|
||||
pub artifact_commands: Vec<ArtifactCommand>,
|
||||
/// Output artifact graph.
|
||||
|
@ -123,7 +123,6 @@ impl ExecState {
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
operations: self.global.operations,
|
||||
artifacts: self.global.artifacts,
|
||||
artifact_commands: self.global.artifact_commands,
|
||||
artifact_graph: self.global.artifact_graph,
|
||||
errors: self.global.errors,
|
||||
@ -146,7 +145,6 @@ impl ExecState {
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
operations: Default::default(),
|
||||
artifacts: Default::default(),
|
||||
artifact_commands: Default::default(),
|
||||
artifact_graph: Default::default(),
|
||||
errors: self.global.errors,
|
||||
|
@ -2809,29 +2809,45 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
|
||||
let _ = open_paren.parse_next(i)?;
|
||||
ignore_whitespace(i);
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ArgPlace {
|
||||
NonCode(Node<NonCodeNode>),
|
||||
LabeledArg(LabeledArg),
|
||||
UnlabeledArg(Expr),
|
||||
}
|
||||
let initial_unlabeled_arg = opt((expression, comma, opt(whitespace)).map(|(arg, _, _)| arg)).parse_next(i)?;
|
||||
let args: Vec<_> = repeat(
|
||||
0..,
|
||||
alt((
|
||||
terminated(non_code_node.map(NonCodeOr::NonCode), whitespace),
|
||||
terminated(labeled_argument, labeled_arg_separator).map(NonCodeOr::Code),
|
||||
terminated(non_code_node.map(ArgPlace::NonCode), whitespace),
|
||||
terminated(labeled_argument, labeled_arg_separator).map(ArgPlace::LabeledArg),
|
||||
expression.map(ArgPlace::UnlabeledArg),
|
||||
)),
|
||||
)
|
||||
.parse_next(i)?;
|
||||
let (args, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = args.into_iter().enumerate().fold(
|
||||
let (args, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = args.into_iter().enumerate().try_fold(
|
||||
(Vec::new(), BTreeMap::new()),
|
||||
|(mut args, mut non_code_nodes), (i, e)| {
|
||||
match e {
|
||||
NonCodeOr::NonCode(x) => {
|
||||
ArgPlace::NonCode(x) => {
|
||||
non_code_nodes.insert(i, vec![x]);
|
||||
}
|
||||
NonCodeOr::Code(x) => {
|
||||
ArgPlace::LabeledArg(x) => {
|
||||
args.push(x);
|
||||
}
|
||||
ArgPlace::UnlabeledArg(arg) => {
|
||||
return Err(ErrMode::Cut(
|
||||
CompilationError::fatal(
|
||||
SourceRange::from(arg),
|
||||
"This argument needs a label, but it doesn't have one",
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
(args, non_code_nodes)
|
||||
Ok((args, non_code_nodes))
|
||||
},
|
||||
);
|
||||
)?;
|
||||
if let Some(std_fn) = crate::std::get_stdlib_fn(&fn_name.name) {
|
||||
let just_args: Vec<_> = args.iter().collect();
|
||||
typecheck_all_kw(std_fn, &just_args)?;
|
||||
@ -4641,6 +4657,27 @@ baz = 2
|
||||
assert_eq!(actual.operator, UnaryOperator::Not);
|
||||
crate::parsing::top_level_parse(some_program_string).unwrap(); // Updated import path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sensible_error_when_missing_equals_in_kwarg() {
|
||||
for (i, program) in ["f(x=1,y)", "f(x=1,y,z)", "f(x=1,y,z=1)", "f(x=1, y, z=1)"]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
|
||||
let err = fn_call_kw.parse(tokens.as_slice()).unwrap_err();
|
||||
let cause = err.inner().cause.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
cause.message, "This argument needs a label, but it doesn't have one",
|
||||
"failed test {i}: {program}"
|
||||
);
|
||||
assert_eq!(
|
||||
cause.source_range.start(),
|
||||
program.find("y").unwrap(),
|
||||
"failed test {i}: {program}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
232
rust/kcl-lib/src/settings/generate_settings_docs.rs
Normal file
232
rust/kcl-lib/src/settings/generate_settings_docs.rs
Normal 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.md";
|
||||
const USER_SETTINGS_DOC_PATH: &str = "../../docs/kcl/settings/user.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");
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
//! This module contains settings for kcl projects as well as the modeling app.
|
||||
|
||||
pub mod types;
|
||||
|
||||
#[cfg(test)]
|
||||
mod generate_settings_docs;
|
||||
|
67
rust/kcl-lib/src/settings/templates/settings.hbs
Normal file
67
rust/kcl-lib/src/settings/templates/settings.hbs
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "{{title}}"
|
||||
excerpt: "{{{description}}}"
|
||||
layout: manual
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
{{{description}}}
|
||||
|
||||
## {{config_type}} Structure
|
||||
|
||||
```toml
|
||||
{{{example}}}
|
||||
```
|
||||
|
||||
## Available Settings
|
||||
|
||||
{{#with settings.properties}}
|
||||
{{#each this}}
|
||||
### {{@key}}
|
||||
|
||||
{{#if metadata.description}}{{metadata.description}}{{/if}}
|
||||
|
||||
{{#with properties}}
|
||||
{{#each this}}
|
||||
#### {{@key}}
|
||||
|
||||
{{#if description}}{{description}}{{/if}}
|
||||
|
||||
{{#if enum}}
|
||||
**Possible values:** {{pretty_enum enum}}
|
||||
{{/if}}
|
||||
|
||||
**Default:** {{#if default}}{{format_default default}}{{else}}None{{/if}}
|
||||
|
||||
{{#if properties}}
|
||||
This setting has the following nested options:
|
||||
|
||||
{{#each properties}}
|
||||
##### {{@key}}
|
||||
|
||||
{{#if description}}{{description}}{{/if}}
|
||||
|
||||
{{#if enum}}
|
||||
**Possible values:** {{pretty_enum enum}}
|
||||
{{/if}}
|
||||
|
||||
**Default:** {{#if default}}{{format_default default}}{{else}}None{{/if}}
|
||||
|
||||
{{#if properties}}
|
||||
This setting has further nested options. See the schema for full details.
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
{{/with}}
|
||||
|
||||
{{/each}}
|
||||
{{/with}}
|
||||
|
||||
## Complete Example
|
||||
|
||||
```toml
|
||||
{{{example}}}
|
||||
```
|
@ -11,7 +11,11 @@ use validator::{Validate, ValidateRange};
|
||||
const DEFAULT_THEME_COLOR: f64 = 264.5;
|
||||
const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
|
||||
|
||||
/// High level configuration.
|
||||
/// User specific settings for the app.
|
||||
/// These live in `user.toml` in the app's configuration directory.
|
||||
/// Updating the settings in the app will update this file automatically.
|
||||
/// Do not edit this file manually, as it may be overwritten by the app.
|
||||
/// Manual edits can cause corruption of the settings file.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
@ -10,7 +10,11 @@ use crate::settings::types::{
|
||||
is_default, AppColor, CommandBarSettings, DefaultTrue, FloatOrInt, OnboardingStatus, TextEditorSettings, UnitLength,
|
||||
};
|
||||
|
||||
/// High level project configuration.
|
||||
/// Project specific settings for the app.
|
||||
/// These live in `project.toml` in the base of the project directory.
|
||||
/// Updating the settings for the project in the app will update this file automatically.
|
||||
/// Do not edit this file manually, as it may be overwritten by the app.
|
||||
/// Manual edits can cause corruption of the settings file.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
@ -27,7 +27,7 @@ struct Test {
|
||||
}
|
||||
|
||||
pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png";
|
||||
//pub(crate) const EXPORTED_STEP_NAME: &str = "exported_step.step";
|
||||
pub(crate) const EXPORTED_STEP_NAME: &str = "exported_step.linux.step";
|
||||
|
||||
impl Test {
|
||||
fn new(name: &str) -> Self {
|
||||
@ -107,15 +107,7 @@ fn unparse_test(test: &Test) {
|
||||
};
|
||||
// Check recasting the AST produces the original string.
|
||||
let actual = ast.recast(&Default::default(), 0);
|
||||
if matches!(std::env::var("EXPECTORATE").as_deref(), Ok("overwrite")) {
|
||||
std::fs::write(test.input_dir.join(&test.entry_point), &actual).unwrap();
|
||||
}
|
||||
let expected = read(&test.entry_point, &test.input_dir);
|
||||
pretty_assertions::assert_eq!(
|
||||
actual,
|
||||
expected,
|
||||
"Parse then unparse didn't recreate the original KCL file"
|
||||
);
|
||||
expectorate::assert_contents(test.input_dir.join(&test.entry_point), &actual);
|
||||
}
|
||||
|
||||
async fn execute(test_name: &str, render_to_png: bool) {
|
||||
@ -153,12 +145,13 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
|
||||
}
|
||||
if export_step {
|
||||
let step = step.unwrap();
|
||||
// TODO FIXME: This is failing because the step file is not deterministic.
|
||||
// But it should be, talk to @katie
|
||||
/*assert_snapshot(test, "Step file", || {
|
||||
insta::assert_binary_snapshot!(EXPORTED_STEP_NAME, step);
|
||||
});*/
|
||||
std::fs::write(test.output_dir.join("exported_step.snap.step"), step).unwrap();
|
||||
let step_str = std::str::from_utf8(&step).unwrap();
|
||||
// We use expectorate here so we can see the diff in ci.
|
||||
expectorate::assert_contents(
|
||||
test.output_dir
|
||||
.join(format!("exported_step.{}.step", std::env::consts::OS)),
|
||||
step_str,
|
||||
);
|
||||
}
|
||||
let outcome = exec_state.to_wasm_outcome(env_ref);
|
||||
assert_common_snapshots(
|
||||
|
@ -1,18 +1,12 @@
|
||||
//! Run all the KCL samples in the `kcl_samples` directory.
|
||||
//!
|
||||
//! Use the `KCL_SAMPLES_ONLY=gear` environment variable to run only a subset of
|
||||
//! the samples, in this case, all those that start with "gear".
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use fnv::FnvHashSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use super::Test;
|
||||
|
||||
@ -25,76 +19,37 @@ lazy_static::lazy_static! {
|
||||
static ref OUTPUTS_DIR: PathBuf = Path::new("tests/kcl_samples").to_path_buf();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
#[kcl_directory_test_macro::test_all_dirs("../public/kcl-samples")]
|
||||
fn parse(dir_name: &str, dir_path: &Path) {
|
||||
let t = test(dir_name, dir_path.join("main.kcl").to_str().unwrap().to_owned());
|
||||
let write_new = matches!(
|
||||
std::env::var("INSTA_UPDATE").as_deref(),
|
||||
Ok("auto" | "always" | "new" | "unseen")
|
||||
);
|
||||
let filter = filter_from_env();
|
||||
let tests = kcl_samples_inputs(filter.as_deref());
|
||||
let expected_outputs = kcl_samples_outputs(filter.as_deref());
|
||||
|
||||
assert!(!tests.is_empty(), "No KCL samples found");
|
||||
|
||||
let input_names = FnvHashSet::from_iter(tests.iter().map(|t| t.name.clone()));
|
||||
|
||||
for test in tests {
|
||||
if write_new {
|
||||
// Ensure the directory exists for new tests.
|
||||
std::fs::create_dir_all(test.output_dir.clone()).unwrap();
|
||||
}
|
||||
super::parse_test(&test);
|
||||
if write_new {
|
||||
// Ensure the directory exists for new tests.
|
||||
std::fs::create_dir_all(t.output_dir.clone()).unwrap();
|
||||
}
|
||||
|
||||
// Ensure that inputs aren't missing.
|
||||
let missing = expected_outputs
|
||||
.into_iter()
|
||||
.filter(|name| !input_names.contains(name))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(missing.is_empty(), "Expected input kcl-samples for the following. If these are no longer tests, delete the expected output directories for them in {}: {missing:?}", OUTPUTS_DIR.to_string_lossy());
|
||||
super::parse_test(&t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unparse() {
|
||||
#[kcl_directory_test_macro::test_all_dirs("../public/kcl-samples")]
|
||||
fn unparse(dir_name: &str, dir_path: &Path) {
|
||||
// kcl-samples don't always use correct formatting. We don't ignore the
|
||||
// test because we want to allow the just command to work. It's actually
|
||||
// fine when no test runs.
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_execute() {
|
||||
let filter = filter_from_env();
|
||||
let tests = kcl_samples_inputs(filter.as_deref());
|
||||
let expected_outputs = kcl_samples_outputs(filter.as_deref());
|
||||
#[kcl_directory_test_macro::test_all_dirs("../public/kcl-samples")]
|
||||
async fn kcl_test_execute(dir_name: &str, dir_path: &Path) {
|
||||
let t = test(dir_name, dir_path.join("main.kcl").to_str().unwrap().to_owned());
|
||||
super::execute_test(&t, true, true).await;
|
||||
}
|
||||
|
||||
assert!(!tests.is_empty(), "No KCL samples found");
|
||||
|
||||
// Note: This is unordered.
|
||||
let mut tasks = JoinSet::new();
|
||||
// Mapping from task ID to test index.
|
||||
let mut id_to_index = HashMap::new();
|
||||
// Spawn a task for each test.
|
||||
for (index, test) in tests.iter().cloned().enumerate() {
|
||||
let handle = tasks.spawn(async move {
|
||||
super::execute_test(&test, true, true).await;
|
||||
});
|
||||
id_to_index.insert(handle.id(), index);
|
||||
}
|
||||
|
||||
// Join all the tasks and collect the failures. We cannot just join_all
|
||||
// because insta's error messages don't clearly indicate which test failed.
|
||||
let mut failed = vec![None; tests.len()];
|
||||
while let Some(result) = tasks.join_next().await {
|
||||
let Err(err) = result else {
|
||||
continue;
|
||||
};
|
||||
// When there's an error, store the test name and error message.
|
||||
let index = *id_to_index.get(&err.id()).unwrap();
|
||||
failed[index] = Some(format!("{}: {err}", &tests[index].name));
|
||||
}
|
||||
let failed = failed.into_iter().flatten().collect::<Vec<_>>();
|
||||
assert!(failed.is_empty(), "Failed tests: {}", failed.join("\n"));
|
||||
#[test]
|
||||
fn test_after_engine_ensure_kcl_samples_manifest_etc() {
|
||||
let tests = kcl_samples_inputs();
|
||||
let expected_outputs = kcl_samples_outputs();
|
||||
|
||||
// Ensure that inputs aren't missing.
|
||||
let input_names = FnvHashSet::from_iter(tests.iter().map(|t| t.name.clone()));
|
||||
@ -125,7 +80,7 @@ async fn kcl_test_execute() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let step_file = OUTPUTS_DIR.join(&tests.name).join("exported_step.snap.step");
|
||||
let step_file = OUTPUTS_DIR.join(&tests.name).join(super::EXPORTED_STEP_NAME);
|
||||
if !step_file.exists() {
|
||||
panic!("Missing step for test: {}", tests.name);
|
||||
}
|
||||
@ -147,7 +102,7 @@ async fn kcl_test_execute() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_manifest() {
|
||||
fn test_after_engine_generate_manifest() {
|
||||
// Generate the manifest.json
|
||||
generate_kcl_manifest(&INPUTS_DIR).unwrap();
|
||||
}
|
||||
@ -161,11 +116,7 @@ fn test(test_name: &str, entry_point: String) -> Test {
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_from_env() -> Option<String> {
|
||||
std::env::var("KCL_SAMPLES_ONLY").ok().filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn kcl_samples_inputs(filter: Option<&str>) -> Vec<Test> {
|
||||
fn kcl_samples_inputs() -> Vec<Test> {
|
||||
let mut tests = Vec::new();
|
||||
|
||||
// Collect all directory entries first and sort them by name for consistent ordering
|
||||
@ -197,11 +148,6 @@ fn kcl_samples_inputs(filter: Option<&str>) -> Vec<Test> {
|
||||
// Skip output directories.
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = &filter {
|
||||
if !dir_name_str.starts_with(filter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
eprintln!("Found KCL sample: {:?}", dir_name.to_string_lossy());
|
||||
// Look for the entry point inside the directory.
|
||||
let sub_dir = INPUTS_DIR.join(dir_name);
|
||||
@ -216,7 +162,7 @@ fn kcl_samples_inputs(filter: Option<&str>) -> Vec<Test> {
|
||||
tests
|
||||
}
|
||||
|
||||
fn kcl_samples_outputs(filter: Option<&str>) -> Vec<String> {
|
||||
fn kcl_samples_outputs() -> Vec<String> {
|
||||
let mut outputs = Vec::new();
|
||||
|
||||
for entry in OUTPUTS_DIR.read_dir().unwrap() {
|
||||
@ -234,11 +180,6 @@ fn kcl_samples_outputs(filter: Option<&str>) -> Vec<String> {
|
||||
// Skip hidden.
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = &filter {
|
||||
if !dir_name_str.starts_with(filter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("Found expected KCL sample: {:?}", &dir_name_str);
|
||||
outputs.push(dir_name_str.into_owned());
|
||||
@ -352,10 +293,7 @@ fn generate_kcl_manifest(dir: &Path) -> Result<()> {
|
||||
|
||||
// Write the manifest.json
|
||||
let output_path = dir.join(MANIFEST_FILE);
|
||||
let manifest_json = serde_json::to_string_pretty(&manifest)?;
|
||||
|
||||
let mut file = fs::File::create(output_path.clone())?;
|
||||
file.write_all(manifest_json.as_bytes())?;
|
||||
expectorate::assert_contents(&output_path, &serde_json::to_string_pretty(&manifest).unwrap());
|
||||
|
||||
println!(
|
||||
"Manifest of {} items written to {}",
|
||||
@ -391,7 +329,7 @@ fn update_readme(dir: &Path, new_content: &str) -> Result<()> {
|
||||
let updated_content = format!("{}{}\n", &content[..position], new_content);
|
||||
|
||||
// Write the modified content back to the file
|
||||
std::fs::write(readme_path, updated_content)?;
|
||||
expectorate::assert_contents(&readme_path, &updated_content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -182,13 +182,22 @@ impl Args {
|
||||
}))?;
|
||||
|
||||
T::from_kcl_val(&arg.value).ok_or_else(|| {
|
||||
let expected_type_name = tynm::type_name::<T>();
|
||||
let actual_type_name = arg.value.human_friendly_type();
|
||||
let msg_base = format!("This function expected this argument to be of type {expected_type_name} but it's actually of type {actual_type_name}");
|
||||
let suggestion = match (expected_type_name.as_str(), actual_type_name) {
|
||||
("SolidSet", "Sketch") => Some(
|
||||
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
let message = match suggestion {
|
||||
None => msg_base,
|
||||
Some(sugg) => format!("{msg_base}. {sugg}"),
|
||||
};
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: arg.source_ranges(),
|
||||
message: format!(
|
||||
"Expected a {} but found {}",
|
||||
type_name::<T>(),
|
||||
arg.value.human_friendly_type()
|
||||
),
|
||||
message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user