subplotlib-derive 0.12.0

macros for constructing subplotlib based test suites, typically generated by `subplot codegen`.
Documentation
use std::{
    path::{Path, PathBuf},
    sync::atomic::{AtomicUsize, Ordering},
};

use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{parse::Parse, Error, LitStr};

pub(crate) struct CodegenArgs {
    subplot_lit: LitStr,
    subplot_path: PathBuf,
}

impl CodegenArgs {
    fn new(subplot_lit: LitStr) -> syn::Result<Self> {
        let manifest = std::env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| {
            syn::Error::new_spanned(&subplot_lit, "Cannot find CARGO_MANIFEST_DIR")
        })?;
        let subplot_path = Path::new(&manifest).join(subplot_lit.value());
        if let Err(e) = std::fs::metadata(&subplot_path) {
            return Err(syn::Error::new_spanned(
                &subplot_lit,
                format!("{}: {}", subplot_path.display(), e),
            ));
        }
        let subplot_path = subplot_path.canonicalize().map_err(|e| {
            syn::Error::new_spanned(&subplot_lit, format!("Failure canonicalising: {e}"))
        })?;
        Ok(Self {
            subplot_lit,
            subplot_path,
        })
    }

    fn basedir(&self) -> syn::Result<&Path> {
        self.subplot_path.parent().ok_or_else(|| {
            syn::Error::new_spanned(
                &self.subplot_lit,
                "Cannot determine directory for subplot file",
            )
        })
    }
}

impl Parse for CodegenArgs {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let inputfile: LitStr = input.parse()?;
        Self::new(inputfile)
    }
}

struct ItemList {
    items: Vec<syn::Item>,
}

impl Parse for ItemList {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mut items = Vec::new();
        while !input.is_empty() {
            items.push(input.parse()?);
        }
        Ok(Self { items })
    }
}

impl ToTokens for ItemList {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        for item in &self.items {
            item.to_tokens(tokens);
        }
    }
}

static UNIQ_COUNTER: AtomicUsize = AtomicUsize::new(0);

pub(crate) fn do_codegen(input: CodegenArgs) -> Result<TokenStream, Error> {
    let code = subplot::codegen_to_memory(&input.subplot_path, Some("rust"))
        .map_err(|e| syn::Error::new_spanned(&input.subplot_lit, e))?;

    let items: ItemList = syn::parse_str(&code.code)?;

    let basedir = input.basedir()?;
    let docimpl = code.doc.meta().document_impl("rust").ok_or_else(|| {
        syn::Error::new_spanned(
            &input.subplot_lit,
            "Could not find rust impl in subplot document",
        )
    })?;
    let files = code
        .doc
        .meta()
        .markdown_filenames()
        .iter()
        .map(|md| basedir.join(md))
        .chain(
            code.doc
                .meta()
                .bindings_filenames()
                .iter()
                .map(|b| basedir.join(b)),
        )
        .chain(docimpl.functions_filenames().map(|f| basedir.join(f)))
        .chain(Some(input.subplot_path.clone()))
        .enumerate()
        .map(|(i, p)| {
            let ctr = UNIQ_COUNTER.fetch_add(1, Ordering::SeqCst);
            let uniq_name =
                syn::Ident::new(&format!("_SUBPLOT_INPUT_{i}_{ctr}"), Span::call_site());
            let pp = format!("{}", p.display());
            if std::fs::metadata(&p).is_ok() {
                quote! {
                    const #uniq_name: (&str, &[u8]) = (#pp, include_bytes!(#pp));
                }
            } else {
                quote! {
                    const #uniq_name: (&str, &[u8]) = (#pp, b"(builtin)");
                }
            }
        })
        .collect::<Vec<_>>();

    Ok(quote! {
        #(#files)*
        #items
    })
}