tslink 0.4.2

Creates typescript definitions based on rust code
Documentation
use std::ops::Deref;

use crate::{
    context::Context,
    error::E,
    nature::{Composite, Nature, Referred, TypeTokenStream, VariableTokenStream},
};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_quote, ReturnType};
use syn::{Block, ImplItemFn, ItemFn};

pub enum FnItem<'a> {
    ItemFn(&'a mut ItemFn),
    ImplItemFn(&'a mut ImplItemFn),
}

impl FnItem<'_> {
    pub fn get_block(&self) -> &Block {
        match self {
            Self::ImplItemFn(item) => &item.block,
            Self::ItemFn(item) => &item.block,
        }
    }

    pub fn set_block(&mut self, block: Block) {
        match self {
            Self::ImplItemFn(item) => {
                item.block = block;
            }
            Self::ItemFn(item) => {
                item.block = Box::new(block);
            }
        }
    }

    pub fn set_output(&mut self, output: ReturnType) {
        match self {
            Self::ImplItemFn(item) => {
                item.sig.output = output;
            }
            Self::ItemFn(item) => {
                item.sig.output = output;
            }
        }
    }
}

fn split_fn_out(out: &Option<Box<Nature>>) -> (Option<Nature>, Option<Nature>) {
    if let Some(out) = out {
        if let Nature::Composite(Composite::Result(_, res, err, _, _)) = out.deref() {
            (
                res.clone().map(|n| n.deref().clone()),
                err.clone().map(|n| n.deref().clone()),
            )
        } else {
            (Some(out.deref().clone()), None)
        }
    } else {
        (None, None)
    }
}

fn bind(item: &mut FnItem, name: &str, context: &Context, fn_nature: &Nature) -> Result<(), E> {
    let (args, out) = if let Nature::Composite(Composite::Func(_, args, out, _, _)) = fn_nature {
        (args, out)
    } else {
        return Err(E::Parsing(format!("Fail to parse fn/method \"{name}\"")));
    };
    let bindings = context.get_bound_args();
    let mut unknown: Vec<String> = vec![];
    bindings.iter().for_each(|(name, _)| {
        if !args.iter().any(|nature| {
            if let Nature::Referred(Referred::FuncArg(n, _, _, _)) = nature {
                name == n
            } else {
                false
            }
        }) && name != "result"
        {
            unknown.push(name.to_owned());
        }
    });
    if !unknown.is_empty() {
        return Err(E::Parsing(format!(
            "Unknown arguments to bind: {}",
            unknown.join(", ")
        )));
    }
    let (fn_res, fn_err) = split_fn_out(out);
    let fn_err_type_ref = if let Some(fn_err) = fn_err.as_ref() {
        Some(fn_err.type_token_stream()?)
    } else {
        None
    };
    let bindings = bindings
        .iter()
        .map(|(name, ref_name)| {
            let varname = format_ident!("{}", name);
            let refname = format_ident!("{}", ref_name);
            if let Some(fn_err_type_ref) = fn_err_type_ref.as_ref() {
                quote! {
                    #[allow(unused_mut)]
                    let mut #varname: #refname = serde_json::from_str(&#varname).map_err(|e| Into::<#fn_err_type_ref>::into(e))?;
                }
            } else {
                quote! {
                    #[allow(unused_mut)]
                    let mut #varname: #refname = serde_json::from_str(&#varname).expect("Parsing from JSON string")?;
                }
            }
        })
        .collect::<Vec<TokenStream>>();
    if !bindings.is_empty() {
        let stmts = &item.get_block().stmts;
        let block = quote! {
            use serde_json;
            #(#bindings)*;
            #(#stmts)*
        };
        item.set_block(parse_quote! {{#block}});
    }
    let result_as_json = context.result_as_json()?;
    let error_as_json = context.error_as_json()?;
    if result_as_json || error_as_json {
        let (fn_res, fn_err) = (
            fn_res.ok_or(E::Parsing("Fail to get Ok option of Result. If result defined as JSON, function/method should return Result<T,E>".to_string()))?,
            fn_err.ok_or(E::Parsing("Fail to get Err option of Result. If result defined as JSON, function/method should return Result<T,E>".to_string()))?,
        );
        let res_rust_type = fn_res.type_token_stream()?;
        let err_rust_type = fn_err.type_token_stream()?;
        if result_as_json && error_as_json {
            let res_token = fn_res.variable_token_stream("res", None)?;
            let err_token = fn_err.variable_token_stream("err", None)?;
            let stmts = &item.get_block().stmts;
            let block = quote! {
                let result: Result<#res_rust_type, #err_rust_type> = (move || {
                    #(#stmts)*
                })();
                match result {
                    Ok(res) => Ok(#res_token),
                    Err(err) => Err(#err_token)
                }
            };
            item.set_block(parse_quote! {{#block}});
            let output = quote! { -> Result<String, String>};
            item.set_output(parse_quote! {#output});
        } else if result_as_json {
            let res_token = fn_res.variable_token_stream("res", Some(&fn_err))?;
            let err_token = {
                let err = format_ident!("{}", "err");
                quote! {#err}
            };
            let stmts = &item.get_block().stmts;
            let block = quote! {
                match {#(#stmts)*} {
                    Ok(res) => Ok(#res_token),
                    Err(err) => Err(#err_token)
                }
            };
            item.set_block(parse_quote! {{#block}});
            let output = quote! { -> Result<String, #err_rust_type>};
            item.set_output(parse_quote! {#output});
        } else if error_as_json {
            let res_token = {
                let res = format_ident!("{}", "res");
                quote! {#res}
            };
            let err_token = fn_err.variable_token_stream("err", None)?;
            let stmts = &item.get_block().stmts;
            let block = quote! {
                let result: Result<#res_rust_type, #err_rust_type> = (move || {
                    #(#stmts)*
                })();
                match result {
                    Ok(res) => Ok(#res_token),
                    Err(err) => Err(#err_token)
                }
            };
            item.set_block(parse_quote! {{#block}});
            let output = quote! { -> Result<#res_rust_type, String>};
            item.set_output(parse_quote! {#output});
        }
    }
    Ok(())
}

pub fn bind_fn(
    item: &mut ItemFn,
    name: &str,
    context: &Context,
    fn_nature: &Nature,
) -> Result<(), E> {
    bind(&mut FnItem::ItemFn(item), name, context, fn_nature)
}

pub fn bind_impl_fn(
    item: &mut ImplItemFn,
    name: &str,
    context: &Context,
    fn_nature: &Nature,
) -> Result<(), E> {
    bind(&mut FnItem::ImplItemFn(item), name, context, fn_nature)
}