ambient_project_macro_common 0.2.1

Common implementation of a proc-macro that generates boilerplate from an `ambient.toml`.
Documentation
use super::{
    tree::{Tree, TreeNode, TreeNodeInner, TreeNodeNamespace},
    util, Context,
};
use ambient_project::{Component, ComponentType, IdentifierPath, IdentifierPathBuf};
use proc_macro2::TokenStream;
use quote::quote;
use thiserror::Error;

pub fn tree_to_token_stream(
    tree: &Tree<Component>,
    context: &Context,
    project_path: IdentifierPath,
) -> anyhow::Result<proc_macro2::TokenStream> {
    let tree_output = to_token_stream(
        tree.root(),
        context,
        |context, ns, ts| match context {
            Context::Host => {
                let namespace_path = IdentifierPath(ns.path.split_first().unwrap().1).to_string();
                quote! {
                    use glam::{Vec2, Vec3, Vec4, UVec2, UVec3, UVec4, Mat4, Quat};
                    use crate::{EntityId, Debuggable, Networked, Store, Resource, MaybeResource, Name, Description};
                    crate::components!(#namespace_path, {
                        #ts
                    });
                }
            }
            Context::Guest { api_path, .. } => quote! {
                use #api_path::{once_cell::sync::Lazy, ecs::{Component, __internal_get_component}};
                #ts
            },
        },
        project_path,
    )?;

    match context {
        Context::Host => {
            let init_all_components = {
                fn get_namespaces<'a>(
                    ns: &'a TreeNodeNamespace<Component>,
                    path: IdentifierPath<'a>,
                ) -> Vec<IdentifierPath<'a>> {
                    let mut result = vec![];
                    if ns
                        .children
                        .iter()
                        .any(|child| matches!(child.1.inner, TreeNodeInner::Other(_)))
                    {
                        result.push(path);
                    }
                    for child in &ns.children {
                        if let TreeNodeInner::Namespace(ns) = &child.1.inner {
                            result.append(&mut get_namespaces(ns, child.1.path.as_path()));
                        }
                    }
                    result
                }

                let namespaces = get_namespaces(tree.root_namespace(), IdentifierPath(&[]));

                quote! {
                    pub fn init() {
                        #(
                            #namespaces::init_components();
                        )*
                    }
                }
            };

            Ok(quote! {
                #tree_output
                #init_all_components
            })
        }
        Context::Guest { .. } => Ok(tree_output),
    }
}

fn to_token_stream(
    node: &TreeNode<Component>,
    context: &Context,
    wrapper: impl Fn(&Context, &TreeNode<Component>, TokenStream) -> TokenStream + Copy,
    project_path: IdentifierPath,
) -> anyhow::Result<TokenStream> {
    util::tree_to_token_stream(
        node,
        context,
        wrapper,
        |node, context, wrapper| to_token_stream(node, context, wrapper, project_path),
        |name, component, context| {
            let name_ident: syn::Path = syn::parse_str(name)?;
            let name_uppercase_ident: syn::Path = syn::parse_str(&name.to_ascii_uppercase())?;
            let component_ty = type_to_token_stream(&component.type_, context, false)?;

            let mut doc_comment = format!(
                "**{}**",
                component.name.as_ref().map(|x| x as &str).unwrap_or(name)
            );

            if let Some(desc) = &component.description {
                if !desc.is_empty() {
                    doc_comment += &format!(": {}", desc.replace('\n', "\n\n"));
                }
            }

            // Metadata
            if !component.attributes.is_empty() {
                doc_comment += &format!("\n\n*Attributes*: {}", component.attributes.join(", "))
            }
            if let Some(default) = component.default.as_ref() {
                doc_comment += &format!("\n\n*Suggested Default*: {default}")
            }

            let id =
                IdentifierPathBuf::from_iter(project_path.iter().chain(node.path.iter()).cloned())
                    .to_string();
            let doc_comment = doc_comment.trim();

            match context {
                Context::Host => {
                    let attrs = component
                        .attributes
                        .iter()
                        .map(|a| syn::Ident::new(a, proc_macro2::Span::call_site()));

                    let description = &component.description;

                    Ok(quote! {
                        #[doc = #doc_comment]
                        @[#(#attrs,)* Name[#name], Description[#description]]
                        #name_ident: #component_ty,
                    })
                }
                Context::Guest { .. } => Ok(quote! {
                    static #name_uppercase_ident: Lazy< Component< #component_ty > > = Lazy::new(|| __internal_get_component(#id));
                    #[doc = #doc_comment]
                    pub fn #name_ident() -> Component< #component_ty > { *#name_uppercase_ident }
                }),
            }
        },
    )
}

#[derive(Error, Debug)]
#[allow(clippy::enum_variant_names)]
pub enum TypeTokenStreamError {
    #[error("invalid primitive type")]
    InvalidPrimitiveType,
    #[error("invalid container type")]
    InvalidContainerType,
    #[error("invalid element type")]
    InvalidElementType,
}

pub fn type_to_token_stream(
    ty: &ComponentType,
    context: &Context,
    with_turbofish: bool,
) -> Result<proc_macro2::TokenStream, TypeTokenStreamError> {
    match ty {
        ComponentType::String(ty) => convert_primitive_type_to_rust_type(ty, context)
            .ok_or(TypeTokenStreamError::InvalidPrimitiveType),
        ComponentType::ContainerType {
            type_,
            element_type,
        } => {
            if let Some(element_type) = element_type {
                let container_ty = convert_container_type_to_rust_type(type_)
                    .ok_or(TypeTokenStreamError::InvalidContainerType)?;

                let element_ty = convert_primitive_type_to_rust_type(element_type, context)
                    .ok_or(TypeTokenStreamError::InvalidElementType)?;

                if with_turbofish {
                    Ok(quote! { #container_ty :: < #element_ty > })
                } else {
                    Ok(quote! { #container_ty < #element_ty > })
                }
            } else {
                Ok(convert_primitive_type_to_rust_type(type_, context)
                    .ok_or(TypeTokenStreamError::InvalidPrimitiveType)?)
            }
        }
    }
}

fn convert_primitive_type_to_rust_type(
    ty: &str,
    context: &Context,
) -> Option<proc_macro2::TokenStream> {
    let fully_qualified_prefix = match context {
        Context::Host => quote! {},
        Context::Guest {
            api_path,
            fully_qualified_path,
        } => {
            if *fully_qualified_path {
                quote! { #api_path::global:: }
            } else {
                quote! {}
            }
        }
    };

    match ty {
        "Empty" => Some(quote! {()}),
        "Bool" => Some(quote! {bool}),
        "EntityId" => Some(quote! {#fully_qualified_prefix EntityId}),
        "F32" => Some(quote! {f32}),
        "F64" => Some(quote! {f64}),
        "Mat4" => Some(quote! {#fully_qualified_prefix Mat4}),
        "I32" => Some(quote! {i32}),
        "Quat" => Some(quote! {#fully_qualified_prefix Quat}),
        "String" => Some(quote! {String}),
        "U8" => Some(quote! {u8}),
        "U32" => Some(quote! {u32}),
        "U64" => Some(quote! {u64}),
        "Vec2" => Some(quote! {#fully_qualified_prefix Vec2}),
        "Vec3" => Some(quote! {#fully_qualified_prefix Vec3}),
        "Vec4" => Some(quote! {#fully_qualified_prefix Vec4}),
        "Uvec2" => Some(quote! {#fully_qualified_prefix UVec2}),
        "Uvec3" => Some(quote! {#fully_qualified_prefix UVec3}),
        "Uvec4" => Some(quote! {#fully_qualified_prefix UVec4}),
        _ => None,
    }
}

fn convert_container_type_to_rust_type(ty: &str) -> Option<proc_macro2::TokenStream> {
    match ty {
        "Vec" => Some(quote! {Vec}),
        "Option" => Some(quote! {Option}),
        _ => None,
    }
}