dodo-bindgen-codegen 2.0.0

TypeScript codegen for dodo_bindgen
Documentation
use std::collections::BTreeMap;

use crate::TsMethod;
use crate::util::*;
use syn::Ident;

#[derive(Clone, Debug, Default)]
pub struct TsClass {
    pub methods: Vec<TsMethod>,
    pub drop_symbol: Option<Ident>,
    pub snippets: Vec<String>,
}

#[derive(Clone, Debug, Default)]
pub struct ClassDefs {
    pub store: BTreeMap<Ident, TsClass>,
}

impl ClassDefs {
    pub fn print(&self) -> String {
        let mut tokens = proc_macro2::TokenStream::new();

        for (ident, class) in &self.store {
            let dispose = class.drop_symbol.as_ref().map(|drop_symbol| {
                quote! {
                    [Symbol.dispose]() {
                        __dodo_dispose(this, symbols.#drop_symbol);
                    }
                }
            });
            let methods = class.methods.iter().map(TsMethod::print);
            let snippets = class
                .snippets
                .iter()
                .map(|snippet| raw(format!("\n{snippet}\n")));

            tokens.extend(quote! {
                export class #ident extends RustPrototype<#ident> {
                    #dispose
                    #(#methods)*
                    #(#snippets)*
                }
            });
        }

        tokens.to_string()
    }
}

/* -------------------------------------------------------------------------- */

// MARK: tests

#[cfg(test)]
mod tests {
    use crate::{CodegenOpts, TsModule};
    use dodo_bindgen_rust_ir::{
        Abi, Arg, HelperSymbol, Item, ItemClass, ItemFn, ItemHelper, ItemMethod, ItemTs, Receiver,
        Type, TypeNumeric,
    };

    fn generate(items: Vec<Item>) -> String {
        crate::util::deno_fmt(TsModule::new(items, &opts()).generate_single(&opts()))
    }

    fn opts() -> CodegenOpts {
        CodegenOpts {
            file_name: "mod.ts".to_string(),
            lib_name: "lib".to_string(),
            lazy: false,
        }
    }

    fn class(name: &str, ty: Type) -> Item {
        Item::Class(ItemClass {
            module_path: "crate".to_string(),
            name: name.to_string(),
            drop_symbol: format!("__dodo_crate_{name}_drop"),
            doc: String::new(),
            ty,
        })
    }

    fn method(class: &str, name: &str, receiver: Receiver, inputs: Vec<Arg>, output: Type) -> Item {
        Item::Method(ItemMethod {
            class: class.to_string(),
            receiver,
            func: ItemFn {
                name: name.to_string(),
                symbol: format!("__dodo_crate_{class}_{name}"),
                doc: String::new(),
                non_blocking: false,
                inputs,
                output_abi: if output == Type::Void {
                    Abi::Void
                } else {
                    Abi::Pointer
                },
                output,
            },
        })
    }

    fn arg(ty: Type, abi: Abi) -> Arg {
        Arg {
            name: "arg".to_string(),
            ty,
            abi,
        }
    }

    fn object(name: &str) -> Type {
        Type::Object {
            module_path: "crate".to_string(),
            name: name.to_string(),
        }
    }

    #[test]
    fn test_print() {
        let items = vec![
            class("CustomType", object("CustomType")),
            method(
                "CustomType",
                "test_fn",
                Receiver::Ref,
                vec![arg(Type::Ref(Box::new(object("CustomType"))), Abi::Pointer)],
                Type::Void,
            ),
            Item::Ts(ItemTs {
                class: "CustomType".to_string(),
                code: "get answer(): number { return 42; }".to_string(),
            }),
        ];
        insta::assert_snapshot!(generate(items));
    }

    fn echo(ty: Type) -> Item {
        Item::Fn(ItemFn {
            name: "echo".to_string(),
            symbol: "__dodo_crate_echo".to_string(),
            doc: String::new(),
            non_blocking: false,
            inputs: vec![arg(ty, Abi::Pointer)],
            output: Type::Void,
            output_abi: Abi::Void,
        })
    }

    #[test]
    fn foreign_classes_map_types_and_prune() {
        let items = vec![
            class("RustString", Type::String),
            method("RustString", "new", Receiver::Static, vec![], Type::String),
            class(
                "RustBytes",
                Type::Vec(Box::new(Type::Numeric(TypeNumeric::U8))),
            ),
            method("RustBytes", "new", Receiver::Static, vec![], Type::Void),
            echo(Type::Box(Box::new(Type::String))),
        ];
        insta::assert_snapshot!(generate(items));
    }

    #[test]
    fn new_becomes_constructor() {
        let items = vec![
            class("Counter", object("Counter")),
            method(
                "Counter",
                "new",
                Receiver::Static,
                vec![arg(Type::Numeric(TypeNumeric::U32), Abi::U32)],
                object("Counter"),
            ),
            method(
                "Counter",
                "fork",
                Receiver::Ref,
                vec![arg(Type::Ref(Box::new(object("Counter"))), Abi::Pointer)],
                object("Counter"),
            ),
        ];
        insta::assert_snapshot!(generate(items));
    }

    #[test]
    fn receivers_reject_moved_values() {
        let this = || arg(object("Encoder"), Abi::Pointer);
        let items = vec![
            class("Encoder", object("Encoder")),
            method(
                "Encoder",
                "len",
                Receiver::Ref,
                vec![this()],
                Type::Numeric(TypeNumeric::U32),
            ),
            method(
                "Encoder",
                "append_frame",
                Receiver::RefMut,
                vec![this()],
                Type::Void,
            ),
            method(
                "Encoder",
                "finish",
                Receiver::Owned,
                vec![this()],
                Type::Void,
            ),
        ];
        insta::assert_snapshot!(generate(items));
    }

    fn non_blocking(mut item: Item) -> Item {
        if let Item::Method(method) = &mut item {
            method.func.non_blocking = true;
        }
        item
    }

    #[test]
    fn non_blocking_receivers_borrow() {
        let this = || arg(object("Decoder"), Abi::Pointer);
        let items = vec![
            class("Decoder", object("Decoder")),
            non_blocking(method(
                "Decoder",
                "next",
                Receiver::Ref,
                vec![this()],
                Type::Void,
            )),
            non_blocking(method(
                "Decoder",
                "seek",
                Receiver::RefMut,
                vec![this()],
                Type::Void,
            )),
            non_blocking(method(
                "Decoder",
                "finish",
                Receiver::Owned,
                vec![this()],
                Type::Void,
            )),
        ];
        insta::assert_snapshot!(generate(items));
    }
}