subplotlib-derive 0.14.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 subplot::{ScenarioFilterElement, SCENARIO_FILTER_EVERYTHING, SCENARIO_FILTER_NOTHING};
use syn::{parse::Parse, Error, Ident, LitStr, Token};

pub(crate) struct CodegenArgs {
    subplot_lit: LitStr,
    subplot_path: PathBuf,
    include: Option<LitStr>,
    exclude: Option<LitStr>,
}

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,
            include: None,
            exclude: None,
        })
    }

    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> {
        // The first input to the args is always a string which is the subplot file
        // to use.
        let inputfile: LitStr = input.parse()?;
        let mut ret = Self::new(inputfile)?;
        // The rest of the input is key=value things separated by commas
        // which are further processed
        while !input.is_empty() {
            let _comma: Token![,] = input.parse()?;
            let key: Ident = input.parse()?;
            let _eq: Token![=] = input.parse()?;
            match key.to_string().as_str() {
                "include" => {
                    ret.include = Some(input.parse()?);
                }
                "exclude" => {
                    ret.exclude = Some(input.parse()?);
                }
                _ => {
                    return Err(syn::Error::new_spanned(key, "Unknown parameter"));
                }
            }
        }
        Ok(ret)
    }
}

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 mut filter = if input.include.is_some() {
        SCENARIO_FILTER_NOTHING.clone()
    } else {
        SCENARIO_FILTER_EVERYTHING.clone()
    };
    if let Some(include) = input.include.as_ref() {
        filter.push(ScenarioFilterElement::new(true, include.value().as_str()));
    }
    if let Some(exclude) = input.exclude.as_ref() {
        filter.push(ScenarioFilterElement::new(false, exclude.value().as_str()));
    }
    let code = subplot::codegen_to_memory(&input.subplot_path, Some("rust"), &filter)
        .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
    })
}