Initial commit

This commit is contained in:
Aleksandr 2025-06-22 05:46:26 +03:00
commit 75a589f235
43 changed files with 4840 additions and 0 deletions

603
macros/src/data.rs Normal file
View 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
View 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
View 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
View 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
View 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![:](proc_macro2::Span::mixed_site()),
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
View 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![where](pred.span()),
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(())
}