renox-macros 1.0.0

Procedural macros for the Renox web framework: derive Model, FromRow and DbEnum, migrations!(), embedded!() and #[renox::test].
Documentation
use std::path::{Path, PathBuf};

use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Error, Result};

fn walk(root: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) -> Result<()> {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return Ok(());
    };
    for entry in entries.flatten() {
        let path = entry.path();
        let name = entry.file_name().to_string_lossy().into_owned();
        if name.starts_with('.') {
            continue;
        }
        if path.is_dir() {
            walk(root, &path, out)?;
        } else {
            let relative = path
                .strip_prefix(root)
                .map_err(|e| Error::new(Span::call_site(), e.to_string()))?
                .components()
                .map(|c| c.as_os_str().to_string_lossy().into_owned())
                .collect::<Vec<_>>()
                .join("/");
            out.push((relative, path));
        }
    }
    Ok(())
}

fn files(root: &Path) -> Result<Vec<(String, String)>> {
    let mut out = Vec::new();
    walk(root, root, &mut out)?;
    out.sort();
    Ok(out
        .into_iter()
        .map(|(name, path)| (name, path.to_string_lossy().into_owned()))
        .collect())
}

pub fn expand(input: TokenStream) -> Result<TokenStream> {
    if !input.is_empty() {
        return Err(Error::new(
            Span::call_site(),
            "embedded!() takes no arguments",
        ));
    }
    let root = std::env::var("CARGO_MANIFEST_DIR")
        .map_err(|_| Error::new(Span::call_site(), "CARGO_MANIFEST_DIR is not set"))?;
    let root = PathBuf::from(root);

    let text = |dir: &str| -> Result<Vec<TokenStream>> {
        Ok(files(&root.join(dir))?
            .into_iter()
            .map(|(name, path)| quote! { (#name, ::core::include_str!(#path)) })
            .collect())
    };
    let views = text("resources/views")?;
    let lang = text("resources/lang")?;
    let public: Vec<TokenStream> = files(&root.join("public"))?
        .into_iter()
        .map(|(name, path)| quote! { (#name, ::core::include_bytes!(#path) as &[u8]) })
        .collect();

    Ok(quote! {
        ::renox::Embedded {
            views: &[#(#views),*],
            lang: &[#(#lang),*],
            public: &[#(#public),*],
        }
    })
}