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