trapeze-macros 0.7.6

Macros for trapeze
Documentation
use std::env;
use std::path::PathBuf;

use anyhow::{Context, Result};
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parse;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{bracketed, parse, Error, LitStr};
use tempfile::tempdir;
use trapeze_codegen::Config;

use crate::inline_includes::inline_includes;

fn env_path(var: &str) -> Result<PathBuf> {
    Ok(PathBuf::from(env::var_os(var).with_context(|| {
        format!("Environment variable `{var}` not set")
    })?))
}

struct Array<T> {
    elems: Punctuated<T, Comma>,
}

impl<T> Default for Array<T> {
    fn default() -> Self {
        let elems = Punctuated::default();
        Self { elems }
    }
}

impl<T: Parse> Parse for Array<T> {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let content;
        let _ = bracketed!(content in input);
        let elems = syn::punctuated::Punctuated::parse_terminated(&content)?;
        Ok(Self { elems })
    }
}

#[derive(Default)]
struct IncludeProtosInput {
    files: Array<LitStr>,
    includes: Option<Array<LitStr>>,
}

impl Parse for IncludeProtosInput {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let files = input.parse()?;
        let includes = None;
        if input.is_empty() {
            return Ok(Self { files, includes });
        }
        input.parse::<Comma>()?;
        if input.is_empty() {
            return Ok(Self { files, includes });
        }
        let includes = Some(input.parse()?);
        if input.is_empty() {
            return Ok(Self { files, includes });
        }
        input.parse::<Comma>()?;
        if input.is_empty() {
            return Ok(Self { files, includes });
        }
        Err(input.error("Unexpected token"))
    }
}

pub fn include_protos(input: TokenStream) -> syn::Result<TokenStream> {
    let span = proc_macro2::TokenStream::from(input.clone()).span();
    let IncludeProtosInput { files, includes } = parse(input)?;

    include_protos_impl(&files, &includes).map_err(|err| Error::new(span, err))
}

fn include_protos_impl(
    files: &Array<LitStr>,
    includes: &Option<Array<LitStr>>,
) -> Result<TokenStream> {
    let root = env_path("CARGO_MANIFEST_DIR")?;
    let out_dir = tempdir()?;

    let files: Vec<_> = files.elems.iter().map(|p| root.join(p.value())).collect();
    let mut includes: Vec<_> = match includes {
        Some(inc) => inc.elems.iter().map(|p| root.join(p.value())).collect(),
        None => files
            .iter()
            .map(|p| p.parent().unwrap().to_owned())
            .collect(),
    };

    includes.sort_unstable();
    includes.dedup();

    Config::new()
        .enable_type_names()
        .include_file("mod.rs")
        .out_dir(out_dir.path())
        .compile_protos(&files, &includes)?;

    let file = inline_includes(out_dir.path().join("mod.rs"))?;

    let tokens: proc_macro2::TokenStream = quote! {
        #file
    };

    Ok(tokens.into())
}