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