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