Initial commit
This commit is contained in:
commit
75a589f235
43 changed files with 4840 additions and 0 deletions
603
macros/src/data.rs
Normal file
603
macros/src/data.rs
Normal file
|
@ -0,0 +1,603 @@
|
|||
use proc_macro::TokenStream;
|
||||
use syn::{Token, parse, punctuated::Punctuated, spanned::Spanned};
|
||||
|
||||
use proc_macro_error::abort;
|
||||
|
||||
use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase};
|
||||
use quote::quote;
|
||||
|
||||
use crate::utils::{self, join, maybe, mk_derive};
|
||||
|
||||
fn wildcard_arm(var: &syn::Variant) -> proc_macro2::TokenStream {
|
||||
use syn::Fields as F;
|
||||
let ident = &var.ident;
|
||||
match var.fields {
|
||||
F::Unit => quote! { #ident },
|
||||
F::Named(..) => quote! { #ident { .. } },
|
||||
F::Unnamed(..) => quote! { #ident(..) },
|
||||
}
|
||||
}
|
||||
|
||||
fn let_destructure_pat(fields: &syn::Fields) -> proc_macro2::TokenStream {
|
||||
let mut f_num = 0_usize;
|
||||
utils::construct_pat(fields, |field| {
|
||||
let wildcard_pat = if let Some(ident) = &field.ident {
|
||||
ident.clone()
|
||||
} else {
|
||||
let prev = quote::format_ident!("_{f_num}");
|
||||
f_num += 1;
|
||||
prev
|
||||
};
|
||||
|
||||
utils::ConstructPat::Wildcard(wildcard_pat)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
let orig = body.clone();
|
||||
let item = syn::parse_macro_input!(body as Item);
|
||||
let args = syn::parse_macro_input!(args as Args);
|
||||
|
||||
let snake_ident = item.ident.to_string().to_snake_case();
|
||||
let mut snake_ident = quote::format_ident!("{snake_ident}");
|
||||
snake_ident.set_span(item.ident.span());
|
||||
let snake_ident = snake_ident;
|
||||
let eva = args.eva.unwrap_or({
|
||||
let eva = quote::format_ident!("eva");
|
||||
syn::parse_quote! {
|
||||
::#eva
|
||||
}
|
||||
});
|
||||
|
||||
let orig: proc_macro2::TokenStream = orig.into();
|
||||
let derives = &args.derives;
|
||||
let deps = join(&eva, "_priv");
|
||||
let serde = join(&deps, "serde");
|
||||
|
||||
let std: syn::Path = syn::parse_quote!(::std);
|
||||
let fmt = join(&std, "fmt");
|
||||
let cmp = join(&std, "cmp");
|
||||
let clone = join(&std, "clone");
|
||||
let marker = join(&std, "marker");
|
||||
|
||||
let ser = maybe(derives.serialize, || mk_derive(&join(&serde, "Serialize")));
|
||||
let de = maybe(derives.deserialize, || {
|
||||
mk_derive(&join(&serde, "Deserialize"))
|
||||
});
|
||||
|
||||
let schemars = join(&deps, "schemars");
|
||||
let json_schema = maybe(derives.json_schema, || {
|
||||
let derive = mk_derive(&join(&schemars, "JsonSchema"));
|
||||
let eva = quote! { #schemars }.to_string();
|
||||
quote! {
|
||||
#derive
|
||||
#[schemars(crate = #eva)]
|
||||
}
|
||||
});
|
||||
|
||||
let serde_attr = maybe(derives.serialize || derives.deserialize, || {
|
||||
let eva = quote! { #serde }.to_string();
|
||||
quote! { #[serde(crate = #eva)] }
|
||||
});
|
||||
|
||||
let partial_eq = maybe(derives.partial_eq, || mk_derive(&join(&cmp, "PartialEq")));
|
||||
let eq = maybe(derives.eq, || mk_derive(&join(&cmp, "Eq")));
|
||||
let partial_ord = maybe(derives.partial_ord, || mk_derive(&join(&cmp, "PartialOrd")));
|
||||
let ord = maybe(derives.ord, || mk_derive(&join(&cmp, "Ord")));
|
||||
|
||||
let clone = maybe(derives.clone, || mk_derive(&join(&clone, "Clone")));
|
||||
let copy = maybe(derives.copy, || mk_derive(&join(&marker, "Copy")));
|
||||
|
||||
let as_static_str = maybe(
|
||||
args.as_static_str && matches!(item.kind, ItemKind::Enum { .. }),
|
||||
|| {
|
||||
let ItemKind::Enum { variants } = &item.kind else {
|
||||
unreachable!()
|
||||
};
|
||||
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||
let case = join(&join(&eva, "generic"), "Case");
|
||||
let arms = variants.iter().fold(quote! {}, |acc, var| {
|
||||
let pat = wildcard_arm(&var);
|
||||
let pat = quote! { Self::#pat };
|
||||
|
||||
let var_name = var.ident.to_string();
|
||||
let snake = var_name.to_snake_case();
|
||||
let kebab = var_name.to_kebab_case();
|
||||
let pascal = var_name.to_pascal_case();
|
||||
let camel = var_name.to_lower_camel_case();
|
||||
|
||||
let case_match = quote! {
|
||||
if let Some(cvt) = convert_case {
|
||||
match cvt {
|
||||
#case::Snake => #snake,
|
||||
#case::Kebab => #kebab,
|
||||
#case::Pascal => #pascal,
|
||||
#case::Camel => #camel,
|
||||
}
|
||||
} else {
|
||||
#var_name
|
||||
}
|
||||
};
|
||||
|
||||
quote! { #acc #pat => #case_match, }
|
||||
});
|
||||
let ident = &item.ident;
|
||||
|
||||
quote! {
|
||||
impl #ig #ident #tyg #where_clause {
|
||||
/// Convert enum discriminant into the string.
|
||||
pub const fn as_static_str(&self, convert_case: Option<#case>) -> &'static str {
|
||||
match *self {
|
||||
#arms
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let display_impl = if let Some(display) = args.display {
|
||||
match &display {
|
||||
DisplayImpl::Fmt(DisplayFmt {
|
||||
fmt: fmt_string,
|
||||
args,
|
||||
}) => {
|
||||
let let_assign = if let ItemKind::Struct { fields } = &item.kind {
|
||||
let pat = let_destructure_pat(fields);
|
||||
quote! { let Self #pat = self; }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||
let ident = &item.ident;
|
||||
|
||||
quote! {
|
||||
const _: () = {
|
||||
use #std::io::Write as _;
|
||||
|
||||
#[allow(non_shorthand_field_pattern)]
|
||||
impl #ig #fmt::Display for #ident #tyg #where_clause {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut #fmt::Formatter<'_>) -> #fmt::Result {
|
||||
#let_assign
|
||||
#std::write!(f, #fmt_string, #(#args),*)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
spec @ (DisplayImpl::Doc | DisplayImpl::Name) => {
|
||||
let ItemKind::Enum { variants } = &item.kind else {
|
||||
abort!(
|
||||
item.ident,
|
||||
"to use this display setting the type must be enum"
|
||||
);
|
||||
};
|
||||
|
||||
let display = join(&fmt, "Display");
|
||||
let match_arms = variants.iter().fold(quote! {}, |acc, var| {
|
||||
let display_str = match spec {
|
||||
DisplayImpl::Name => var.ident.to_string().to_snake_case(),
|
||||
DisplayImpl::Doc => combine_docs(&var.attrs).unwrap_or_else(|| {
|
||||
abort!(var, "this variant has no docs");
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let pat = wildcard_arm(&var);
|
||||
let pat = quote! { Self::#pat };
|
||||
quote! { #acc #pat => #display_str, }
|
||||
});
|
||||
|
||||
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||
let ty_name = &item.ident;
|
||||
|
||||
let result = join(&fmt, "Result");
|
||||
let formatter = join(&fmt, "Formatter");
|
||||
|
||||
let doc_match = quote! {
|
||||
match *self {
|
||||
#match_arms
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
const _: () = {
|
||||
#[automatically_derived]
|
||||
#[allow(unreachable_code)]
|
||||
impl #ig #display for #ty_name #tyg #where_clause {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut #formatter<'_>) -> #result {
|
||||
let doc_str: &'static str = #doc_match;
|
||||
f.write_str(doc_str)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let ItemKind::Enum { variants } = &item.kind {
|
||||
let mut arms = quote! {};
|
||||
for var in variants.iter() {
|
||||
let Some(args) = var
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|attr| attr.path().is_ident("display"))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let syn::Meta::List(args) = &args.meta else {
|
||||
abort!(args, "must be meta list");
|
||||
};
|
||||
let args: DisplayFmt = match syn::parse(args.tokens.clone().into()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return TokenStream::from(e.to_compile_error()),
|
||||
};
|
||||
|
||||
let fmt_string = args.fmt;
|
||||
let fmt_args = args.args;
|
||||
let destructure = let_destructure_pat(&var.fields);
|
||||
let ident = &var.ident;
|
||||
arms = quote! {
|
||||
#arms
|
||||
Self::#ident #destructure => {
|
||||
#std::write!(f, #fmt_string, #(#fmt_args),*)
|
||||
},
|
||||
};
|
||||
}
|
||||
let arms = arms;
|
||||
|
||||
if arms.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||
let ident = &item.ident;
|
||||
quote! {
|
||||
const _: () = {
|
||||
use #std::io::Write as _;
|
||||
|
||||
#[automatically_derived]
|
||||
#[allow(non_shorthand_field_patterns)]
|
||||
impl #ig #fmt::Display for #ident #tyg #where_clause {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut #fmt::Formatter<'_>) -> #fmt::Result {
|
||||
match self {
|
||||
#arms
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
};
|
||||
let from_impls = {
|
||||
let mut impls = Vec::<proc_macro2::TokenStream>::new();
|
||||
match &item.kind {
|
||||
ItemKind::Enum { variants } => {
|
||||
for var in variants {
|
||||
let mut generics = item.generics.clone();
|
||||
let mut from_assigned = None;
|
||||
let create = utils::construct(&var.fields, |field| {
|
||||
let ty = &field.ty;
|
||||
if let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("from")) {
|
||||
if from_assigned.is_some() {
|
||||
abort!(attr, "must be only one #[from]");
|
||||
}
|
||||
if !matches!(attr.meta, syn::Meta::Path(..)) {
|
||||
abort!(attr, "must be simple path");
|
||||
}
|
||||
|
||||
from_assigned = Some(ty.clone());
|
||||
quote! { from_value }
|
||||
} else {
|
||||
utils::add_bound(
|
||||
&mut generics.where_clause,
|
||||
syn::parse_quote! {
|
||||
#ty: ::core::default::Default
|
||||
},
|
||||
);
|
||||
quote! { <#ty as ::core::default::Default>::default() }
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(ty) = from_assigned {
|
||||
let var_ident = &var.ident;
|
||||
let (ig, tyg, where_clause) = generics.split_for_impl();
|
||||
let ident = &item.ident;
|
||||
impls.push(quote! {
|
||||
impl #ig ::core::convert::From<#ty> for #ident #tyg #where_clause {
|
||||
fn from(from_value: #ty) -> Self {
|
||||
Self::#var_ident #create
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Struct { fields } => {}
|
||||
}
|
||||
|
||||
quote! { #(#impls)* }
|
||||
};
|
||||
let error = maybe(args.error, || {
|
||||
let (ig, tyg, where_clause) = item.generics.split_for_impl();
|
||||
let ident = &item.ident;
|
||||
|
||||
// TODO: make not that stupid.
|
||||
quote! {
|
||||
impl #ig #std::error::Error for #ident #tyg #where_clause {}
|
||||
}
|
||||
});
|
||||
let debug = maybe(derives.debug, || mk_derive(&join(&fmt, "Debug")));
|
||||
let rename = maybe(derives.serialize || derives.deserialize, || {
|
||||
quote! {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#debug
|
||||
#clone
|
||||
#copy
|
||||
#partial_eq
|
||||
#eq
|
||||
#partial_ord
|
||||
#ord
|
||||
#ser
|
||||
#de
|
||||
#serde_attr
|
||||
#json_schema
|
||||
#rename
|
||||
#[derive(#eva::_priv::RastGawno)]
|
||||
#orig
|
||||
|
||||
#error
|
||||
|
||||
#from_impls
|
||||
|
||||
#as_static_str
|
||||
|
||||
#display_impl
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn combine_docs(attrs: &[syn::Attribute]) -> Option<String> {
|
||||
let docs = utils::collect_docs(attrs)?;
|
||||
let res = docs.trim().trim_end_matches('.');
|
||||
let mut it = res.chars();
|
||||
// Make first letter lowercase.
|
||||
Some(
|
||||
it.next()
|
||||
.into_iter()
|
||||
.flat_map(|c| c.to_lowercase())
|
||||
.chain(it)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
struct Derives {
|
||||
serialize: bool,
|
||||
deserialize: bool,
|
||||
json_schema: bool,
|
||||
clone: bool,
|
||||
copy: bool,
|
||||
partial_eq: bool,
|
||||
eq: bool,
|
||||
debug: bool,
|
||||
partial_ord: bool,
|
||||
ord: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DisplayImpl {
|
||||
Fmt(DisplayFmt),
|
||||
|
||||
/// Implement display on enums by parsing doc-comments. Strips
|
||||
/// trailing dot if present.
|
||||
Doc,
|
||||
/// Implement display on enums by variant names.
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DisplayFmt {
|
||||
pub fmt: syn::LitStr,
|
||||
pub args: Vec<syn::Expr>,
|
||||
}
|
||||
|
||||
impl parse::Parse for DisplayFmt {
|
||||
fn parse(ps: parse::ParseStream) -> syn::Result<Self> {
|
||||
let fmt: syn::LitStr = ps.parse()?;
|
||||
let mut args = Vec::new();
|
||||
|
||||
loop {
|
||||
if ps.peek(Token![,]) {
|
||||
let _: Token![,] = ps.parse()?;
|
||||
if ps.is_empty() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
args.push(ps.parse()?);
|
||||
}
|
||||
|
||||
Ok(Self { fmt, args })
|
||||
}
|
||||
}
|
||||
|
||||
struct Args {
|
||||
eva: Option<syn::Path>,
|
||||
meta_mod: Option<syn::Ident>,
|
||||
error: bool,
|
||||
derives: Derives,
|
||||
as_static_str: bool,
|
||||
display: Option<DisplayImpl>,
|
||||
}
|
||||
|
||||
impl parse::Parse for Args {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let mut meta_mod: Option<syn::Ident> = None;
|
||||
let mut error = false;
|
||||
let mut eva: Option<syn::Path> = None;
|
||||
|
||||
let mut as_static_str = true;
|
||||
|
||||
let mut display: Option<DisplayImpl> = None;
|
||||
let mut derives = Derives {
|
||||
serialize: true,
|
||||
deserialize: true,
|
||||
json_schema: true,
|
||||
clone: true,
|
||||
debug: true,
|
||||
partial_eq: true,
|
||||
eq: false,
|
||||
copy: false,
|
||||
partial_ord: false,
|
||||
ord: false,
|
||||
};
|
||||
|
||||
utils::comma_separated(input, |ps| {
|
||||
if ps.peek(Token![crate]) {
|
||||
let _: Token![crate] = ps.parse().unwrap();
|
||||
let _: Token![=] = ps.parse()?;
|
||||
eva = Some(ps.parse()?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let param = utils::ident(ps)?;
|
||||
|
||||
if param == "error" {
|
||||
error = true;
|
||||
} else if param == "display" {
|
||||
let content;
|
||||
syn::parenthesized!(content in ps);
|
||||
if let Ok(fmt) = content.parse::<DisplayFmt>() {
|
||||
display = Some(DisplayImpl::Fmt(fmt));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let param = utils::ident(&content)?;
|
||||
let imp = if param == "doc" {
|
||||
DisplayImpl::Doc
|
||||
} else if param == "name" {
|
||||
DisplayImpl::Name
|
||||
} else {
|
||||
return Err(syn::Error::new(param.span(), "doc expected"));
|
||||
};
|
||||
|
||||
display = Some(imp);
|
||||
} else if param == "meta_mod" {
|
||||
let _: Token![=] = ps.parse()?;
|
||||
meta_mod = Some(ps.parse()?);
|
||||
} else if param == "not" {
|
||||
let content;
|
||||
syn::parenthesized!(content in ps);
|
||||
utils::comma_separated(&content, |ps| {
|
||||
let ident = utils::ident(ps)?;
|
||||
|
||||
if ident == "serde" {
|
||||
derives.serialize = false;
|
||||
derives.deserialize = false;
|
||||
} else if ident == "Serialize" {
|
||||
derives.serialize = false;
|
||||
} else if ident == "Deserialize" {
|
||||
derives.deserialize = false;
|
||||
} else if ident == "Debug" {
|
||||
derives.debug = false;
|
||||
} else if ident == "Clone" {
|
||||
derives.clone = false;
|
||||
} else if ident == "schemars" || ident == "JsonSchema" {
|
||||
derives.json_schema = false;
|
||||
} else if ident == "as_static_str" {
|
||||
as_static_str = false;
|
||||
} else {
|
||||
return Err(syn::Error::new(ident.span(), "got unknown `not` attribute"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
} else if param == "copy" {
|
||||
derives.clone = true;
|
||||
derives.copy = true;
|
||||
} else if param == "eq" {
|
||||
derives.eq = true;
|
||||
derives.partial_eq = true;
|
||||
} else if param == "ord" {
|
||||
derives.eq = true;
|
||||
derives.partial_eq = true;
|
||||
derives.ord = true;
|
||||
derives.partial_ord = true;
|
||||
} else {
|
||||
return Err(syn::Error::new(param.span(), "got unknown attribute"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
meta_mod,
|
||||
error,
|
||||
eva,
|
||||
derives,
|
||||
display,
|
||||
as_static_str,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ItemKind {
|
||||
Enum {
|
||||
variants: Punctuated<syn::Variant, Token![,]>,
|
||||
},
|
||||
Struct {
|
||||
fields: syn::Fields,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Item {
|
||||
attrs: Vec<syn::Attribute>,
|
||||
vis: syn::Visibility,
|
||||
ident: syn::Ident,
|
||||
generics: syn::Generics,
|
||||
|
||||
kind: ItemKind,
|
||||
}
|
||||
|
||||
impl parse::Parse for Item {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let item: syn::Item = input.parse()?;
|
||||
let span = item.span();
|
||||
|
||||
match item {
|
||||
syn::Item::Enum(e) => Ok(Self {
|
||||
attrs: e.attrs,
|
||||
vis: e.vis,
|
||||
ident: e.ident,
|
||||
generics: e.generics,
|
||||
kind: ItemKind::Enum {
|
||||
variants: e.variants,
|
||||
},
|
||||
}),
|
||||
syn::Item::Struct(s) => Ok(Self {
|
||||
attrs: s.attrs,
|
||||
vis: s.vis,
|
||||
ident: s.ident,
|
||||
generics: s.generics,
|
||||
kind: ItemKind::Struct { fields: s.fields },
|
||||
}),
|
||||
_ => Err(syn::Error::new(
|
||||
span,
|
||||
"only structs and enums are supported",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
68
macros/src/endpoint.rs
Normal file
68
macros/src/endpoint.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use proc_macro::TokenStream;
|
||||
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
|
||||
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
_ = args;
|
||||
|
||||
let f = syn::parse_macro_input!(body as syn::ItemFn);
|
||||
let syn::ItemFn {
|
||||
attrs,
|
||||
vis,
|
||||
sig,
|
||||
block,
|
||||
} = f;
|
||||
|
||||
if sig.asyncness.is_none() {
|
||||
abort!(sig, "must be async");
|
||||
}
|
||||
|
||||
let generics = &sig.generics;
|
||||
let ident = &sig.ident;
|
||||
|
||||
if sig.inputs.len() != 2 {
|
||||
abort!(sig.inputs, "must contain only state and args");
|
||||
}
|
||||
|
||||
let fn_state = sig.inputs.get(0).unwrap();
|
||||
let fn_args = sig.inputs.get(1).unwrap();
|
||||
|
||||
let syn::FnArg::Typed(fn_state) = fn_state else {
|
||||
abort!(fn_state, "must not be self");
|
||||
};
|
||||
let syn::FnArg::Typed(fn_args) = fn_args else {
|
||||
abort!(fn_args, "must not be self");
|
||||
};
|
||||
|
||||
let fn_args_pat = &*fn_args.pat;
|
||||
let fn_state_pat = &*fn_state.pat;
|
||||
|
||||
let fn_args_ty = &*fn_args.ty;
|
||||
let fn_state_ty = &*fn_state.ty;
|
||||
|
||||
let output = match sig.output {
|
||||
syn::ReturnType::Default => syn::parse_quote!(()),
|
||||
syn::ReturnType::Type(_, t) => t.as_ref().clone(),
|
||||
};
|
||||
|
||||
let (ig, tyg, where_clause) = generics.split_for_impl();
|
||||
|
||||
quote! {
|
||||
#vis struct #ident;
|
||||
|
||||
impl #ig ::eva::handling::Endpoint<#fn_args_ty, #fn_state_ty> for #ident #where_clause {
|
||||
type Output = #output;
|
||||
|
||||
#(#attrs)*
|
||||
async fn call(
|
||||
&self,
|
||||
#fn_state_pat: #fn_state_ty,
|
||||
#fn_args_pat: #fn_args_ty,
|
||||
) -> Self::Output {
|
||||
#block
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
544
macros/src/int.rs
Normal file
544
macros/src/int.rs
Normal file
|
@ -0,0 +1,544 @@
|
|||
use proc_macro::{Span, TokenStream};
|
||||
|
||||
use range_set_blaze::{RangeSetBlaze, UIntPlusOne};
|
||||
|
||||
use crate::utils::{self, join, maybe};
|
||||
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{Token, parse, spanned::Spanned};
|
||||
|
||||
struct IsInclusive(bool);
|
||||
|
||||
impl parse::Parse for IsInclusive {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
if input.peek(Token![=]) {
|
||||
input.parse::<Token![=]>().expect("checked above");
|
||||
Ok(Self(true))
|
||||
} else {
|
||||
Ok(Self(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportedLit(i128);
|
||||
|
||||
impl parse::Parse for SupportedLit {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let lit: syn::Lit = input.parse()?;
|
||||
match lit {
|
||||
syn::Lit::Int(int) => {
|
||||
let res = int.base10_parse::<i128>()?;
|
||||
Ok(Self(res))
|
||||
}
|
||||
syn::Lit::Char(chr) => Ok(Self(chr.value() as i128)),
|
||||
syn::Lit::Byte(b) => Ok(Self(b.value() as i128)),
|
||||
_ => Err(syn::Error::new(
|
||||
Span::call_site().into(),
|
||||
"this literal type is not supported",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SingleRange {
|
||||
start: i128,
|
||||
end: i128,
|
||||
}
|
||||
|
||||
impl parse::Parse for SingleRange {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let SupportedLit(start) = input.parse()?;
|
||||
// Allow specifying single values.
|
||||
if !input.peek(Token![..]) {
|
||||
return Ok(Self { start, end: start });
|
||||
}
|
||||
|
||||
let _: Token![..] = input.parse()?;
|
||||
let IsInclusive(inclusive) = input.parse()?;
|
||||
let SupportedLit(mut end) = input.parse()?;
|
||||
|
||||
if !inclusive {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
if start > end {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site().into(),
|
||||
"start must be less than end",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self { start, end })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Repr {
|
||||
I8,
|
||||
U8,
|
||||
I16,
|
||||
U16,
|
||||
I32,
|
||||
U32,
|
||||
I64,
|
||||
U64,
|
||||
I128,
|
||||
U128,
|
||||
}
|
||||
|
||||
impl ToTokens for Repr {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
tokens.extend(match self {
|
||||
Self::U8 => quote! { u8 },
|
||||
Self::I8 => quote! { i8 },
|
||||
Self::U16 => quote! { u16 },
|
||||
Self::I16 => quote! { i16 },
|
||||
Self::U32 => quote! { u32 },
|
||||
Self::I32 => quote! { i32 },
|
||||
Self::U64 => quote! { u64 },
|
||||
Self::I64 => quote! { i64 },
|
||||
Self::U128 => quote! { u128 },
|
||||
Self::I128 => quote! { i128 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl parse::Parse for Repr {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let tp: syn::TypePath = input.parse()?;
|
||||
let p = tp.path;
|
||||
let span = p.span();
|
||||
|
||||
Ok(if p.is_ident("u8") {
|
||||
Self::U8
|
||||
} else if p.is_ident("u16") {
|
||||
Self::U16
|
||||
} else if p.is_ident("u32") {
|
||||
Self::U32
|
||||
} else if p.is_ident("u64") {
|
||||
Self::U64
|
||||
} else if p.is_ident("u128") {
|
||||
Self::U128
|
||||
} else if p.is_ident("i8") {
|
||||
Self::I8
|
||||
} else if p.is_ident("i16") {
|
||||
Self::I16
|
||||
} else if p.is_ident("i32") {
|
||||
Self::I32
|
||||
} else if p.is_ident("i64") {
|
||||
Self::I64
|
||||
} else if p.is_ident("i128") {
|
||||
Self::I128
|
||||
} else {
|
||||
return Err(syn::Error::new(span, "only u*/i* types are supported"));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Args {
|
||||
repr: Repr,
|
||||
range: RangeSetBlaze<i128>,
|
||||
crate_: syn::Path,
|
||||
|
||||
ser: bool,
|
||||
de: bool,
|
||||
schema: bool,
|
||||
}
|
||||
|
||||
impl parse::Parse for Args {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let repr: Repr = input.parse()?;
|
||||
let _: Token![,] = input.parse()?;
|
||||
|
||||
let mut ser = true;
|
||||
let mut de = true;
|
||||
let mut schema = true;
|
||||
|
||||
let range = {
|
||||
let mut set = RangeSetBlaze::new();
|
||||
|
||||
loop {
|
||||
let range: SingleRange = input.parse()?;
|
||||
set.ranges_insert(range.start..=range.end);
|
||||
if input.peek(Token![ | ]) {
|
||||
let _: Token![ | ] = input.parse()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
};
|
||||
|
||||
let mut crate_: syn::Path = syn::parse_quote!(eva);
|
||||
if input.peek(Token![,]) {
|
||||
let _: Token![,] = input.parse()?;
|
||||
|
||||
utils::comma_separated(input, |ps| {
|
||||
if ps.peek(Token![crate]) {
|
||||
let _: Token![crate] = ps.parse().unwrap();
|
||||
utils::assign(ps)?;
|
||||
crate_ = ps.parse()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ident: syn::Ident = ps.parse()?;
|
||||
if ident == "not" {
|
||||
let content;
|
||||
syn::parenthesized!(content in ps);
|
||||
|
||||
utils::comma_separated(&content, |ps| {
|
||||
let ident: syn::Ident = ps.parse()?;
|
||||
|
||||
if ident == "serde" {
|
||||
ser = false;
|
||||
de = false;
|
||||
} else if ident == "schemars" || ident == "JsonSchema" {
|
||||
schema = false;
|
||||
} else if ident == "Serialize" {
|
||||
ser = false;
|
||||
} else if ident == "Deserialize" {
|
||||
de = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
repr,
|
||||
range,
|
||||
crate_,
|
||||
|
||||
ser,
|
||||
de,
|
||||
schema,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn int_lit(v: i128) -> syn::LitInt {
|
||||
let v = v.to_string();
|
||||
syn::LitInt::new(&v, Span::call_site().into())
|
||||
}
|
||||
|
||||
fn variant_name(of: i128) -> syn::Ident {
|
||||
quote::format_ident!(
|
||||
"{}{}",
|
||||
if of >= 0 { "POS" } else { "NEG" },
|
||||
of.abs().to_string()
|
||||
)
|
||||
}
|
||||
|
||||
fn rs_len(rs: &RangeSetBlaze<i128>) -> usize {
|
||||
let UIntPlusOne::UInt(m) = rs.len() else {
|
||||
panic!("Shit happens")
|
||||
};
|
||||
|
||||
m.try_into().unwrap()
|
||||
}
|
||||
|
||||
fn mk_enum(item: &syn::ItemEnum, args: &Args) -> TokenStream {
|
||||
let name = &item.ident;
|
||||
let attrs = &item.attrs;
|
||||
let vis = item.vis.clone();
|
||||
let docs = utils::collect_docs(attrs);
|
||||
|
||||
let cap: usize = rs_len(&args.range);
|
||||
let mut variants: Vec<syn::Variant> = Vec::with_capacity(cap);
|
||||
let repr = args.repr;
|
||||
|
||||
for value in args.range.iter() {
|
||||
let ident = variant_name(value);
|
||||
let value = value.to_string();
|
||||
let lit = syn::LitInt::new(&value, Span::call_site().into());
|
||||
let var: syn::Variant = syn::parse_quote! {
|
||||
#ident = #lit
|
||||
};
|
||||
|
||||
variants.push(var);
|
||||
}
|
||||
|
||||
let range_checks = args.range.ranges().map(|range| {
|
||||
let start = int_lit(*range.start());
|
||||
let end = int_lit(*range.end());
|
||||
quote! {
|
||||
if (repr >= #start) && (repr <= #end) {
|
||||
return Some(unsafe { Self::new_unchecked(repr) })
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let eva = &args.crate_;
|
||||
let priv_ = join(&args.crate_, "_priv");
|
||||
let serde = join(&priv_, "serde");
|
||||
let schemars = join(&priv_, "schemars");
|
||||
|
||||
let json_schema = maybe(args.schema, || {
|
||||
let schema_id = quote! {
|
||||
::std::borrow::Cow::Borrowed(concat!(module_path!(), "::", stringify!(#name)))
|
||||
};
|
||||
let schema_name = name.to_string();
|
||||
let items: Vec<_> = args
|
||||
.range
|
||||
.ranges()
|
||||
.map(|range| {
|
||||
let minimum = int_lit(*range.start());
|
||||
let maximum = int_lit(*range.end());
|
||||
quote! {
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": #minimum,
|
||||
"maximum": #maximum
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let desc = if let Some(desc) = docs.as_deref() {
|
||||
quote! {
|
||||
"description": #desc,
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let schema = quote! {
|
||||
#schemars::json_schema!({
|
||||
#desc
|
||||
"anyOf": [#(#items),*]
|
||||
})
|
||||
};
|
||||
|
||||
quote! {
|
||||
impl #schemars::JsonSchema for #name {
|
||||
fn schema_id() -> ::std::borrow::Cow<'static, str> {
|
||||
#schema_id
|
||||
}
|
||||
|
||||
fn schema_name() -> ::std::borrow::Cow<'static, str> {
|
||||
::std::borrow::Cow::Borrowed(#schema_name)
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut #schemars::SchemaGenerator) -> #schemars::Schema {
|
||||
#schema
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let serialize = maybe(args.ser, || {
|
||||
quote! {
|
||||
const _: () = {
|
||||
use ::core::result::Result;
|
||||
|
||||
impl #serde::Serialize for #name {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: #serde::Serializer,
|
||||
{
|
||||
<#repr as #serde::Serialize>::serialize(&self.into_inner(), serializer)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
let deserialize = maybe(args.de, || {
|
||||
quote! {
|
||||
const _: () = {
|
||||
use ::core::result::Result;
|
||||
|
||||
impl<'de> #serde::Deserialize<'de> for #name {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: #serde::Deserializer<'de>,
|
||||
{
|
||||
Self::new(<#repr as #serde::Deserialize<'de>>::deserialize(deserializer)?)
|
||||
.ok_or_else(|| #serde::de::Error::custom(#eva::generic::OutOfRange))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let ranges = {
|
||||
let ranges_no = args.range.ranges_len();
|
||||
let repr = &args.repr;
|
||||
let ranges = args.range.ranges().map(|r| {
|
||||
let from = int_lit(*r.start());
|
||||
let to = int_lit(*r.end());
|
||||
|
||||
quote! { #from..=#to }
|
||||
});
|
||||
let values_no = rs_len(&args.range);
|
||||
let values = args.range.iter().map(|x| {
|
||||
let lit = int_lit(x);
|
||||
quote! { #lit }
|
||||
});
|
||||
|
||||
let variants = args.range.iter().map(|x| {
|
||||
let variant = variant_name(x);
|
||||
quote! { Self::#variant }
|
||||
});
|
||||
|
||||
quote! {
|
||||
impl #name {
|
||||
#vis const RANGES: [::std::ops::RangeInclusive<#repr>; #ranges_no] = [#(#ranges),*];
|
||||
#vis const VALUES: [#repr; #values_no] = [#(#values),*];
|
||||
#vis const VARIANTS: [Self; #values_no] = [#(#variants),*];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let nth_impl = {
|
||||
let mut skip = 0;
|
||||
let arms = args.range.ranges().fold(quote! {}, |acc, range| {
|
||||
let skip_cur = int_lit(skip);
|
||||
skip += *range.end() - *range.start() + 1;
|
||||
let start = int_lit(*range.start());
|
||||
let end = int_lit(*range.end());
|
||||
|
||||
quote! {
|
||||
#acc
|
||||
#start ..= #end => (value - #start + #skip_cur) as usize,
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
impl #name {
|
||||
/// Get index in the [`Self::VARIANTS`] array.
|
||||
pub const fn nth(self) -> usize {
|
||||
let value = self.into_inner();
|
||||
match value {
|
||||
#arms
|
||||
|
||||
_ => unsafe { ::core::hint::unreachable_unchecked() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let rand_impl = {
|
||||
if args.range.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! {
|
||||
impl #eva::rand::distr::Distribution<#name> for #eva::rand::distr::StandardUniform {
|
||||
fn sample<R: #eva::rand::Rng + ?Sized>(&self, rng: &mut R) -> #name {
|
||||
let idx: usize = <R as #eva::rand::Rng>::random_range(rng, 0..#name::VARIANTS.len());
|
||||
#name::VARIANTS[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[repr(#repr)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
#(#attrs)*
|
||||
#vis enum #name {
|
||||
#(#variants),*
|
||||
}
|
||||
|
||||
#nth_impl
|
||||
#rand_impl
|
||||
#ranges
|
||||
|
||||
#serialize
|
||||
#deserialize
|
||||
|
||||
#json_schema
|
||||
|
||||
const _: () = {
|
||||
use ::core::{mem, fmt, slice, cmp, result::Result, option::Option};
|
||||
|
||||
impl cmp::PartialEq<#repr> for #name {
|
||||
fn eq(&self, other: &#repr) -> bool {
|
||||
*other == *self as #repr
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::PartialOrd for #name {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::Ord for #name {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
self.into_inner().cmp(&other.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl #name {
|
||||
/// # Safety
|
||||
///
|
||||
/// The `repr` must be in range.
|
||||
#vis const unsafe fn new_unchecked(repr: #repr) -> Self {
|
||||
unsafe { mem::transmute::<#repr, Self>(repr) }
|
||||
}
|
||||
|
||||
/// Create integer from underlying representation.
|
||||
#vis const fn new(repr: #repr) -> Option<Self> {
|
||||
#(#range_checks)*
|
||||
None
|
||||
}
|
||||
|
||||
/// Convert slice into slice of underlying integer
|
||||
/// representation.
|
||||
#vis const fn as_repr_slice(slice: &[Self]) -> &[#repr] {
|
||||
// SAFETY: #repr type domain is wider than ours.
|
||||
unsafe { slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) }
|
||||
}
|
||||
|
||||
/// Convert mutable slice into mutable slice of underlying integer
|
||||
/// representation.
|
||||
#vis const fn as_repr_slice_mut(slice: &mut [Self]) -> &mut [#repr] {
|
||||
// SAFETY: #repr type domain is wider than ours.
|
||||
unsafe { slice::from_raw_parts_mut(slice.as_mut_ptr().cast(), slice.len()) }
|
||||
}
|
||||
|
||||
/// Convert integer into its representation.
|
||||
#vis const fn into_inner(self) -> #repr {
|
||||
self as #repr
|
||||
}
|
||||
}
|
||||
|
||||
impl Copy for #name {}
|
||||
impl Clone for #name {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<#name> for #repr {
|
||||
fn from(val: #name) -> Self {
|
||||
val as #repr
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for #name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let underlying: #repr = (*self).into();
|
||||
<#repr as fmt::Debug>::fmt(&underlying, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for #name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let underlying: #repr = (*self).into();
|
||||
<#repr as fmt::Display>::fmt(&underlying, f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
let body = syn::parse_macro_input!(body as syn::ItemEnum);
|
||||
let args: Args = syn::parse_macro_input!(args as Args);
|
||||
|
||||
mk_enum(&body, &args)
|
||||
}
|
69
macros/src/lib.rs
Normal file
69
macros/src/lib.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
#![expect(warnings)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error::proc_macro_error;
|
||||
|
||||
/// A "property" (trait). Includes set of utilities for working
|
||||
/// with traits.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn prop(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn endpoint(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
self::endpoint::transform(args, body)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(RastGawno, attributes(display, from, bounds))]
|
||||
pub fn rast_gawno(body: TokenStream) -> TokenStream {
|
||||
quote::quote! {}.into()
|
||||
}
|
||||
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn data(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
self::data::transform(args, body)
|
||||
}
|
||||
|
||||
/// An integer type in a specified range.
|
||||
///
|
||||
/// ```rust
|
||||
/// #[int(u8, 0..=100)]
|
||||
/// pub enum Rating {}
|
||||
///
|
||||
/// #[int(u8, 1..=8 | 70..=100)]
|
||||
/// pub enum Age {}
|
||||
/// ```
|
||||
///
|
||||
/// Extension around [`sum`] macro.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn int(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
self::int::transform(args, body)
|
||||
}
|
||||
|
||||
/// A string type.
|
||||
///
|
||||
/// ```rust
|
||||
/// #[str(fixed)]
|
||||
/// struct Fixed([ascii::Printable; 4]);
|
||||
///
|
||||
/// #[str(newtype)]
|
||||
/// struct Newtype(Fixed);
|
||||
/// ```
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn str(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
self::str::transform(args, body)
|
||||
}
|
||||
|
||||
mod utils;
|
||||
|
||||
mod int;
|
||||
mod str;
|
||||
|
||||
mod data;
|
||||
mod endpoint;
|
472
macros/src/str.rs
Normal file
472
macros/src/str.rs
Normal file
|
@ -0,0 +1,472 @@
|
|||
use syn::{Token, parse};
|
||||
|
||||
use crate::utils;
|
||||
|
||||
use quote::quote;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error::{OptionExt as _, abort};
|
||||
|
||||
pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
|
||||
let mut args = syn::parse_macro_input!(args as Args);
|
||||
let mut input = syn::parse_macro_input!(body as syn::DeriveInput);
|
||||
|
||||
let eva = &args.crate_;
|
||||
let ident = input.ident.clone();
|
||||
let mut impls = quote! {};
|
||||
|
||||
let docs = utils::collect_docs(&input.attrs);
|
||||
|
||||
let priv_ = utils::join(&eva, "_priv");
|
||||
let serde = utils::join(&priv_, "serde");
|
||||
let schemars = utils::join(&priv_, "schemars");
|
||||
|
||||
let (ig, tyg, where_clause) = input.generics.split_for_impl();
|
||||
let mut json_schema = quote! {};
|
||||
|
||||
match args.kind {
|
||||
Kind::Fixed { error_ty } => {
|
||||
input.attrs.push(syn::parse_quote!(#[repr(C)]));
|
||||
|
||||
let mut generics = input.generics.clone();
|
||||
let syn::Data::Struct(s) = &input.data else {
|
||||
todo!()
|
||||
};
|
||||
let fields = &s.fields;
|
||||
|
||||
args.copy = true;
|
||||
args.clone = true;
|
||||
|
||||
let mut pats = quote! {};
|
||||
|
||||
for field in fields {
|
||||
let ty = &field.ty;
|
||||
pats = quote! {
|
||||
#pats
|
||||
<#ty as #eva::str::HasPattern>::pat_into(buf);
|
||||
};
|
||||
let predicate = syn::PredicateType {
|
||||
lifetimes: None,
|
||||
bounded_ty: ty.clone(),
|
||||
colon_token: Token),
|
||||
bounds: syn::parse_quote!(#eva::str::FixedUtf8),
|
||||
};
|
||||
let predicate = syn::WherePredicate::Type(predicate);
|
||||
if let Some(ref mut clause) = generics.where_clause {
|
||||
clause.predicates.push(predicate);
|
||||
} else {
|
||||
generics.where_clause = Some(syn::parse_quote! {
|
||||
where #predicate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let (ig, tyg, where_clause) = generics.split_for_impl();
|
||||
let schema_id = quote! {
|
||||
concat!(module_path!(), "::", stringify!(#ident))
|
||||
};
|
||||
let desc = if let Some(desc) = docs.as_deref() {
|
||||
quote! { "description": #desc, }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let check = {
|
||||
let tys = fields.iter().map(|f| &f.ty);
|
||||
let expected_size = quote! {
|
||||
0_usize #(+ ::std::mem::size_of::<#tys>())*
|
||||
};
|
||||
quote! {
|
||||
let expected_size = #expected_size;
|
||||
if s.len() != expected_size {
|
||||
return Err(<#error_ty as #eva::str::FixedParseError>::length(expected_size));
|
||||
}
|
||||
}
|
||||
};
|
||||
let parse = utils::construct(&fields, |field| {
|
||||
let ty = &field.ty;
|
||||
quote! {{
|
||||
let ty_size = ::std::mem::size_of::<#ty>();
|
||||
let chunk = unsafe { s.get_unchecked(..ty_size) };
|
||||
let res = chunk.parse()?;
|
||||
s = unsafe { s.get_unchecked(ty_size..) };
|
||||
res
|
||||
}}
|
||||
});
|
||||
impls = quote! {
|
||||
#impls
|
||||
|
||||
const _: () = {
|
||||
use ::core::{primitive::str, result::Result, str::FromStr};
|
||||
use ::std::{borrow::Cow, string::String};
|
||||
|
||||
impl #ig #schemars::JsonSchema for #ident #tyg #where_clause {
|
||||
#[inline]
|
||||
fn schema_id() -> Cow<'static, str> {
|
||||
Cow::Borrowed(#schema_id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed(stringify!(#ident))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn json_schema(_: &mut #schemars::SchemaGenerator) -> #schemars::Schema {
|
||||
let len = std::mem::size_of::<Self>();
|
||||
let mut pat = String::from("^");
|
||||
<Self as #eva::str::HasPattern>::pat_into(&mut pat);
|
||||
pat.push('$');
|
||||
|
||||
#schemars::json_schema!({
|
||||
"type": "string",
|
||||
#desc
|
||||
"minLength": len,
|
||||
"maxLength": len,
|
||||
"pattern": pat,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig #eva::str::HasPattern for #ident #tyg #where_clause {
|
||||
#[inline]
|
||||
fn pat_into(buf: &mut String) {
|
||||
#pats
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl #ig #eva::str::FixedUtf8 for #ident #tyg #where_clause {}
|
||||
impl #ig #ident #tyg #where_clause {
|
||||
/// Get str representation.
|
||||
pub const fn as_str(&self) -> &str {
|
||||
#eva::str::reinterpret(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig FromStr for #ident #tyg #where_clause {
|
||||
type Err = #error_ty;
|
||||
|
||||
#[inline]
|
||||
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
|
||||
#check
|
||||
Ok(Self #parse)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Kind::Custom => {}
|
||||
|
||||
Kind::Newtype => {
|
||||
let syn::Data::Struct(s) = &input.data else {
|
||||
abort!(input, "this must be struct");
|
||||
};
|
||||
let field = s
|
||||
.fields
|
||||
.iter()
|
||||
.next()
|
||||
.expect_or_abort("struct must have at least one field");
|
||||
let ty = &field.ty;
|
||||
let access = if let Some(name) = &field.ident {
|
||||
quote! { #name }
|
||||
} else {
|
||||
quote! { 0 }
|
||||
};
|
||||
let schemars_path = quote! {#schemars}.to_string();
|
||||
json_schema = quote! {
|
||||
#[derive(#schemars::JsonSchema)]
|
||||
#[schemars(crate = #schemars_path)]
|
||||
};
|
||||
|
||||
if args.pat {
|
||||
impls = quote! {
|
||||
#impls
|
||||
|
||||
const _: () = {
|
||||
use ::std::string::String;
|
||||
|
||||
impl #ig #eva::str::HasPattern for #ident #tyg #where_clause {
|
||||
#[inline(always)]
|
||||
fn pat_into(buf: &mut String) {
|
||||
<#ty as #eva::str::HasPattern>::pat_into(buf)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
let mut first = true;
|
||||
let parse = utils::construct(&s.fields, |field| {
|
||||
if first {
|
||||
first = false;
|
||||
let ty = &field.ty;
|
||||
quote! {
|
||||
<#ty as FromStr>::from_str(s)?
|
||||
}
|
||||
} else {
|
||||
quote! { ::core::default::Default::default() }
|
||||
}
|
||||
});
|
||||
|
||||
impls = quote! {
|
||||
#impls
|
||||
|
||||
const _: () = {
|
||||
use ::core::{str::FromStr, result::Result};
|
||||
|
||||
impl #ig FromStr for #ident #tyg #where_clause {
|
||||
type Err = <#ty as FromStr>::Err;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self #parse)
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig #ident #tyg #where_clause {
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.#access.as_str()
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let ser = utils::maybe(args.ser, || {
|
||||
quote! {
|
||||
const _: () = {
|
||||
use ::core::result::Result;
|
||||
|
||||
impl #ig #serde::Serialize for #ident #tyg #where_clause {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: #serde::Serializer,
|
||||
{
|
||||
<str as #serde::Serialize>::serialize(self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
let de = utils::maybe(args.de, || {
|
||||
let mut generics = input.generics.clone();
|
||||
let lt: syn::Lifetime = syn::parse_quote!('__de);
|
||||
generics.params.insert(
|
||||
0,
|
||||
syn::GenericParam::Lifetime(syn::LifetimeParam::new(lt.clone())),
|
||||
);
|
||||
|
||||
let (ig, ..) = generics.split_for_impl();
|
||||
let (_, tyg, where_clause) = input.generics.split_for_impl();
|
||||
|
||||
quote! {
|
||||
const _: () = {
|
||||
use ::core::{result::Result, str::FromStr};
|
||||
|
||||
impl #ig #serde::Deserialize<#lt> for #ident #tyg #where_clause {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: #serde::Deserializer<#lt>,
|
||||
{
|
||||
let s = <#eva::str::CompactString as #serde::Deserialize<#lt>>::deserialize(deserializer)?;
|
||||
<Self as FromStr>::from_str(&s)
|
||||
.map_err(#serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
impls = quote! {
|
||||
#impls
|
||||
|
||||
const _: () = {
|
||||
use ::core::{
|
||||
ops::Deref,
|
||||
borrow::Borrow,
|
||||
convert::AsRef,
|
||||
cmp::{PartialEq, Eq},
|
||||
primitive::str,
|
||||
hash::{Hasher, Hash},
|
||||
fmt,
|
||||
};
|
||||
|
||||
impl #ig fmt::Debug for #ident #tyg #where_clause {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
<str as fmt::Debug>::fmt(self.as_str(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig fmt::Display for #ident #tyg #where_clause {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig Hash for #ident #tyg #where_clause {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.as_str().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig PartialEq<str> for #ident #tyg #where_clause {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.as_str() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig Borrow<str> for #ident #tyg #where_clause {
|
||||
fn borrow(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig AsRef<str> for #ident #tyg #where_clause {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl #ig Deref for #ident #tyg #where_clause {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let clone = utils::maybe(args.clone, || {
|
||||
utils::mk_derive(&syn::parse_quote!(::core::clone::Clone))
|
||||
});
|
||||
let copy = utils::maybe(args.copy, || {
|
||||
utils::mk_derive(&syn::parse_quote!(::core::marker::Copy))
|
||||
});
|
||||
|
||||
quote! {
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
#clone
|
||||
#copy
|
||||
#json_schema
|
||||
#input
|
||||
#impls
|
||||
|
||||
#ser
|
||||
#de
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
struct Args {
|
||||
kind: Kind,
|
||||
ser: bool,
|
||||
de: bool,
|
||||
clone: bool,
|
||||
pat: bool,
|
||||
copy: bool,
|
||||
schema: bool,
|
||||
crate_: syn::Path,
|
||||
}
|
||||
|
||||
impl parse::Parse for Args {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let ser = true;
|
||||
let de = true;
|
||||
let schema = true;
|
||||
let clone = true;
|
||||
let mut copy = false;
|
||||
let mut pat = true;
|
||||
|
||||
let kind = utils::ident(input)?;
|
||||
let kind = if kind == "newtype" {
|
||||
Kind::Newtype
|
||||
} else if kind == "fixed" {
|
||||
let content;
|
||||
syn::parenthesized!(content in input);
|
||||
let mut error_ty = None::<syn::Type>;
|
||||
utils::comma_separated(&content, |ps| {
|
||||
let ident = utils::ident(ps)?;
|
||||
if ident == "error" {
|
||||
utils::assign(ps)?;
|
||||
error_ty = Some(ps.parse()?);
|
||||
} else {
|
||||
return Err(syn::Error::new(ident.span(), "unexpected parameter"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
Kind::Fixed {
|
||||
error_ty: error_ty.expect_or_abort("there must be error type"),
|
||||
}
|
||||
} else if kind == "custom" {
|
||||
Kind::Custom
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
kind.span(),
|
||||
"expected `newtype`, `fixed` or `custom`",
|
||||
));
|
||||
};
|
||||
let mut crate_: syn::Path = syn::parse_quote!(::eva);
|
||||
|
||||
if utils::token_is(input, Token![,]) {
|
||||
utils::comma_separated(input, |ps| {
|
||||
if utils::token_is(ps, Token![crate]) {
|
||||
utils::assign(ps)?;
|
||||
crate_ = ps.parse()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ident = utils::ident(ps)?;
|
||||
if ident == "copy" {
|
||||
copy = true;
|
||||
} else if ident == "not" {
|
||||
let content;
|
||||
syn::parenthesized!(content in ps);
|
||||
utils::comma_separated(&content, |ps| {
|
||||
let key = utils::ident(ps)?;
|
||||
|
||||
if key == "pat" {
|
||||
pat = false;
|
||||
} else {
|
||||
return Err(syn::Error::new(key.span(), "invalid key"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
} else {
|
||||
return Err(syn::Error::new(ident.span(), "unexpected parameter"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(Self {
|
||||
kind,
|
||||
pat,
|
||||
crate_,
|
||||
clone: clone || copy,
|
||||
copy,
|
||||
ser,
|
||||
de,
|
||||
schema,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Kind {
|
||||
/// Newtype over some other string.
|
||||
Newtype,
|
||||
|
||||
/// Fixed size string combined from other fixed
|
||||
/// size strings.
|
||||
Fixed { error_ty: syn::Type },
|
||||
|
||||
/// Custom string, must provide `as_str` and `FromStr`.
|
||||
Custom,
|
||||
}
|
239
macros/src/utils.rs
Normal file
239
macros/src/utils.rs
Normal file
|
@ -0,0 +1,239 @@
|
|||
use syn::{Token, parse, punctuated::Punctuated, spanned::Spanned as _};
|
||||
|
||||
use std::{fmt, ops::Deref};
|
||||
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
|
||||
type PunctFields = Punctuated<syn::Field, Token![,]>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RefOrOwned<'a, T> {
|
||||
Ref(&'a T),
|
||||
Owned(T),
|
||||
}
|
||||
|
||||
impl<'a, T> Deref for RefOrOwned<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Ref(r) => *r,
|
||||
Self::Owned(r) => r,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn once<F, S, I, O>(fst: F, mut snd: S) -> impl FnMut(I) -> O
|
||||
where
|
||||
F: FnOnce(I) -> O,
|
||||
S: FnMut(I) -> O,
|
||||
{
|
||||
let mut fst = Some(fst);
|
||||
move |x| {
|
||||
if let Some(fst) = fst.take() {
|
||||
fst(x)
|
||||
} else {
|
||||
snd(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_bound(to: &mut Option<syn::WhereClause>, pred: syn::WherePredicate) {
|
||||
if let Some(clause) = to {
|
||||
clause.predicates.push(pred);
|
||||
} else {
|
||||
*to = Some(syn::WhereClause {
|
||||
where_token: syn::Token),
|
||||
predicates: {
|
||||
let mut preds = syn::punctuated::Punctuated::new();
|
||||
preds.push(pred);
|
||||
|
||||
preds
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn construct_wrap(
|
||||
fields: &syn::Fields,
|
||||
) -> fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream {
|
||||
match fields {
|
||||
syn::Fields::Named(..) => |x| quote! { { #x } },
|
||||
syn::Fields::Unnamed(..) => |x| quote! { (#x) },
|
||||
syn::Fields::Unit => |_| quote! {},
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_punct() -> Punctuated<syn::Field, Token![,]> {
|
||||
Punctuated::new()
|
||||
}
|
||||
|
||||
pub fn fields_of<'a>(fields: &'a syn::Fields) -> RefOrOwned<'a, PunctFields> {
|
||||
match fields {
|
||||
syn::Fields::Named(fields) => RefOrOwned::Ref(&fields.named),
|
||||
syn::Fields::Unnamed(fields) => RefOrOwned::Ref(&fields.unnamed),
|
||||
syn::Fields::Unit => RefOrOwned::Owned(PunctFields::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ConstructPat {
|
||||
Wildcard(syn::Ident),
|
||||
Other(proc_macro2::TokenStream),
|
||||
}
|
||||
|
||||
impl ConstructPat {
|
||||
fn to_ts(&self) -> proc_macro2::TokenStream {
|
||||
match self {
|
||||
ConstructPat::Wildcard(i) => quote! { #i },
|
||||
ConstructPat::Other(othr) => othr.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn construct_pat<F>(fields: &syn::Fields, mut mk_pat: F) -> proc_macro2::TokenStream
|
||||
where
|
||||
F: FnMut(&syn::Field) -> ConstructPat,
|
||||
{
|
||||
let wrap = construct_wrap(fields);
|
||||
let fields = fields_of(fields);
|
||||
let inside = fields.iter().fold(quote! {}, |acc, field| {
|
||||
let pat = mk_pat(field);
|
||||
if let Some(ident) = &field.ident {
|
||||
'res: {
|
||||
if let ConstructPat::Wildcard(ref wc) = pat {
|
||||
if wc == ident {
|
||||
break 'res quote! { #acc #wc, };
|
||||
}
|
||||
}
|
||||
|
||||
let ts = pat.to_ts();
|
||||
quote! { #acc #ident : #ts, }
|
||||
}
|
||||
} else {
|
||||
let value = pat.to_ts();
|
||||
quote! { #acc #value, }
|
||||
}
|
||||
});
|
||||
|
||||
wrap(inside)
|
||||
}
|
||||
|
||||
pub fn construct<F>(fields: &syn::Fields, mut mk_value: F) -> proc_macro2::TokenStream
|
||||
where
|
||||
F: FnMut(&syn::Field) -> proc_macro2::TokenStream,
|
||||
{
|
||||
let wrap = construct_wrap(fields);
|
||||
let fields = fields_of(fields);
|
||||
let inside = fields.iter().fold(quote! {}, |acc, field| {
|
||||
let value = mk_value(field);
|
||||
if let Some(ident) = &field.ident {
|
||||
quote! { #acc #ident : #value, }
|
||||
} else {
|
||||
quote! { #acc #value, }
|
||||
}
|
||||
});
|
||||
|
||||
wrap(inside)
|
||||
}
|
||||
|
||||
pub fn token_is<F, I, T>(ps: parse::ParseStream, tok: F) -> bool
|
||||
where
|
||||
F: parse::Peek + Copy + FnOnce(I) -> T,
|
||||
T: syn::token::Token + parse::Parse,
|
||||
{
|
||||
if ps.peek(tok) {
|
||||
let _: T = ps.parse().unwrap();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_docs(attrs: &[syn::Attribute]) -> Option<String> {
|
||||
let mut docs = String::new();
|
||||
let mut had = false;
|
||||
|
||||
for attr in attrs {
|
||||
if !attr.path().is_ident("doc") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let syn::Meta::NameValue(nv) = &attr.meta else {
|
||||
abort!(attr, r##"must be in form #[doc = "string literal"]"##);
|
||||
};
|
||||
|
||||
let syn::Expr::Lit(syn::ExprLit {
|
||||
attrs: _,
|
||||
lit: syn::Lit::Str(litstr),
|
||||
}) = &nv.value
|
||||
else {
|
||||
abort!(nv, "must be string literal");
|
||||
};
|
||||
had = true;
|
||||
docs.push_str(&litstr.value());
|
||||
}
|
||||
|
||||
had.then_some(docs.trim().to_owned())
|
||||
}
|
||||
|
||||
pub fn maybe<F>(b: bool, f: F) -> proc_macro2::TokenStream
|
||||
where
|
||||
F: FnOnce() -> proc_macro2::TokenStream,
|
||||
{
|
||||
if b {
|
||||
f()
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mk_derive(path: &syn::Path) -> proc_macro2::TokenStream {
|
||||
quote! { #[derive(#path)] }
|
||||
}
|
||||
|
||||
pub fn try_next(ts: parse::ParseStream) -> bool {
|
||||
if ts.peek(Token![,]) {
|
||||
let _: Token![,] = ts.parse().unwrap();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assign(ts: parse::ParseStream) -> syn::Result<()> {
|
||||
let _: Token![=] = ts.parse()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ident(ts: parse::ParseStream) -> syn::Result<syn::Ident> {
|
||||
ts.parse()
|
||||
}
|
||||
|
||||
pub fn err<T: quote::ToTokens, U: fmt::Display>(tokens: T, msg: U) -> syn::Error {
|
||||
syn::Error::new_spanned(tokens, msg)
|
||||
}
|
||||
|
||||
pub fn join(p: &syn::Path, next: &str) -> syn::Path {
|
||||
let ident = quote::format_ident!("{next}");
|
||||
syn::parse_quote!(#p :: #ident)
|
||||
}
|
||||
|
||||
pub fn comma_separated(
|
||||
ts: parse::ParseStream,
|
||||
mut parse: impl FnMut(parse::ParseStream) -> syn::Result<()>,
|
||||
) -> syn::Result<()> {
|
||||
loop {
|
||||
if ts.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
parse(ts)?;
|
||||
|
||||
if !try_next(ts) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue