inline-mod 0.0.2

Inline modules at macro expansion time
Documentation
#![cfg_attr(feature = "docs",
   cfg_attr(all(), doc = include_str!("../README.md")),
)]
use std::{
	fs::read_to_string,
	path::{Path, PathBuf},
};

use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::{
	parse2, parse_file, spanned::Spanned, Error, File, Item, ItemMod, Lit, LitStr, Meta,
	MetaNameValue, Result,
};

#[proc_macro]
pub fn inline_mod(input: TokenStream) -> TokenStream {
	inline_mod_impl(input.into(), None)
		.unwrap_or_else(|err| err.to_compile_error())
		.into()
}

fn inline_mod_impl(input: TokenStream2, default_path: Option<LitStr>) -> Result<TokenStream2> {
	let input: ItemMod = parse2(input)?;
	if let Some((brace, _)) = input.content {
		return Err(Error::new(
			brace.span,
			"This macro only accepts non-inlined modules",
		));
	}
	let path = input
		.attrs
		.iter()
		.find_map(|attr| match attr.parse_meta() {
			Ok(Meta::NameValue(MetaNameValue {
				path,
				lit: Lit::Str(lit),
				..
			})) if path.is_ident("path") => Some(lit),
			_ => None,
		})
		.or(default_path)
		.ok_or_else(|| Error::new(Span::call_site(), "Path attribute is required"))?;
	let (path, path_span) = (path.value(), path.span());
	let mut path = PathBuf::from(path);
	if path.is_relative() {
		path = Path::new(
			&std::env::var_os("CARGO_MANIFEST_DIR").expect("Missing `CARGO_MANIFEST_DIR` variable"),
		)
		.join(path);
	}
	let path_str = path.to_str().unwrap();
	let root = path_str
		.strip_suffix("/mod.rs")
		.or_else(|| path_str.strip_suffix(".rs"))
		.unwrap_or(path_str);
	let root = Path::new(root);
	let ItemMod {
		ident, vis, attrs, ..
	} = input;

	let File {
		attrs: file_attrs,
		items,
		..
	} = {
		let content = read_to_string(&path).map_err(|err| {
			Error::new(
				path_span,
				format!(
					"Error reading module `{}` (path = `{:?}`): {}",
					&ident, path, err
				),
			)
		})?;
		parse_file(&content)?
	};

	let items = items.into_iter().map(|item| match item {
		Item::Mod(module) if module.content.is_none() => {
			let mut mod_path = root.join(format!("{}.rs", module.ident));
			if !mod_path.is_file() {
				mod_path = root.join(format!("{}/mod.rs", module.ident));
			}
			let mod_path = LitStr::new(mod_path.to_str().unwrap(), module.span());
			inline_mod_impl(module.into_token_stream(), Some(mod_path))
				.unwrap_or_else(|err| err.to_compile_error())
		}
		_ => item.into_token_stream(),
	});

	Ok(quote! {
		const _: &[::core::primitive::u8] = ::core::include_bytes!( #path_str ).as_slice();
		#( #attrs )*
		#vis mod #ident {
			#( #file_attrs )*
			#( #items )*
		}
	})
}