kcl_derive_docs/
lib.rs

1// Clippy's style advice is definitely valuable, but not worth the trouble for
2// automated enforcement.
3#![allow(clippy::style)]
4
5mod example_tests;
6#[cfg(test)]
7mod tests;
8mod unbox;
9
10use std::collections::HashMap;
11
12use convert_case::Casing;
13use inflector::{cases::camelcase::to_camel_case, Inflector};
14use once_cell::sync::Lazy;
15use quote::{format_ident, quote, quote_spanned, ToTokens};
16use regex::Regex;
17use serde::Deserialize;
18use serde_tokenstream::{from_tokenstream, Error};
19use syn::{
20    parse::{Parse, ParseStream},
21    Attribute, Signature, Visibility,
22};
23use unbox::unbox;
24
25#[proc_macro_attribute]
26pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
27    do_output(do_stdlib(attr.into(), item.into()))
28}
29
30#[proc_macro_attribute]
31pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
32    example_tests::do_for_each_example_test(item.into()).into()
33}
34
35#[proc_macro_attribute]
36pub fn for_all_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
37    example_tests::do_for_all_example_test(item.into()).into()
38}
39
40/// Describes an argument of a stdlib function.
41#[derive(Deserialize, Debug)]
42struct ArgMetadata {
43    /// Docs for the argument.
44    docs: String,
45
46    /// If this argument is optional, it should still be included in completion snippets.
47    /// Does not do anything if the argument is already required.
48    #[serde(default)]
49    include_in_snippet: bool,
50
51    /// The snippet should suggest this value for the arg.
52    #[serde(default)]
53    snippet_value: Option<String>,
54
55    /// The snippet should suggest this value for the arg.
56    #[serde(default)]
57    snippet_value_array: Option<Vec<String>>,
58}
59
60#[derive(Deserialize, Debug)]
61struct StdlibMetadata {
62    /// The name of the function in the API.
63    name: String,
64
65    /// Tags for the function.
66    #[serde(default)]
67    tags: Vec<String>,
68
69    /// Whether the function is unpublished.
70    /// Then docs will not be generated.
71    #[serde(default)]
72    unpublished: bool,
73
74    /// Whether the function is deprecated.
75    /// Then specific docs detailing that this is deprecated will be generated.
76    #[serde(default)]
77    deprecated: bool,
78
79    /// Whether the function is displayed in the feature tree.
80    /// If true, calls to the function will be available for display.
81    /// If false, calls to the function will never be displayed.
82    #[serde(default)]
83    feature_tree_operation: bool,
84
85    /// If true, the first argument is unlabeled.
86    /// If false, all arguments require labels.
87    #[serde(default)]
88    unlabeled_first: bool,
89
90    /// Key = argument name, value = argument doc.
91    #[serde(default)]
92    args: HashMap<String, ArgMetadata>,
93}
94
95fn do_stdlib(
96    attr: proc_macro2::TokenStream,
97    item: proc_macro2::TokenStream,
98) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
99    let metadata = from_tokenstream(&attr)?;
100    do_stdlib_inner(metadata, attr, item)
101}
102
103fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
104    match res {
105        Err(err) => err.to_compile_error().into(),
106        Ok((stdlib_docs, errors)) => {
107            let compiler_errors = errors.iter().map(|err| err.to_compile_error());
108
109            let output = quote! {
110                #stdlib_docs
111                #( #compiler_errors )*
112            };
113
114            output.into()
115        }
116    }
117}
118
119fn do_stdlib_inner(
120    metadata: StdlibMetadata,
121    _attr: proc_macro2::TokenStream,
122    item: proc_macro2::TokenStream,
123) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
124    let ast: ItemFnForSignature = syn::parse2(item.clone())?;
125
126    let mut errors = Vec::new();
127
128    if ast.sig.constness.is_some() {
129        errors.push(Error::new_spanned(
130            &ast.sig.constness,
131            "stdlib functions may not be const functions",
132        ));
133    }
134
135    if ast.sig.unsafety.is_some() {
136        errors.push(Error::new_spanned(
137            &ast.sig.unsafety,
138            "stdlib functions may not be unsafe",
139        ));
140    }
141
142    if ast.sig.abi.is_some() {
143        errors.push(Error::new_spanned(
144            &ast.sig.abi,
145            "stdlib functions may not use an alternate ABI",
146        ));
147    }
148
149    if !ast.sig.generics.params.is_empty() {
150        if ast.sig.generics.params.iter().any(|generic_type| match generic_type {
151            syn::GenericParam::Lifetime(_) => false,
152            syn::GenericParam::Type(_) => true,
153            syn::GenericParam::Const(_) => true,
154        }) {
155            errors.push(Error::new_spanned(
156                &ast.sig.generics,
157                "Stdlib functions may not be generic over types or constants, only lifetimes.",
158            ));
159        }
160    }
161
162    if ast.sig.variadic.is_some() {
163        errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
164    }
165
166    let name = metadata.name;
167
168    // Fail if the name is not camel case.
169    // Remove some known suffix exceptions first.
170    let name_cleaned = name.strip_suffix("2d").unwrap_or(name.as_str());
171    let name_cleaned = name.strip_suffix("3d").unwrap_or(name_cleaned);
172    if !name_cleaned.is_camel_case() {
173        errors.push(Error::new_spanned(
174            &ast.sig.ident,
175            format!("stdlib function names must be in camel case: `{}`", name),
176        ));
177    }
178
179    let name_ident = format_ident!("{}", name.to_case(convert_case::Case::UpperCamel));
180    let name_str = name.to_string();
181
182    let fn_name = &ast.sig.ident;
183    let fn_name_str = fn_name.to_string().replace("inner_", "");
184    let fn_name_ident = format_ident!("{}", fn_name_str);
185    let boxed_fn_name_ident = format_ident!("boxed_{}", fn_name_str);
186    let _visibility = &ast.vis;
187
188    let doc_info = extract_doc_from_attrs(&ast.attrs);
189    let comment_text = {
190        let mut buf = String::new();
191        buf.push_str("Std lib function: ");
192        buf.push_str(&name_str);
193        if let Some(s) = &doc_info.summary {
194            buf.push_str("\n");
195            buf.push_str(&s);
196        }
197        if let Some(s) = &doc_info.description {
198            buf.push_str("\n");
199            buf.push_str(&s);
200        }
201        buf
202    };
203    let description_doc_comment = quote! {
204        #[doc = #comment_text]
205    };
206
207    let summary = if let Some(summary) = doc_info.summary {
208        quote! { #summary }
209    } else {
210        quote! { "" }
211    };
212    let description = if let Some(description) = doc_info.description {
213        quote! { #description }
214    } else {
215        quote! { "" }
216    };
217
218    if doc_info.code_blocks.is_empty() {
219        errors.push(Error::new_spanned(
220            &ast.sig,
221            "stdlib functions must have at least one code block",
222        ));
223    }
224
225    // Make sure the function name is in all the code blocks.
226    for code_block in doc_info.code_blocks.iter() {
227        if !code_block.0.contains(&name) {
228            errors.push(Error::new_spanned(
229                &ast.sig,
230                format!(
231                    "stdlib functions must have the function name `{}` in the code block",
232                    name
233                ),
234            ));
235        }
236    }
237
238    let test_code_blocks = doc_info
239        .code_blocks
240        .iter()
241        .enumerate()
242        .map(|(index, (code_block, norun))| {
243            if !norun {
244                generate_code_block_test(&fn_name_str, code_block, index)
245            } else {
246                quote! {}
247            }
248        })
249        .collect::<Vec<_>>();
250
251    let (cb, norun): (Vec<_>, Vec<_>) = doc_info.code_blocks.into_iter().unzip();
252    let code_blocks = quote! {
253        let code_blocks = vec![#(#cb),*];
254        let norun = vec![#(#norun),*];
255        code_blocks.iter().zip(norun).map(|(cb, norun)| {
256            let program = crate::Program::parse_no_errs(cb).unwrap();
257
258            let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
259            options.insert_final_newline = false;
260            (program.ast.recast(&options, 0), norun)
261        }).collect::<Vec<(String, bool)>>()
262    };
263
264    let tags = metadata
265        .tags
266        .iter()
267        .map(|tag| {
268            quote! { #tag.to_string() }
269        })
270        .collect::<Vec<_>>();
271
272    let deprecated = if metadata.deprecated {
273        quote! { true }
274    } else {
275        quote! { false }
276    };
277
278    let unpublished = if metadata.unpublished {
279        quote! { true }
280    } else {
281        quote! { false }
282    };
283
284    let feature_tree_operation = if metadata.feature_tree_operation {
285        quote! { true }
286    } else {
287        quote! { false }
288    };
289
290    let docs_crate = get_crate(None);
291
292    // When the user attaches this proc macro to a function with the wrong type
293    // signature, the resulting errors can be deeply inscrutable. To attempt to
294    // make failures easier to understand, we inject code that asserts the types
295    // of the various parameters. We do this by calling dummy functions that
296    // require a type that satisfies SharedExtractor or ExclusiveExtractor.
297    let mut arg_types = Vec::new();
298    for (i, arg) in ast.sig.inputs.iter().enumerate() {
299        // Get the name of the argument.
300        let arg_name = match arg {
301            syn::FnArg::Receiver(pat) => {
302                let span = pat.self_token.span.unwrap();
303                span.source_text().unwrap().to_string()
304            }
305            syn::FnArg::Typed(pat) => match &*pat.pat {
306                syn::Pat::Ident(ident) => ident.ident.to_string(),
307                _ => {
308                    errors.push(Error::new_spanned(
309                        &pat.pat,
310                        "stdlib functions may not use destructuring patterns",
311                    ));
312                    continue;
313                }
314            },
315        }
316        .trim_start_matches('_')
317        .to_string();
318        // These aren't really KCL args, they're just state that each stdlib function's impl needs.
319        if arg_name == "exec_state" || arg_name == "args" {
320            continue;
321        }
322
323        let ty = match arg {
324            syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
325            syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
326        };
327
328        let (ty_string, ty_ident) = clean_ty_string(ty.to_string().as_str());
329
330        let ty_string = rust_type_to_openapi_type(&ty_string);
331        let required = !ty_ident.to_string().starts_with("Option <");
332        let Some(arg_meta) = metadata.args.get(&arg_name) else {
333            errors.push(Error::new_spanned(arg, format!("arg {arg_name} not found")));
334            continue;
335        };
336        let description = arg_meta.docs.clone();
337        let include_in_snippet = required || arg_meta.include_in_snippet;
338        let snippet_value = arg_meta.snippet_value.clone();
339        let snippet_value_array = arg_meta.snippet_value_array.clone();
340        if snippet_value.is_some() && snippet_value_array.is_some() {
341            errors.push(Error::new_spanned(arg, format!("arg {arg_name} has set both snippet_value and snippet_value array, but at most one of these may be set. Please delete one of them.")));
342        }
343        let label_required = !(i == 0 && metadata.unlabeled_first);
344        let camel_case_arg_name = to_camel_case(&arg_name);
345        if ty_string != "ExecState" && ty_string != "Args" {
346            let schema = quote! {
347                generator.root_schema_for::<#ty_ident>()
348            };
349            let q0 = quote! {
350                    name: #camel_case_arg_name.to_string(),
351                    type_: #ty_string.to_string(),
352                    schema: #schema,
353                    required: #required,
354                    label_required: #label_required,
355                    description: #description.to_string(),
356                    include_in_snippet: #include_in_snippet,
357            };
358            let q1 = if let Some(snippet_value) = snippet_value {
359                quote! {
360                    snippet_value: Some(#snippet_value.to_owned()),
361                }
362            } else {
363                quote! {
364                    snippet_value: None,
365                }
366            };
367            let q2 = if let Some(snippet_value_array) = snippet_value_array {
368                quote! {
369                    snippet_value_array: Some(vec![
370                        #(#snippet_value_array.to_owned()),*
371                    ]),
372                }
373            } else {
374                quote! {
375                    snippet_value_array: None,
376                }
377            };
378            arg_types.push(quote! {
379                #docs_crate::StdLibFnArg {
380                #q0
381                #q1
382                #q2
383                }
384            });
385        }
386    }
387
388    let return_type_inner = match &ast.sig.output {
389        syn::ReturnType::Default => quote! { () },
390        syn::ReturnType::Type(_, ty) => {
391            // Get the inside of the result.
392            match &**ty {
393                syn::Type::Path(syn::TypePath { path, .. }) => {
394                    let path = &path.segments;
395                    if path.len() == 1 {
396                        let seg = &path[0];
397                        if seg.ident == "Result" {
398                            if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
399                                args,
400                                ..
401                            }) = &seg.arguments
402                            {
403                                if args.len() == 2 || args.len() == 1 {
404                                    let mut args = args.iter();
405                                    let ok = args.next().unwrap();
406                                    if let syn::GenericArgument::Type(ty) = ok {
407                                        let ty = unbox(ty.clone());
408                                        quote! { #ty }
409                                    } else {
410                                        quote! { () }
411                                    }
412                                } else {
413                                    quote! { () }
414                                }
415                            } else {
416                                quote! { () }
417                            }
418                        } else {
419                            let ty = unbox(*ty.clone());
420                            quote! { #ty }
421                        }
422                    } else {
423                        quote! { () }
424                    }
425                }
426                _ => {
427                    quote! { () }
428                }
429            }
430        }
431    };
432
433    let ret_ty_string = return_type_inner.to_string().replace(' ', "");
434    let return_type = if !ret_ty_string.is_empty() || ret_ty_string != "()" {
435        let ret_ty_string = rust_type_to_openapi_type(&ret_ty_string);
436        quote! {
437            let schema = generator.root_schema_for::<#return_type_inner>();
438            Some(#docs_crate::StdLibFnArg {
439                name: "".to_string(),
440                type_: #ret_ty_string.to_string(),
441                schema,
442                required: true,
443                label_required: true,
444                description: String::new(),
445                include_in_snippet: true,
446                snippet_value: None,
447                snippet_value_array: None,
448            })
449        }
450    } else {
451        quote! {
452            None
453        }
454    };
455
456    // For reasons that are not well understood unused constants that use the
457    // (default) call_site() Span do not trigger the dead_code lint. Because
458    // defining but not using an endpoint is likely a programming error, we
459    // want to be sure to have the compiler flag this. We force this by using
460    // the span from the name of the function to which this macro was applied.
461    let span = ast.sig.ident.span();
462    let const_struct = quote_spanned! {span=>
463        pub(crate) const #name_ident: #name_ident = #name_ident {};
464    };
465
466    let test_mod_name = format_ident!("test_examples_{}", fn_name_str);
467
468    // The final TokenStream returned will have a few components that reference
469    // `#name_ident`, the name of the function to which this macro was applied...
470    let stream = quote! {
471        #[cfg(test)]
472        mod #test_mod_name {
473            #(#test_code_blocks)*
474        }
475
476        // ... a struct type called `#name_ident` that has no members
477        #[allow(non_camel_case_types, missing_docs)]
478        #description_doc_comment
479        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars::JsonSchema, ts_rs::TS)]
480        #[ts(export)]
481        pub(crate) struct #name_ident {}
482        // ... a constant of type `#name` whose identifier is also #name_ident
483        #[allow(non_upper_case_globals, missing_docs)]
484        #description_doc_comment
485        #const_struct
486
487        fn #boxed_fn_name_ident(
488            exec_state: &mut crate::execution::ExecState,
489            args: crate::std::Args,
490        ) -> std::pin::Pin<
491            Box<dyn std::future::Future<Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>> + Send + '_>,
492        > {
493            Box::pin(#fn_name_ident(exec_state, args))
494        }
495
496        impl #docs_crate::StdLibFn for #name_ident
497        {
498            fn name(&self) -> String {
499                #name_str.to_string()
500            }
501
502            fn summary(&self) -> String {
503                #summary.to_string()
504            }
505
506            fn description(&self) -> String {
507                #description.to_string()
508            }
509
510            fn tags(&self) -> Vec<String> {
511                vec![#(#tags),*]
512            }
513
514            fn args(&self, inline_subschemas: bool) -> Vec<#docs_crate::StdLibFnArg> {
515                let mut settings = schemars::gen::SchemaSettings::openapi3();
516                // We set this to false so we can recurse them later.
517                settings.inline_subschemas = inline_subschemas;
518                let mut generator = schemars::gen::SchemaGenerator::new(settings);
519
520                vec![#(#arg_types),*]
521            }
522
523            fn return_value(&self, inline_subschemas: bool) -> Option<#docs_crate::StdLibFnArg> {
524                let mut settings = schemars::gen::SchemaSettings::openapi3();
525                // We set this to false so we can recurse them later.
526                settings.inline_subschemas = inline_subschemas;
527                let mut generator = schemars::gen::SchemaGenerator::new(settings);
528
529                #return_type
530            }
531
532            fn unpublished(&self) -> bool {
533                #unpublished
534            }
535
536            fn deprecated(&self) -> bool {
537                #deprecated
538            }
539
540            fn feature_tree_operation(&self) -> bool {
541                #feature_tree_operation
542            }
543
544            fn examples(&self) -> Vec<(String, bool)> {
545                #code_blocks
546            }
547
548            fn std_lib_fn(&self) -> crate::std::StdFn {
549                #boxed_fn_name_ident
550            }
551
552            fn clone_box(&self) -> Box<dyn #docs_crate::StdLibFn> {
553                Box::new(self.clone())
554            }
555        }
556
557        #item
558    };
559
560    // Prepend the usage message if any errors were detected.
561    if !errors.is_empty() {
562        errors.insert(0, Error::new_spanned(&ast.sig, ""));
563    }
564
565    Ok((stream, errors))
566}
567
568#[allow(dead_code)]
569fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
570    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
571    quote!(#(#compile_errors)*)
572}
573
574fn get_crate(var: Option<String>) -> proc_macro2::TokenStream {
575    if let Some(s) = var {
576        if let Ok(ts) = syn::parse_str(s.as_str()) {
577            return ts;
578        }
579    }
580    quote!(crate::docs)
581}
582
583#[derive(Debug)]
584struct DocInfo {
585    pub summary: Option<String>,
586    pub description: Option<String>,
587    pub code_blocks: Vec<(String, bool)>,
588}
589
590fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> DocInfo {
591    let doc = syn::Ident::new("doc", proc_macro2::Span::call_site());
592    let raw_lines = attrs.iter().flat_map(|attr| {
593        if let syn::Meta::NameValue(nv) = &attr.meta {
594            if nv.path.is_ident(&doc) {
595                if let syn::Expr::Lit(syn::ExprLit {
596                    lit: syn::Lit::Str(s), ..
597                }) = &nv.value
598                {
599                    return normalize_comment_string(s.value());
600                }
601            }
602        }
603        Vec::new()
604    });
605
606    // Parse any code blocks from the doc string.
607    let mut code_blocks: Vec<(String, bool)> = Vec::new();
608    let mut code_block: Option<(String, bool)> = None;
609    let mut parsed_lines = Vec::new();
610    for line in raw_lines {
611        if line.starts_with("```") {
612            if let Some((inner_code_block, norun)) = code_block {
613                code_blocks.push((inner_code_block.trim().to_owned(), norun));
614                code_block = None;
615            } else {
616                let norun = line.contains("kcl,norun") || line.contains("kcl,no_run");
617                code_block = Some((String::new(), norun));
618            }
619
620            continue;
621        }
622        if let Some((code_block, _)) = &mut code_block {
623            code_block.push_str(&line);
624            code_block.push('\n');
625        } else {
626            parsed_lines.push(line);
627        }
628    }
629
630    if let Some((code_block, norun)) = code_block {
631        code_blocks.push((code_block.trim().to_string(), norun));
632    }
633
634    let mut summary = None;
635    let mut description: Option<String> = None;
636    for line in parsed_lines {
637        if line.is_empty() {
638            if let Some(desc) = &mut description {
639                // Handle fully blank comments as newlines we keep.
640                if !desc.is_empty() && !desc.ends_with('\n') {
641                    if desc.ends_with(' ') {
642                        desc.pop().unwrap();
643                    }
644                    desc.push_str("\n\n");
645                }
646            } else if summary.is_some() {
647                description = Some(String::new());
648            }
649            continue;
650        }
651
652        if let Some(desc) = &mut description {
653            desc.push_str(&line);
654            // Default to space-separating comment fragments.
655            desc.push(' ');
656            continue;
657        }
658
659        if summary.is_none() {
660            summary = Some(String::new());
661        }
662        match &mut summary {
663            Some(summary) => {
664                summary.push_str(&line);
665                // Default to space-separating comment fragments.
666                summary.push(' ');
667            }
668            None => unreachable!(),
669        }
670    }
671
672    // Trim the summary and description.
673    if let Some(s) = &mut summary {
674        while s.ends_with(' ') || s.ends_with('\n') {
675            s.pop().unwrap();
676        }
677
678        if s.is_empty() {
679            summary = None;
680        }
681    }
682
683    if let Some(d) = &mut description {
684        while d.ends_with(' ') || d.ends_with('\n') {
685            d.pop().unwrap();
686        }
687
688        if d.is_empty() {
689            description = None;
690        }
691    }
692
693    DocInfo {
694        summary,
695        description,
696        code_blocks,
697    }
698}
699
700fn normalize_comment_string(s: String) -> Vec<String> {
701    s.split('\n')
702        .map(|s| {
703            // Rust-style comments are intrinsically single-line.
704            // We only want to trim a single space character from the start of
705            // a line, and only if it's the first character.
706            s.strip_prefix(' ').unwrap_or(s).trim_end().to_owned()
707        })
708        .collect()
709}
710
711/// Represent an item without concern for its body which may (or may not)
712/// contain syntax errors.
713#[derive(Clone)]
714struct ItemFnForSignature {
715    pub attrs: Vec<Attribute>,
716    pub vis: Visibility,
717    pub sig: Signature,
718    pub _block: proc_macro2::TokenStream,
719}
720
721impl Parse for ItemFnForSignature {
722    fn parse(input: ParseStream) -> syn::parse::Result<Self> {
723        let attrs = input.call(Attribute::parse_outer)?;
724        let vis: Visibility = input.parse()?;
725        let sig: Signature = input.parse()?;
726        let block = input.parse()?;
727        Ok(ItemFnForSignature {
728            attrs,
729            vis,
730            sig,
731            _block: block,
732        })
733    }
734}
735
736fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
737    let mut ty_string = t
738        .replace("& 'a", "")
739        .replace('&', "")
740        .replace("mut", "")
741        .replace("< 'a >", "")
742        .replace(' ', "");
743    if ty_string.starts_with("ExecState") {
744        ty_string = "ExecState".to_string();
745    }
746    if ty_string.starts_with("Args") {
747        ty_string = "Args".to_string();
748    }
749    let ty_string = ty_string.trim().to_string();
750    let ty_ident = if ty_string.starts_with("Vec<") {
751        let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
752        let (_, ty_ident) = clean_ty_string(&ty_string);
753        quote! {
754           Vec<#ty_ident>
755        }
756    } else if ty_string.starts_with("kittycad::types::") {
757        let ty_string = ty_string.trim_start_matches("kittycad::types::").trim_end_matches('>');
758        let ty_ident = format_ident!("{}", ty_string);
759        quote! {
760           kittycad::types::#ty_ident
761        }
762    } else if ty_string.starts_with("Option<") {
763        let ty_string = ty_string.trim_start_matches("Option<").trim_end_matches('>');
764        let (_, ty_ident) = clean_ty_string(&ty_string);
765        quote! {
766           Option<#ty_ident>
767        }
768    } else if let Some((inner_array_type, num)) = parse_array_type(&ty_string) {
769        let ty_string = inner_array_type.to_owned();
770        let (_, ty_ident) = clean_ty_string(&ty_string);
771        quote! {
772           [#ty_ident; #num]
773        }
774    } else if ty_string.starts_with("Box<") {
775        let ty_string = ty_string.trim_start_matches("Box<").trim_end_matches('>');
776        let (_, ty_ident) = clean_ty_string(&ty_string);
777        quote! {
778           #ty_ident
779        }
780    } else {
781        let ty_ident = format_ident!("{}", ty_string);
782        quote! {
783           #ty_ident
784        }
785    };
786
787    (ty_string, ty_ident)
788}
789
790fn rust_type_to_openapi_type(t: &str) -> String {
791    let mut t = t.to_string();
792    // Turn vecs into arrays.
793    // TODO: handle nested types
794    if t.starts_with("Vec<") {
795        t = t.replace("Vec<", "[").replace('>', "]");
796    }
797    if t.starts_with("Box<") {
798        t = t.replace("Box<", "").replace('>', "");
799    }
800    if t.starts_with("Option<") {
801        t = t.replace("Option<", "").replace('>', "");
802    }
803
804    if t == "[TyF64;2]" {
805        return "Point2d".to_owned();
806    }
807    if t == "[TyF64;3]" {
808        return "Point3d".to_owned();
809    }
810
811    if let Some((inner_type, _length)) = parse_array_type(&t) {
812        t = format!("[{inner_type}]")
813    }
814
815    if t == "f64" || t == "TyF64" || t == "u32" || t == "NonZeroU32" {
816        return "number".to_string();
817    } else if t == "str" || t == "String" {
818        return "string".to_string();
819    } else {
820        return t.replace("f64", "number").replace("TyF64", "number").to_string();
821    }
822}
823
824fn parse_array_type(type_name: &str) -> Option<(&str, usize)> {
825    static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[([a-zA-Z0-9<>]+); ?(\d+)\]").unwrap());
826    let cap = RE.captures(type_name)?;
827    let inner_type = cap.get(1)?;
828    let length = cap.get(2)?.as_str().parse().ok()?;
829    Some((inner_type.as_str(), length))
830}
831
832// For each kcl code block, we want to generate a test that checks that the
833// code block is valid kcl code and compiles and executes.
834fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> proc_macro2::TokenStream {
835    let test_name = format_ident!("kcl_test_example_{}{}", fn_name, index);
836    let test_name_mock = format_ident!("test_mock_example_{}{}", fn_name, index);
837    let output_test_name_str = format!("serial_test_example_{}{}", fn_name, index);
838
839    quote! {
840        #[tokio::test(flavor = "multi_thread")]
841        async fn #test_name_mock() -> miette::Result<()> {
842            let program = crate::Program::parse_no_errs(#code_block).unwrap();
843            let ctx = crate::ExecutorContext {
844                engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
845                fs: std::sync::Arc::new(crate::fs::FileManager::new()),
846                stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
847                settings: Default::default(),
848                context_type: crate::execution::ContextType::Mock,
849            };
850
851            if let Err(e) = ctx.run(&program, &mut crate::execution::ExecState::new(&ctx)).await {
852                    return Err(miette::Report::new(crate::errors::Report {
853                        error: e.error,
854                        filename: format!("{}{}", #fn_name, #index),
855                        kcl_source: #code_block.to_string(),
856                    }));
857            }
858            Ok(())
859        }
860
861        #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
862        async fn #test_name() -> miette::Result<()> {
863            let code = #code_block;
864            // Note, `crate` must be kcl_lib
865            let result = match crate::test_server::execute_and_snapshot(code, None).await {
866                Err(crate::errors::ExecError::Kcl(e)) => {
867                    return Err(miette::Report::new(crate::errors::Report {
868                        error: e.error,
869                        filename: format!("{}{}", #fn_name, #index),
870                        kcl_source: #code_block.to_string(),
871                    }));
872                }
873                Err(other_err)=> panic!("{}", other_err),
874                Ok(img) => img,
875            };
876            twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #output_test_name_str), &result, 0.99);
877            Ok(())
878        }
879    }
880}