tea-codec-macros 0.3.0-dev.7

The TEA SDK
Documentation
mod impls;

use convert_case::{Case, Casing};
use itertools::Itertools;
use proc_macro2::{Ident, TokenStream};
use quote::quote;

use super::ast::{DefineScope, Definition, Name, StringAuto, StringExpr, TypedDefinition};

pub fn emit_all(source: &[DefineScope]) -> TokenStream {
	let source = source
		.iter()
		.inspect(|ast| {
			if ast.name == "Global" {
				panic!("The scope's name cannot be \"Global\".");
			}
		})
		.enumerate()
		.map(|(i, ast)| {
			if i == 0 {
				emit::<true>(ast)
			} else {
				emit::<false>(ast)
			}
		});
	quote! {#(#source)*}
}

pub fn emit<const WRAP_ERROR: bool>(source: &DefineScope) -> TokenStream {
	let DefineScope {
		name,
		parents,
		definitions,
	} = source;

	let name_string = name.to_string();
	let is_global = name_string == "Global";

	let parent = if let Some(parent) = parents
		.iter()
		.fold(None, |r, (r#type, is_pub)| {
			if *is_pub {
				if r.is_some() {
					panic!("There cannot be multiple pub parents.")
				} else {
					Some(r#type)
				}
			} else {
				r
			}
		})
		.or_else(|| match parents.len() {
			0 | 1 => parents.first().map(|(r#type, _)| r#type),
			_ => panic!("Must specify a unique pub parent."),
		}) {
		quote! { #parent }
	} else {
		quote! { ::tea_sdk::errorx::Global }
	};

	let parents = {
		let mut parents = parents
			.iter()
			.map(|(r#type, _)| quote! {#r#type})
			.collect::<Vec<_>>();

		if parents.is_empty() && !is_global {
			parents.push(quote! {::tea_sdk::errorx::Global});
		}
		parents
	};

	let r#enum = emit_enum(name, definitions);

	let definitions = definitions
		.iter()
		.filter_map(|x| match x {
			Definition::Abstract(_) => None,
			Definition::Typed(def) => Some(def),
		})
		.map(|def| emit_definition(name, def));

	let fullname = if is_global {
		quote! {#name_string}
	} else {
		emit_const_concat_dot(
			quote! {<#parent as ::tea_sdk::errorx::Scope>::NAME},
			quote! {<#name as ::tea_sdk::errorx::Scope>::NAME},
		)
	};

	let impls = impls::impls(name);

	let defs = [
		(quote! {name}, quote! {::std::borrow::Cow<str>}),
		(
			quote! {inner},
			quote! {::tea_sdk::errorx::SmallVec<[&::tea_sdk::errorx::Error; 1]>},
		),
		(quote! {type_id}, quote! {::std::any::TypeId}),
	]
	.map(|(name, r#type)| {
		quote! {
			default fn #name(v: &T) -> Option<#r#type> {
				#(if let Some(r) = <#parents as ::tea_sdk::errorx::Descriptor<T>>::#name(v) {
					return Some(r);
				})*
				None
			}
		}
	});

	let def_summary = quote! {
		default fn summary(v: &T) -> Option<::std::borrow::Cow<str>> {
			#(if let Some(r) = <#parents as ::tea_sdk::errorx::Descriptor<T>>::summary(v) {
				return Some(r);
			})*
			Some(::std::format!("{}", ::tea_sdk::ImplDefault(v)).into())
		}
	};

	let def_detail = quote! {
		default fn detail(v: &T) -> Option<::std::borrow::Cow<str>> {
			#(if let Some(r) = <#parents as ::tea_sdk::errorx::Descriptor<T>>::detail(v) {
				return Some(r);
			})*
			Some(::std::format!("{:?}", ::tea_sdk::ImplDefault(v)).into())
		}
	};

	let wrap_error = if WRAP_ERROR {
		quote! {
			pub type Error<S = #name> = ::tea_sdk::errorx::Error<S>;
			pub type Result<T, E = Error> = std::result::Result<T, E>;
		}
	} else {
		Default::default()
	};

	let impl_marks = quote! {
		#(
			impl<T> ::tea_sdk::errorx::DescriptableMark<T> for #name
			where
				#parents: ::tea_sdk::errorx::DescriptableMark<T>,
				T: ?Sized,
			{
			}
		)*
	};

	quote! {
		#r#enum

		#impls

		impl ::tea_sdk::errorx::Scope for #name {
			type Parent = #parent;
			type Descriptor<T> = Self;
			const NAME: &'static str = #name_string;
			const FULLNAME: &'static str = #fullname;
		}

		#impl_marks

		impl<T> ::tea_sdk::errorx::Descriptor<T> for #name {
			#(#defs)*
			#def_summary
			#def_detail
		}

		#(#definitions)*

		#wrap_error
	}
}

fn emit_const_concat_dot(op1: TokenStream, op2: TokenStream) -> TokenStream {
	quote! {{
		const N1: &[u8] = #op1.as_bytes();
		const N2: &[u8] = #op2.as_bytes();
		if let b"Global" = N1 {
			#op2
		} else {
			const LEN: usize = N1.len() + N2.len() + 1;
			const fn combine() -> [u8; LEN] {
				let mut result = [0u8; LEN];
				let mut i = 0;
				while i < N1.len() {
					result[i] = N1[i];
					i += 1;
				}
				result[i] = b'.';
				i = 0;
				while i < N2.len() {
					result[N1.len() + 1 + i] = N2[i];
					i += 1;
				}
				result
			}
			unsafe { ::std::str::from_utf8_unchecked(&combine()) }
		}
	}}
}

fn emit_enum<'a>(
	scope: &'a Ident,
	def: impl IntoIterator<Item = &'a Definition> + 'a,
) -> TokenStream {
	let names: Vec<_> = def
		.into_iter()
		.filter_map(|def| match def {
			Definition::Abstract(def) => Some(&def.0),
			Definition::Typed(def) => match &def.name {
				Name::Define(id) => Some(id),
				Name::Use(_) => None,
			},
		})
		.unique()
		.collect();

	let name_const = names.iter().map(|name| {
		let name_string = name.to_string();
		let value = emit_const_concat_dot(
			quote! { <#scope as ::tea_sdk::errorx::Scope>::FULLNAME },
			quote! { #name_string },
		);
		let const_name = convert_const_name_case(name);

		quote! {
			const #const_name: &'static str = #value;
		}
	});

	let name_match = names.iter().map(|name| {
		let const_name = convert_const_name_case(name);
		quote! {
			#scope::#name => #const_name,
		}
	});

	quote! {
		#[derive(PartialEq, Eq, Hash, Clone, Copy)]
		pub enum #scope {
			#(#names,)*
		}

		impl #scope {
			pub const fn name_const(&self) -> &'static str {
				#(#name_const)*
				match self {
					#(#name_match)*
					_ => panic!("Bad scope value"),
				}
			}
		}
	}
}

fn convert_const_name_case(id: &Ident) -> Ident {
	Ident::new(id.to_string().to_case(Case::UpperSnake).as_str(), id.span())
}

fn emit_definition(scope: &Ident, def: &TypedDefinition) -> TokenStream {
	let TypedDefinition {
		r#type,
		value,
		name,
		summary,
		detail,
		inner,
	} = def;

	let value = value
		.as_ref()
		.map(|v| quote! {#v})
		.unwrap_or_else(|| quote! {__value__});

	let name = match name {
		Name::Define(name) => {
			quote! { (&*#scope::#name).into() }
		}
		Name::Use(s) => quote! { (#s).into() },
	};

	let summary = summary
		.as_ref()
		.map(|summary| {
			let summary = emit_string_expr(&value, summary);
			quote! {
				fn summary<'a>(#value: &'a #r#type) -> Option<std::borrow::Cow<'a, str>> {
					#summary
				}
			}
		})
		.unwrap_or_default();

	let detail = detail
		.as_ref()
		.map(|detail| {
			let detail = emit_string_expr(&value, detail);
			quote! {
				fn detail<'a>(#value: &'a #r#type) -> Option<std::borrow::Cow<'a, str>> {
					#detail
				}
			}
		})
		.unwrap_or_default();

	let inner = inner
		.as_ref()
		.map(|inner| {
			let inner = match inner {
				super::ast::Inner::Values(values) => {
					let len = values.len();
					let values = values
						.iter()
						.map(|e| quote! { inner.push((#e).into()); });
					quote! {{
						let mut inner = ::tea_sdk::errorx::SmallVec::<[&::tea_sdk::errorx::Error; 1]>::new();
						inner.reserve_exact(#len);
						#(#values)*
						inner
					}}
				}
				super::ast::Inner::Raw(value) => quote! { (#value).into() },
			};

			quote! {
				fn inner<'a>(#value: &'a #r#type) -> Option<::tea_sdk::errorx::SmallVec<[&'a ::tea_sdk::errorx::Error; 1]>> {
					Some(#inner)
				}
			}
		})
		.unwrap_or_default();

	quote! {
		impl ::tea_sdk::errorx::DescriptableMark<#r#type> for #scope {}

		#[allow(unused_variables)]
		#[allow(unused_parens)]
		impl ::tea_sdk::errorx::Descriptor<#r#type> for #scope {
			fn name<'a>(_: &'a #r#type) -> Option<::std::borrow::Cow<'a, str>> {
				Some(#name)
			}

			#summary
			#detail
			#inner

			fn type_id<'a>(_: &'a #r#type) -> Option<::std::any::TypeId> {
				Some(::std::any::TypeId::of::<#r#type>())
			}
		}
	}
}

fn emit_string_expr(value: &TokenStream, expr: &StringExpr) -> TokenStream {
	match expr {
		StringExpr::Expr(expr) => quote! { Some((#expr).into()) },
		StringExpr::Use(auto) => match auto {
			StringAuto::Display => quote! { Some(format!("{}", #value).into()) },
			StringAuto::Debug => quote! { Some(format!("{:?}", #value).into()) },
			StringAuto::Serde => quote! { ::serde_json::to_string(#value).ok().map(Into::into) },
		},
	}
}