witgen_macro_helper 0.7.0

Macros helpers for witgen which is a library to help you generate wit definitions in a wit file for WebAssembly
Documentation
use std::{fmt::Write, path::PathBuf};

use anyhow::{bail, Context, Result};
use cargo_metadata::MetadataCommand;
use heck::ToKebabCase;
use syn::{Attribute, ItemEnum, ItemFn, ItemStruct, ItemType, Lit, ReturnType, Type};

use crate::{is_known_keyword, ToWitType};

pub fn get_target_dir() -> PathBuf {
    let metadata = MetadataCommand::new()
        .exec()
        .expect("cannot fetch cargo metadata");

    metadata.target_directory.join("witgen").into()
}


/// Generate a wit record
/// ```rust
/// /// Document String
/// struct FooRecord {
///    a: string,
///    /// Comment field
///    b: Option<i32>,
/// }
/// ```
/// becomes
/// ```ts
/// /// Document String
/// record foo-record {
///   a: string,
///   b: option<s32>,
/// }
/// ```
/// 
pub fn gen_wit_struct(strukt: &ItemStruct) -> Result<String> {
    if !strukt.generics.params.is_empty() {
        bail!("doesn't support generic parameters with witgen");
    }

    let struct_name = strukt.ident.to_string().to_kebab_case();
    is_known_keyword(&struct_name)?;

    let mut is_tuple_struct = false;
    let comment = get_doc_comment(&strukt.attrs)?;
    let attrs = strukt
        .fields
        .iter()
        .map(|field| {
            let field_name = match &field.ident {
                Some(ident) => ident.to_string().to_kebab_case() + ": ",
                None => {
                    is_tuple_struct = true;
                    String::new()
                }
            };
            is_known_keyword(&field_name)?;

            let comment = get_doc_comment(&field.attrs)?;

            let field_wit = format!("{}{}", field_name, field.ty.to_wit()?);
            match comment {
                Some(comment) => Ok(format!("{}\t{}", comment, field_wit)),
                None => Ok(field_wit),
            }
        })
        .collect::<Result<Vec<String>>>()?;
    let attrs = if is_tuple_struct {
        attrs.join(", ")
    } else {
        attrs.join(",\n\t")
    };

    let content = if is_tuple_struct {
        format!("type {} = tuple<{}>\n", struct_name, attrs)
    } else {
        format!(
            r#"record {} {{
    {}
}}
"#,
            struct_name, attrs
        )
    };

    match comment {
        Some(comment) => Ok(format!("{}{}", comment, content)),
        None => Ok(content),
    }
}

/// Generate a wit enum
/// ```rust
/// /// Top comment
/// enum MyEnum {
///   /// comment case
///   Unit,
///   TupleVariant(String, i32)
/// }
/// ```
/// 
/// ```ts
/// /// Top comment
/// variant my-enum {
///   /// comment case
///   unit,
///   tuple-variant(tuple<string, s32>),
/// }
/// ```
pub fn gen_wit_enum(enm: &ItemEnum) -> Result<String> {
    if !enm.generics.params.is_empty() {
        bail!("doesn't support generic parameters with witgen");
    }

    let enm_name = enm.ident.to_string().to_kebab_case();
    is_known_keyword(&enm_name)?;

    let comment = get_doc_comment(&enm.attrs)?;
    let variants = enm
        .variants
        .iter()
        .map(|variant| match &variant.fields {
            syn::Fields::Named(_named) => Err(anyhow::anyhow!(
                "named variant fields are not already supported"
            )),
            syn::Fields::Unnamed(unamed) => {
                let comment = get_doc_comment(&variant.attrs)?;
                let fields = unamed
                    .unnamed
                    .iter()
                    .map(|field| field.ty.to_wit())
                    .collect::<Result<Vec<String>>>()?
                    .join(", ");
                let variant_ident = variant.ident.to_string().to_kebab_case();
                is_known_keyword(&variant_ident)?;

                let variant_wit = if unamed.unnamed.len() > 1 {
                    format!("{}(tuple<{}>),", variant_ident, fields)
                } else {
                    format!("{}({}),", variant_ident, fields)
                };

                match comment {
                    Some(comment) => Ok(format!("{}\t{}", comment, variant_wit)),
                    None => Ok(variant_wit),
                }
            }
            syn::Fields::Unit => {
                let comment = get_doc_comment(&variant.attrs)?;
                let variant_wit = variant.ident.to_string().to_kebab_case() + ",";

                match comment {
                    Some(comment) => Ok(format!("{}\t{}", comment, variant_wit)),
                    None => Ok(variant_wit),
                }
            }
        })
        .collect::<Result<Vec<String>>>()?
        .join("\n\t");
    let content = format!(
        r#"variant {} {{
    {}
}}
"#,
        enm_name, variants
    );

    match comment {
        Some(comment) => Ok(format!("{}{}", comment, content)),
        None => Ok(content),
    }
}

/// Generate a wit function
/// ```rust
/// /// Document String
/// fn foo(a: string, b: Option<i32>) -> Result<string> { Ok(a)}
/// ```
/// becomes
/// ```ts
/// /// Document String
/// foo: function(a: string, b: option<s32>) -> expected<string>
/// ```
/// 
pub fn gen_wit_function(func: &ItemFn) -> Result<String> {
    let signature = &func.sig;
    let comment = get_doc_comment(&func.attrs)?;
    let func_name_fmt = func.sig.ident.to_string().to_kebab_case();
    is_known_keyword(&func_name_fmt)?;

    let mut content = String::new();
    write!(&mut content, "{}: function(", func_name_fmt)
        .context("cannot write function declaration in wit")?;
    let fn_args: Vec<String> = signature
        .inputs
        .iter()
        .map(|fn_arg| match fn_arg {
            syn::FnArg::Receiver(_) => bail!("does not support methods"),
            syn::FnArg::Typed(typed_pat) => {
                let pat = match &*typed_pat.pat {
                    syn::Pat::Ident(ident) => ident.ident.to_string().to_kebab_case(),
                    _ => bail!("can't handle this kind of fn argument"),
                };
                is_known_keyword(&pat)?;

                let ty = typed_pat.ty.to_wit()?;
                Ok(format!("{}: {}", pat, ty))
            }
        })
        .collect::<Result<Vec<String>>>()?;
    write!(&mut content, "{})", fn_args.join(", ")).context("cannot write end of func params")?;

    if let ReturnType::Type(_, return_ty) = &signature.output {
        if let Type::Tuple(tuple) = return_ty.as_ref() {
            let tuple_fields = tuple
                .elems
                .iter()
                .map(|f| f.to_wit())
                .collect::<Result<Vec<String>>>()?
                .join(", ");
            writeln!(&mut content, " -> ({})", tuple_fields).context("cannot write return type")?;
        } else {
            writeln!(&mut content, " -> {}", return_ty.to_wit()?)
                .context("cannot write return type")?;
        }
    }

    match comment {
        Some(comment) => Ok(format!("{}{}", comment, content)),
        None => Ok(content),
    }
}


/// Generate a wit type alias
/// ```rust
/// /// Document String
/// type foo = (String, option<bool>);
/// ```
/// becomes
/// ```ts
/// /// Document String
/// type foo = tuple<string, option<bool>>
/// ```
/// 
pub fn gen_wit_type_alias(type_alias: &ItemType) -> Result<String> {
    if !type_alias.generics.params.is_empty() {
        bail!("doesn't support generic parameters with witgen");
    }
    let comment = get_doc_comment(&type_alias.attrs)?;
    let ty = type_alias.ty.to_wit()?;
    let type_alias_ident = type_alias.ident.to_string().to_kebab_case();
    is_known_keyword(&type_alias_ident)?;

    let content = format!("type {} = {}\n", type_alias_ident, ty);

    match comment {
        Some(comment) => Ok(format!("{}{}", comment, content)),
        None => Ok(content),
    }
}

fn get_doc_comment(attrs: &[Attribute]) -> Result<Option<String>> {
    let mut comment = String::new();

    for attr in attrs {
        match &attr.parse_meta()? {
            syn::Meta::NameValue(name_val) if name_val.path.is_ident("doc") => {
                if let Lit::Str(lit_str) = &name_val.lit {
                    writeln!(&mut comment, "/// {}", lit_str.value())?;
                }
            }
            _ => {}
        }
    }

    Ok((!comment.is_empty()).then(|| comment))
}