move-ts 0.4.0

Generates TypeScript code from a Move IDL.
Documentation
use anyhow::*;
use heck::ToPascalCase;
use move_idl::{IDLArgument, IDLModule, IDLScriptFunction};

use crate::{
    format::{gen_doc_string, indent},
    idl_type::{generate_idl_type_with_type_args, serialize_arg},
};

use super::{CodeText, Codegen, CodegenContext};

pub struct ScriptFunctionPayloadStruct<'info>(&'info ScriptFunctionType<'info>);

impl<'info> ScriptFunctionPayloadStruct<'info> {
    fn args_inline(&self, ctx: &CodegenContext) -> Result<CodeText> {
        Ok(ctx
            .try_join_with_separator(&self.0.script.args, "\n")?
            .indent())
    }

    fn type_args_inline(&self) -> CodeText {
        script_fn_type_args(&self.0.script.ty_args).indent()
    }
}

impl<'info> Codegen for ScriptFunctionPayloadStruct<'info> {
    fn generate_typescript(&self, ctx: &CodegenContext) -> Result<String> {
        Ok(CodeText::new_fields_export(
            &self.0.payload_args_type_name(),
            &format!(
                "{}{}",
                if self.0.script.args.is_empty() {
                    "".to_string()
                } else {
                    format!(
                        "{}\n",
                        indent(&format!("args: {{\n{}\n}};\n", self.args_inline(ctx)?))
                    )
                },
                if self.0.script.ty_args.is_empty() {
                    "".to_string()
                } else {
                    format!(
                        "{}\n",
                        indent(&format!("typeArgs: {{\n{}\n}};\n", self.type_args_inline()))
                    )
                },
            ),
        )
        .docs(&format!("Payload arguments for {}.", self.0.doc_link()))
        .into())
    }
}

pub struct ScriptFunctionType<'info> {
    type_name: String,
    module: &'info IDLModule,
    script: &'info IDLScriptFunction,
}

fn script_fn_type_args(args: &[String]) -> CodeText {
    args.iter()
        .map(|arg| format!("{}: string;", arg))
        .collect::<Vec<_>>()
        .join("\n")
        .into()
}

impl Codegen for IDLArgument {
    fn generate_typescript(&self, ctx: &CodegenContext) -> Result<String> {
        let doc = gen_doc_string(&format!("IDL type: `{:?}`", &self.ty));
        Ok(format!(
            "{}{}: {};",
            doc,
            self.name,
            &self.ty.generate_typescript(ctx)?
        ))
    }
}

impl<'info> ScriptFunctionType<'info> {
    pub fn new(module: &'info IDLModule, script: &'info IDLScriptFunction) -> Self {
        let type_name = script.name.to_pascal_case();
        Self {
            type_name,
            module,
            script,
        }
    }

    pub fn doc_link(&self) -> String {
        format!("{{@link entry.{}}}", self.script.name)
    }

    pub fn payload(&'info self) -> ScriptFunctionPayloadStruct<'info> {
        ScriptFunctionPayloadStruct(self)
    }

    pub fn generate_entry_payload_struct(&self, ctx: &CodegenContext) -> Result<CodeText> {
        let arguments = format!(
            "[{}]",
            self.script
                .args
                .iter()
                .map(|a| {
                    let ts_type = &generate_idl_type_with_type_args(&a.ty, ctx, &[], false)?;
                    Ok(format!("{}: {}", a.name, &ts_type))
                })
                .collect::<Result<Vec<_>>>()?
                .join(", ")
        );
        let type_arguments = format!(
            "[{}]",
            self.script
                .ty_args
                .iter()
                .map(|a| format!("{}: string", a))
                .collect::<Vec<_>>()
                .join(", ")
        );

        Ok(CodeText::new_fields_export(
            &self.type_name,
            &CodeText::try_join_with_separator(
                &[
                    CodeText::new("readonly type: \"script_function_payload\";"),
                    CodeText::new(&format!("readonly function: \"{}\";", self.full_name())),
                    CodeText::new(&format!("readonly arguments: {};", &arguments)),
                    CodeText::new(&format!("readonly type_arguments: {};", &type_arguments)),
                ],
                "\n",
            )?
            .indent()
            .append_newline()
            .to_string(),
        )
        .docs(&format!(
            "Script function payload for `{}`.{}",
            self.full_name(),
            self.script
                .doc
                .as_ref()
                .map(|s| format!("\n\n{}", s))
                .unwrap_or_default()
        )))
    }

    pub fn doc(&self) -> Option<String> {
        self.script.doc.clone()
    }

    pub fn name(&self) -> &str {
        &self.script.name
    }

    pub fn full_name(&self) -> String {
        format!("{}::{}", self.module.module_id, self.script.name)
    }

    pub fn payload_args_type_name(&'info self) -> String {
        format!("{}Args", self.type_name)
    }

    pub fn should_render_payload_struct(&'info self) -> bool {
        !(self.script.args.is_empty() && self.script.ty_args.is_empty())
    }
}

impl<'info> Codegen for ScriptFunctionType<'info> {
    fn generate_typescript(&self, ctx: &CodegenContext) -> Result<String> {
        let function = format!(
            "{}::{}",
            &self.module.module_id.short_str_lossless(),
            &self.script.name
        );
        let type_arguments = format!(
            "[{}]",
            self.script
                .ty_args
                .iter()
                .map(|a| format!("typeArgs.{}", a))
                .collect::<Vec<_>>()
                .join(", ")
        );
        let arguments = format!(
            "[{}]",
            self.script
                .args
                .iter()
                .map(|a| {
                    let inner = format!("args.{}", a.name);
                    serialize_arg(&inner, &a.ty, ctx)
                })
                .collect::<Result<Vec<_>>>()?
                .join(", ")
        );

        Ok(format!(
            r#"{}export const {} = ({}): payloads.{} => ({{
  type: "script_function_payload",
  function: "{}",
  type_arguments: {},
  arguments: {},
}});"#,
            self.script
                .doc
                .as_ref()
                .map(|doc| gen_doc_string(doc))
                .unwrap_or_default(),
            self.script.name,
            if self.should_render_payload_struct() {
                format!(
                    "{{ {} }}: mod.{}",
                    vec![
                        if self.script.args.is_empty() {
                            None
                        } else {
                            Some("args")
                        },
                        if self.script.ty_args.is_empty() {
                            None
                        } else {
                            Some("typeArgs")
                        },
                    ]
                    .into_iter()
                    .flatten()
                    .collect::<Vec<_>>()
                    .join(", "),
                    self.payload_args_type_name()
                )
            } else {
                "".to_string()
            },
            &self.type_name,
            &function,
            &type_arguments,
            &arguments
        ))
    }
}