From 6667a9eb51b54ff488dd192dfaabbbac2df16c50 Mon Sep 17 00:00:00 2001 From: Troy Benson Date: Wed, 1 May 2024 00:56:40 +0000 Subject: [PATCH] feat: dataloader in foundations --- foundations/Cargo.toml | 9 + foundations/examples/src/generics.rs | 16 +- foundations/examples/src/http.rs | 269 +++-- foundations/examples/src/simple.rs | 60 +- foundations/macros/src/bootstrap.rs | 119 ++- foundations/macros/src/helpers.rs | 33 +- foundations/macros/src/lib.rs | 27 +- foundations/macros/src/metrics/mod.rs | 946 +++++++++--------- foundations/macros/src/settings/mod.rs | 189 ++-- .../macros/src/settings/types/enum_ty.rs | 385 ++++--- .../macros/src/settings/types/field_ty.rs | 107 +- foundations/macros/src/settings/types/mod.rs | 178 ++-- .../macros/src/settings/types/serde.rs | 300 +++--- .../macros/src/settings/types/struct_ty.rs | 325 +++--- .../macros/src/settings/types/variant_ty.rs | 412 ++++---- foundations/macros/src/wrapped.rs | 123 ++- foundations/src/dataloader/batch_loader.rs | 22 + foundations/src/dataloader/mod.rs | 358 +++++++ foundations/src/dataloader/types.rs | 101 ++ foundations/src/dataloader/utils.rs | 33 + foundations/src/lib.rs | 3 + 21 files changed, 2243 insertions(+), 1772 deletions(-) create mode 100644 foundations/src/dataloader/batch_loader.rs create mode 100644 foundations/src/dataloader/mod.rs create mode 100644 foundations/src/dataloader/types.rs create mode 100644 foundations/src/dataloader/utils.rs diff --git a/foundations/Cargo.toml b/foundations/Cargo.toml index 94abaf2d..a0f0918c 100644 --- a/foundations/Cargo.toml +++ b/foundations/Cargo.toml @@ -178,6 +178,14 @@ cli = [ "settings", ] +dataloader = [ + "tokio/sync", + "tokio/time", + "runtime", + "futures", + "tracing", +] + bootstrap = [ "settings", "cli", @@ -202,4 +210,5 @@ default = [ "telemetry-server", "context", "signal", + "dataloader", ] diff --git a/foundations/examples/src/generics.rs b/foundations/examples/src/generics.rs index f69f373c..fec473ac 100644 --- a/foundations/examples/src/generics.rs +++ b/foundations/examples/src/generics.rs @@ -2,19 +2,19 @@ use scuffle_foundations::settings::{auto_settings, Settings}; #[auto_settings] pub struct BaseSettings { - #[serde(flatten)] - /// The internal settings. - external: S, + #[serde(flatten)] + /// The internal settings. + external: S, } #[auto_settings] pub struct ExtraSettings { - /// An extra setting. - pub extra: bool, - /// Another extra setting. - pub another: bool, + /// An extra setting. + pub extra: bool, + /// Another extra setting. + pub another: bool, } fn main() { - println!("{}", BaseSettings::::default().to_yaml_string().unwrap()); + println!("{}", BaseSettings::::default().to_yaml_string().unwrap()); } diff --git a/foundations/examples/src/http.rs b/foundations/examples/src/http.rs index 2380b858..67282130 100644 --- a/foundations/examples/src/http.rs +++ b/foundations/examples/src/http.rs @@ -1,20 +1,20 @@ +use std::convert::Infallible; +use std::net::{SocketAddr, TcpListener as StdTcpListener}; + use hyper::body::{Bytes, Incoming}; use hyper::service::service_fn; use hyper::{Request, Response}; use hyper_util::rt::TokioIo; use opentelemetry::trace::Status; use rand::Rng; -use socket2::Socket; - use scuffle_foundations::bootstrap::{bootstrap, Bootstrap, RuntimeSettings}; use scuffle_foundations::runtime::spawn; +use scuffle_foundations::settings::auto_settings; use scuffle_foundations::settings::cli::Matches; use scuffle_foundations::telementry::opentelemetry::OpenTelemetrySpanExt; use scuffle_foundations::telementry::settings::TelementrySettings; -use scuffle_foundations::{settings::auto_settings, wrapped, BootstrapResult}; -use std::convert::Infallible; -use std::net::SocketAddr; -use std::net::TcpListener as StdTcpListener; +use scuffle_foundations::{wrapped, BootstrapResult}; +use socket2::Socket; use tokio::net::{TcpListener, TcpStream}; type Body = http_body_util::Full; @@ -22,192 +22,185 @@ type Body = http_body_util::Full; #[auto_settings] #[serde(default)] struct Config { - telemetry: TelementrySettings, - runtime: RuntimeSettings, - #[settings(default = SocketAddr::from(([127, 0, 0, 1], 8080)))] - bind: SocketAddr, + telemetry: TelementrySettings, + runtime: RuntimeSettings, + #[settings(default = SocketAddr::from(([127, 0, 0, 1], 8080)))] + bind: SocketAddr, + #[settings(default = 1)] + listener_count: usize, } impl Bootstrap for Config { - type Settings = Self; + type Settings = Self; - fn runtime_mode(&self) -> RuntimeSettings { - self.runtime.clone() - } + fn runtime_mode(&self) -> RuntimeSettings { + self.runtime.clone() + } - fn telemetry_config(&self) -> Option { - Some(self.telemetry.clone()) - } + fn telemetry_config(&self) -> Option { + Some(self.telemetry.clone()) + } } fn create_listner(bind: SocketAddr) -> BootstrapResult { - let listener = Socket::new( - socket2::Domain::IPV4, - socket2::Type::STREAM, - Some(socket2::Protocol::TCP), - )?; + let listener = Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?; - listener.set_reuse_address(true)?; - listener.set_reuse_port(true)?; - listener.set_keepalive(true)?; + listener.set_reuse_address(true)?; + listener.set_reuse_port(true)?; + listener.set_keepalive(true)?; - listener.bind(&bind.into())?; - listener.set_nonblocking(true)?; - listener.listen(1024)?; + listener.bind(&bind.into())?; + listener.set_nonblocking(true)?; + listener.listen(1024)?; - Ok(listener.into()) + Ok(listener.into()) } #[bootstrap] async fn main(cli: Matches) -> BootstrapResult<()> { - tracing::info!("starting"); + tracing::info!("starting"); - for i in 0..1 { - let listener = create_listner(cli.settings.bind)?; + for i in 0..cli.settings.listener_count { + let listener = create_listner(cli.settings.bind)?; - tracing::debug!(idx = %i, "starting listener"); - spawn(run_endpoint(i, listener)); - } + tracing::debug!(idx = %i, "starting listener"); + spawn(run_endpoint(i, listener)); + } - tracing::info!("started"); + tracing::info!("started"); - scuffle_foundations::signal::SignalHandler::new() - .with_signal(tokio::signal::unix::SignalKind::interrupt()) - .with_signal(tokio::signal::unix::SignalKind::terminate()) - .recv() - .await; + scuffle_foundations::signal::SignalHandler::new() + .with_signal(tokio::signal::unix::SignalKind::interrupt()) + .with_signal(tokio::signal::unix::SignalKind::terminate()) + .recv() + .await; - tracing::info!("stopping"); + tracing::info!("stopping"); - scuffle_foundations::context::Handler::global() - .shutdown() - .await; + scuffle_foundations::context::Handler::global().shutdown().await; - tracing::info!("stopped"); + tracing::info!("stopped"); - Ok(()) + Ok(()) } #[tracing::instrument(skip(listener))] async fn run_endpoint(idx: usize, listener: StdTcpListener) -> BootstrapResult<()> { - let listener = TcpListener::from_std(listener)?; - - tracing::info!("listening"); - - loop { - match listener.accept().await { - Ok((conn, client_addr)) => { - spawn(serve_connection(conn, client_addr)); - } - Err(e) => { - tracing::error!(err = %e, "failed to accept connection"); - } - } - } + let listener = TcpListener::from_std(listener)?; + + tracing::info!("listening"); + + loop { + match listener.accept().await { + Ok((conn, client_addr)) => { + spawn(serve_connection(conn, client_addr)); + } + Err(e) => { + tracing::error!(err = %e, "failed to accept connection"); + } + } + } } #[tracing::instrument(skip(conn))] async fn serve_connection(conn: TcpStream, _: SocketAddr) { - tracing::trace!("accepted client connection"); + tracing::trace!("accepted client connection"); - let on_request = service_fn(respond); + let on_request = service_fn(respond); - let mut http = hyper::server::conn::http1::Builder::new(); + let mut http = hyper::server::conn::http1::Builder::new(); - http.keep_alive(true); + http.keep_alive(true); - http.serve_connection(TokioIo::new(conn), on_request) - .await - .ok(); + http.serve_connection(TokioIo::new(conn), on_request).await.ok(); - tracing::trace!("closed client connection"); + tracing::trace!("closed client connection"); } #[wrapped(map_response)] #[tracing::instrument(skip(req), fields(path = req.uri().path(), method = req.method().as_str(), response.status))] async fn respond(req: Request) -> Result, Infallible> { - tracing::Span::current().make_root(); - tracing::trace!("received request"); - - let response = match req.uri().path() { - "/hello" => hello_req(req).await?, - _ => { - let body = Bytes::from_static(b"Not Found"); - Response::builder() - .status(404) - .header("Content-Type", "text/plain") - .body(body.into()) - .unwrap() - } - }; - - Ok(response) + tracing::Span::current().make_root(); + tracing::trace!("received request"); + + let response = match req.uri().path() { + "/hello" => hello_req(req).await?, + _ => { + let body = Bytes::from_static(b"Not Found"); + Response::builder() + .status(404) + .header("Content-Type", "text/plain") + .body(body.into()) + .unwrap() + } + }; + + Ok(response) } fn map_response(result: Result, Infallible>) -> Result, Infallible> { - let span = tracing::Span::current(); - tracing::debug!("where am i?"); - - result - .map(|mut ok| { - span.record("response.status", ok.status().as_u16()); - span.set_status(Status::Ok); - - span.trace_id().map(|trace_id| { - ok.headers_mut() - .insert("X-Ray-Id", trace_id.to_string().parse().unwrap()); - }); - - ok - }) - .inspect_err(|err| { - span.record("response.status", 500); - span.set_status(Status::Error { - description: err.to_string().into(), - }); - }) + let span = tracing::Span::current(); + tracing::debug!("where am i?"); + + result + .map(|mut ok| { + span.record("response.status", ok.status().as_u16()); + span.set_status(Status::Ok); + + span.trace_id().map(|trace_id| { + ok.headers_mut().insert("X-Ray-Id", trace_id.to_string().parse().unwrap()); + }); + + ok + }) + .inspect_err(|err| { + span.record("response.status", 500); + span.set_status(Status::Error { + description: err.to_string().into(), + }); + }) } #[wrapped(map_response_resource)] #[tracing::instrument] async fn load_resource() -> Result<(), &'static str> { - tokio::time::sleep(std::time::Duration::from_millis(30)).await; - if rand::thread_rng().gen_bool(0.01) { - Err("failed to load resource") - } else { - Ok(()) - } + tokio::time::sleep(std::time::Duration::from_millis(30)).await; + if rand::thread_rng().gen_bool(0.01) { + Err("failed to load resource") + } else { + Ok(()) + } } fn map_response_resource(result: Result<(), &'static str>) -> Result<(), &'static str> { - let span = tracing::Span::current(); - - result - .inspect(|_| { - span.set_status(Status::Ok); - }) - .inspect_err(|err| { - span.set_status(Status::Error { - description: err.to_string().into(), - }); - }) + let span = tracing::Span::current(); + + result + .inspect(|_| { + span.set_status(Status::Ok); + }) + .inspect_err(|err| { + span.set_status(Status::Error { + description: err.to_string().into(), + }); + }) } #[tracing::instrument] async fn hello_req(_: Request) -> Result, Infallible> { - let body = Bytes::from_static(b"Hello, World!"); - - if let Err(err) = load_resource().await { - Ok(Response::builder() - .status(500) - .header("Content-Type", "text/plain") - .body(Bytes::from(err).into()) - .unwrap()) - } else { - Ok(Response::builder() - .status(200) - .header("Content-Type", "text/plain") - .body(body.into()) - .unwrap()) - } + let body = Bytes::from_static(b"Hello, World!"); + + if let Err(err) = load_resource().await { + Ok(Response::builder() + .status(500) + .header("Content-Type", "text/plain") + .body(Bytes::from(err).into()) + .unwrap()) + } else { + Ok(Response::builder() + .status(200) + .header("Content-Type", "text/plain") + .body(body.into()) + .unwrap()) + } } diff --git a/foundations/examples/src/simple.rs b/foundations/examples/src/simple.rs index 26aab1ab..fc2eea1f 100644 --- a/foundations/examples/src/simple.rs +++ b/foundations/examples/src/simple.rs @@ -1,54 +1,54 @@ -use scuffle_foundations::{ - bootstrap::{bootstrap, Bootstrap, RuntimeSettings}, - settings::{cli::Matches, auto_settings}, - telementry::{metrics::metrics, settings::TelementrySettings}, -}; +use scuffle_foundations::bootstrap::{bootstrap, Bootstrap, RuntimeSettings}; +use scuffle_foundations::settings::auto_settings; +use scuffle_foundations::settings::cli::Matches; +use scuffle_foundations::telementry::metrics::metrics; +use scuffle_foundations::telementry::settings::TelementrySettings; #[metrics] mod http_server { - use std::sync::Arc; + use std::sync::Arc; - use scuffle_foundations::telementry::metrics::prometheus_client::metrics::counter::Counter; + use scuffle_foundations::telementry::metrics::prometheus_client::metrics::counter::Counter; - /// Number of active client connections. - pub fn active_connections(endpoint_name: &str) -> Counter; + /// Number of active client connections. + pub fn active_connections(endpoint_name: &str) -> Counter; - /// Number of failed client connections. - pub fn failed_connections_total(endpoint_name: &Arc) -> Counter; + /// Number of failed client connections. + pub fn failed_connections_total(endpoint_name: &Arc) -> Counter; - /// Number of HTTP requests. - /// xd - pub fn requests_total(endpoint_name: &Arc) -> Counter; + /// Number of HTTP requests. + /// xd + pub fn requests_total(endpoint_name: &Arc) -> Counter; - /// Number of failed requests. - pub fn requests_failed_total(endpoint_name: &Arc, status_code: u16) -> Counter; + /// Number of failed requests. + pub fn requests_failed_total(endpoint_name: &Arc, status_code: u16) -> Counter; } #[auto_settings] pub struct HttpServerSettings { - /// Telementry Settings - telementry: TelementrySettings, - /// Runtime Settings - runtime: RuntimeSettings, + /// Telementry Settings + telementry: TelementrySettings, + /// Runtime Settings + runtime: RuntimeSettings, } impl Bootstrap for HttpServerSettings { - type Settings = Self; + type Settings = Self; - fn runtime_mode(&self) -> RuntimeSettings { - self.runtime.clone() - } + fn runtime_mode(&self) -> RuntimeSettings { + self.runtime.clone() + } - fn telemetry_config(&self) -> Option { - Some(self.telementry.clone()) - } + fn telemetry_config(&self) -> Option { + Some(self.telementry.clone()) + } } #[bootstrap] async fn main(settings: Matches) { - tracing::info!("hello world"); + tracing::info!("hello world"); - dbg!(&settings); + dbg!(&settings); - tokio::signal::ctrl_c().await.unwrap(); + tokio::signal::ctrl_c().await.unwrap(); } diff --git a/foundations/macros/src/bootstrap.rs b/foundations/macros/src/bootstrap.rs index 6840c39e..f4479e4d 100644 --- a/foundations/macros/src/bootstrap.rs +++ b/foundations/macros/src/bootstrap.rs @@ -1,86 +1,85 @@ -use darling::{ast::NestedMeta, FromMeta}; +use darling::ast::NestedMeta; +use darling::FromMeta; use proc_macro::TokenStream; -use syn::{parse_quote, punctuated::Punctuated, Token}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, Token}; #[derive(FromMeta)] #[darling(default)] struct BootstrapArgs { - crate_name: syn::Path, + crate_name: syn::Path, } impl Default for BootstrapArgs { - fn default() -> Self { - BootstrapArgs { - crate_name: parse_quote!(::scuffle_foundations), - } - } + fn default() -> Self { + BootstrapArgs { + crate_name: parse_quote!(::scuffle_foundations), + } + } } impl syn::parse::Parse for BootstrapArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - if input.is_empty() { - Ok(BootstrapArgs::default()) - } else { - let meta_list = Punctuated::::parse_terminated(input)? - .into_iter() - .collect::>(); - Ok(BootstrapArgs::from_list(&meta_list)?) - } - } + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.is_empty() { + Ok(BootstrapArgs::default()) + } else { + let meta_list = Punctuated::::parse_terminated(input)? + .into_iter() + .collect::>(); + Ok(BootstrapArgs::from_list(&meta_list)?) + } + } } /// #[bootstrap(Type, crate = "crate_name")] pub fn bootstrap(args: TokenStream, input: TokenStream) -> syn::Result { - let args = syn::parse::(args)?; - let input = syn::parse::(input)?; + let args = syn::parse::(args)?; + let input = syn::parse::(input)?; - let name = &input.sig.ident; + let name = &input.sig.ident; - // The main function should have a single argument - if input.sig.inputs.len() != 1 { - return Err(syn::Error::new_spanned( - input.sig.ident, - "bootstrap function must have a single argument", - )); - } + // The main function should have a single argument + if input.sig.inputs.len() != 1 { + return Err(syn::Error::new_spanned( + input.sig.ident, + "bootstrap function must have a single argument", + )); + } - // Main should be async - if input.sig.asyncness.is_none() { - return Err(syn::Error::new_spanned( - input.sig.fn_token, - "bootstrap function must be async", - )); - } + // Main should be async + if input.sig.asyncness.is_none() { + return Err(syn::Error::new_spanned( + input.sig.fn_token, + "bootstrap function must be async", + )); + } - let ret = &input.sig.output; + let ret = &input.sig.output; - // Sometimes the return value will not be specified - let call_fn = match ret { - syn::ReturnType::Default => quote::quote! { |settings| async move { - #name(settings).await; - Ok(()) - } }, - _ => quote::quote! { #name }, - }; + // Sometimes the return value will not be specified + let call_fn = match ret { + syn::ReturnType::Default => quote::quote! { |settings| async move { + #name(settings).await; + Ok(()) + } }, + _ => quote::quote! { #name }, + }; - let handle_result = match ret { - syn::ReturnType::Default => quote::quote! { .unwrap(); }, - _ => quote::quote! {}, - }; + let handle_result = match ret { + syn::ReturnType::Default => quote::quote! { .unwrap(); }, + _ => quote::quote! {}, + }; - let crate_name = &args.crate_name; + let crate_name = &args.crate_name; - let cfg_attrs = input - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cfg")); + let cfg_attrs = input.attrs.iter().filter(|attr| attr.path().is_ident("cfg")); - Ok(quote::quote! { - #(#cfg_attrs)* - fn #name() #ret { - #input + Ok(quote::quote! { + #(#cfg_attrs)* + fn #name() #ret { + #input - #crate_name::bootstrap::bootstrap(&::std::default::Default::default(), #crate_name::service_info!(), #call_fn)#handle_result - } - }) + #crate_name::bootstrap::bootstrap(&::std::default::Default::default(), #crate_name::service_info!(), #call_fn)#handle_result + } + }) } diff --git a/foundations/macros/src/helpers.rs b/foundations/macros/src/helpers.rs index f395f3dd..85c47f53 100644 --- a/foundations/macros/src/helpers.rs +++ b/foundations/macros/src/helpers.rs @@ -1,20 +1,19 @@ /// #[doc = "docs"] or /// docs pub fn parse_docs(attr: &[syn::Attribute]) -> Vec { - attr.iter() - .filter(|attr| attr.path().is_ident("doc")) - .filter_map(|attr| match &attr.meta { - syn::Meta::NameValue(meta) => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - Some(syn::LitStr::new(lit.value().trim(), lit.span())) - } else { - None - } - } - _ => None, - }) - .collect() + attr.iter() + .filter(|attr| attr.path().is_ident("doc")) + .filter_map(|attr| match &attr.meta { + syn::Meta::NameValue(meta) => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + Some(syn::LitStr::new(lit.value().trim(), lit.span())) + } else { + None + } + } + _ => None, + }) + .collect() } diff --git a/foundations/macros/src/lib.rs b/foundations/macros/src/lib.rs index df2d4610..f162d496 100644 --- a/foundations/macros/src/lib.rs +++ b/foundations/macros/src/lib.rs @@ -7,16 +7,18 @@ mod settings; mod wrapped; /// #[wrapped(inspect_output)] -/// async fn respond(req: Request) -> Result, Infallible> { todo!() } +/// async fn respond(req: Request) -> Result, +/// Infallible> { todo!() } /// /// -/// async fn respond(req: Request) -> Result, Infallible> { -/// async fn __inspect_output(req: Request) -> Result, Infallible> { todo!() } +/// async fn respond(req: Request) -> Result, +/// Infallible> { async fn __inspect_output(req: Request) -> +/// Result, Infallible> { todo!() } /// inspect_output(__wrapped(req).await) /// } #[proc_macro_attribute] pub fn wrapped(args: TokenStream, input: TokenStream) -> TokenStream { - handle_error(wrapped::wrapped(args, input)) + handle_error(wrapped::wrapped(args, input)) } /// #[settings] @@ -32,30 +34,29 @@ pub fn wrapped(args: TokenStream, input: TokenStream) -> TokenStream { /// /// The log level to use. /// pub level: String, /// } -/// #[proc_macro_attribute] pub fn auto_settings(args: TokenStream, input: TokenStream) -> TokenStream { - handle_error(settings::settings(args, input)) + handle_error(settings::settings(args, input)) } #[proc_macro_derive(Settings, attributes(settings))] pub fn derive_settings(input: TokenStream) -> TokenStream { - handle_error(settings::derive_settings(input)) + handle_error(settings::derive_settings(input)) } #[proc_macro_attribute] pub fn metrics(args: TokenStream, input: TokenStream) -> TokenStream { - handle_error(metrics::metrics(args, input)) + handle_error(metrics::metrics(args, input)) } #[proc_macro_attribute] pub fn bootstrap(args: TokenStream, input: TokenStream) -> TokenStream { - handle_error(bootstrap::bootstrap(args, input)) + handle_error(bootstrap::bootstrap(args, input)) } fn handle_error>(result: Result) -> TokenStream { - match result { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into(), - } + match result { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } } diff --git a/foundations/macros/src/metrics/mod.rs b/foundations/macros/src/metrics/mod.rs index 9fca2d0b..6f8bb5a9 100644 --- a/foundations/macros/src/metrics/mod.rs +++ b/foundations/macros/src/metrics/mod.rs @@ -1,538 +1,516 @@ -use darling::{ast::NestedMeta, FromMeta}; +use darling::ast::NestedMeta; +use darling::FromMeta; use quote::ToTokens; -use syn::{parse::Parse, punctuated::Punctuated, spanned::Spanned, Token}; +use syn::parse::Parse; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::Token; #[derive(Debug, FromMeta)] #[darling(default)] struct Options { - crate_path: Option, - optional: bool, - builder: Option, + crate_path: Option, + optional: bool, + builder: Option, } impl Options { - fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result { - let mut optional = false; - let mut builder = None; - - for attr in attrs { - match &attr.meta { - syn::Meta::Path(path) => { - if path.is_ident("optional") { - if optional { - return Err(syn::Error::new_spanned(attr, "duplicate attribute")); - } - - optional = true; - } - } - syn::Meta::NameValue(syn::MetaNameValue { path, value, .. }) => { - if path.is_ident("builder") { - if builder.is_some() { - return Err(syn::Error::new_spanned(attr, "duplicate attribute")); - } - - builder = Some(value.clone()); - } - } - _ => return Err(syn::Error::new_spanned(attr, "unexpected attribute")), - } - } - - Ok(Options { - crate_path: None, - optional, - builder, - }) - } + fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result { + let mut optional = false; + let mut builder = None; + + for attr in attrs { + match &attr.meta { + syn::Meta::Path(path) => { + if path.is_ident("optional") { + if optional { + return Err(syn::Error::new_spanned(attr, "duplicate attribute")); + } + + optional = true; + } + } + syn::Meta::NameValue(syn::MetaNameValue { path, value, .. }) => { + if path.is_ident("builder") { + if builder.is_some() { + return Err(syn::Error::new_spanned(attr, "duplicate attribute")); + } + + builder = Some(value.clone()); + } + } + _ => return Err(syn::Error::new_spanned(attr, "unexpected attribute")), + } + } + + Ok(Options { + crate_path: None, + optional, + builder, + }) + } } impl Default for Options { - fn default() -> Self { - Options { - crate_path: None, - optional: false, - builder: None, - } - } + fn default() -> Self { + Options { + crate_path: None, + optional: false, + builder: None, + } + } } impl Parse for Options { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - if input.is_empty() { - Ok(Options::default()) - } else { - let meta_list = Punctuated::::parse_terminated(input)? - .into_iter() - .collect::>(); - - Ok(Options::from_list(&meta_list)?) - } - } + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.is_empty() { + Ok(Options::default()) + } else { + let meta_list = Punctuated::::parse_terminated(input)? + .into_iter() + .collect::>(); + + Ok(Options::from_list(&meta_list)?) + } + } } enum ModuleItem { - Other(syn::Item), - Function(proc_macro2::TokenStream), + Other(syn::Item), + Function(proc_macro2::TokenStream), } struct FunctionAttrs { - cfg_attrs: Vec, - docs: Vec, - options: Options, + cfg_attrs: Vec, + docs: Vec, + options: Options, } impl FunctionAttrs { - fn from_attrs(attrs: Vec) -> syn::Result { - let (cfg_attrs, others): (Vec<_>, Vec<_>) = attrs - .into_iter() - .partition(|attr| attr.path().is_ident("cfg")); - - let (doc_attrs, others): (Vec<_>, Vec<_>) = others - .into_iter() - .partition(|attr| attr.path().is_ident("doc")); - - Ok(FunctionAttrs { - cfg_attrs, - docs: doc_attrs - .into_iter() - .map(|attr| match attr.meta { - syn::Meta::NameValue(syn::MetaNameValue { - value: - syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }), - .. - }) => Ok(lit), - _ => Err(syn::Error::new_spanned(attr, "expected string literal")), - }) - .collect::>()?, - options: Options::from_attrs(&others)?, - }) - } + fn from_attrs(attrs: Vec) -> syn::Result { + let (cfg_attrs, others): (Vec<_>, Vec<_>) = attrs.into_iter().partition(|attr| attr.path().is_ident("cfg")); + + let (doc_attrs, others): (Vec<_>, Vec<_>) = others.into_iter().partition(|attr| attr.path().is_ident("doc")); + + Ok(FunctionAttrs { + cfg_attrs, + docs: doc_attrs + .into_iter() + .map(|attr| match attr.meta { + syn::Meta::NameValue(syn::MetaNameValue { + value: syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }), + .. + }) => Ok(lit), + _ => Err(syn::Error::new_spanned(attr, "expected string literal")), + }) + .collect::>()?, + options: Options::from_attrs(&others)?, + }) + } } pub struct Function { - vis: syn::Visibility, - fn_token: Token![fn], - ident: syn::Ident, - args: syn::punctuated::Punctuated, - arrow_token: Token![->], - ret: syn::Type, - attrs: FunctionAttrs, + vis: syn::Visibility, + fn_token: Token![fn], + ident: syn::Ident, + args: syn::punctuated::Punctuated, + arrow_token: Token![->], + ret: syn::Type, + attrs: FunctionAttrs, } impl Parse for Function { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let attrs = input.call(syn::Attribute::parse_outer)?; - let vis = input.parse()?; - let fn_token = input.parse()?; - let ident = input.parse()?; - let args_content; - let _paren = syn::parenthesized!(args_content in input); - let args = args_content.parse_terminated(FnArg::parse, Token![,])?; - let arrow_token = input.parse()?; - let ret = input.parse()?; - input.parse::()?; - - Ok(Function { - vis, - fn_token, - ident, - args, - arrow_token, - ret, - attrs: FunctionAttrs::from_attrs(attrs)?, - }) - } + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + let vis = input.parse()?; + let fn_token = input.parse()?; + let ident = input.parse()?; + let args_content; + let _paren = syn::parenthesized!(args_content in input); + let args = args_content.parse_terminated(FnArg::parse, Token![,])?; + let arrow_token = input.parse()?; + let ret = input.parse()?; + input.parse::()?; + + Ok(Function { + vis, + fn_token, + ident, + args, + arrow_token, + ret, + attrs: FunctionAttrs::from_attrs(attrs)?, + }) + } } struct FnArg { - cfg_attrs: Vec, - other_attrs: Vec, - ident: syn::Ident, - colon_token: Token![:], - ty: syn::Type, - struct_ty: StructTy, + cfg_attrs: Vec, + other_attrs: Vec, + ident: syn::Ident, + colon_token: Token![:], + ty: syn::Type, + struct_ty: StructTy, } enum StructTy { - Clone(syn::Type), - Into(syn::Type), - Raw(syn::Type), - Str(syn::Type), + Clone(syn::Type), + Into(syn::Type), + Raw(syn::Type), + Str(syn::Type), } impl StructTy { - fn ty(&self) -> &syn::Type { - match self { - StructTy::Clone(ty) => ty, - StructTy::Into(ty) => ty, - StructTy::Raw(ty) => ty, - StructTy::Str(ty) => ty, - } - } + fn ty(&self) -> &syn::Type { + match self { + StructTy::Clone(ty) => ty, + StructTy::Into(ty) => ty, + StructTy::Raw(ty) => ty, + StructTy::Str(ty) => ty, + } + } } fn type_to_struct_type(ty: syn::Type) -> syn::Result { - match ty.clone() { - syn::Type::Reference(syn::TypeReference { elem, lifetime, .. }) => { - if lifetime.map_or(false, |lifetime| lifetime.ident == "static") { - return Ok(StructTy::Raw(ty)); - } - - if let syn::Type::Path(syn::TypePath { path, .. }) = &*elem { - if path.is_ident("str") { - return Ok(StructTy::Str( - syn::parse_quote_spanned! { ty.span() => ::std::sync::Arc<#path> }, - )); - } - } - - Ok(StructTy::Clone(*elem)) - } - // Also support impl types - syn::Type::ImplTrait(impl_trait) => impl_trait - .bounds - .iter() - .find_map(|bound| match bound { - syn::TypeParamBound::Trait(syn::TraitBound { - path: syn::Path { segments, .. }, - .. - }) => { - let first_segment = segments.first()?; - if first_segment.ident != "Into" { - return None; - } - - let args = match first_segment.arguments { - syn::PathArguments::AngleBracketed(ref args) => args.args.clone(), - _ => return None, - }; - - if args.len() != 1 { - return None; - } - - match &args[0] { - syn::GenericArgument::Type(ty) => Some(StructTy::Into(ty.clone())), - _ => None, - } - } - _ => None, - }) - .ok_or_else(|| { - syn::Error::new_spanned(impl_trait, "only impl Into trait bounds are supported") - }), - _ => Ok(StructTy::Raw(ty)), - } + match ty.clone() { + syn::Type::Reference(syn::TypeReference { elem, lifetime, .. }) => { + if lifetime.map_or(false, |lifetime| lifetime.ident == "static") { + return Ok(StructTy::Raw(ty)); + } + + if let syn::Type::Path(syn::TypePath { path, .. }) = &*elem { + if path.is_ident("str") { + return Ok(StructTy::Str( + syn::parse_quote_spanned! { ty.span() => ::std::sync::Arc<#path> }, + )); + } + } + + Ok(StructTy::Clone(*elem)) + } + // Also support impl types + syn::Type::ImplTrait(impl_trait) => impl_trait + .bounds + .iter() + .find_map(|bound| match bound { + syn::TypeParamBound::Trait(syn::TraitBound { + path: syn::Path { segments, .. }, + .. + }) => { + let first_segment = segments.first()?; + if first_segment.ident != "Into" { + return None; + } + + let args = match first_segment.arguments { + syn::PathArguments::AngleBracketed(ref args) => args.args.clone(), + _ => return None, + }; + + if args.len() != 1 { + return None; + } + + match &args[0] { + syn::GenericArgument::Type(ty) => Some(StructTy::Into(ty.clone())), + _ => None, + } + } + _ => None, + }) + .ok_or_else(|| syn::Error::new_spanned(impl_trait, "only impl Into trait bounds are supported")), + _ => Ok(StructTy::Raw(ty)), + } } impl Parse for FnArg { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let attrs = input.call(syn::Attribute::parse_outer)?; - let ident = input.parse()?; - let colon_token = input.parse()?; - let ty: syn::Type = input.parse()?; - let struct_ty = type_to_struct_type(ty.clone())?; - - let (cfg_attrs, other_attrs): (Vec<_>, Vec<_>) = attrs - .into_iter() - .partition(|attr| attr.path().is_ident("cfg")); - - Ok(FnArg { - ident, - cfg_attrs, - other_attrs, - colon_token, - ty, - struct_ty, - }) - } + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + let ident = input.parse()?; + let colon_token = input.parse()?; + let ty: syn::Type = input.parse()?; + let struct_ty = type_to_struct_type(ty.clone())?; + + let (cfg_attrs, other_attrs): (Vec<_>, Vec<_>) = attrs.into_iter().partition(|attr| attr.path().is_ident("cfg")); + + Ok(FnArg { + ident, + cfg_attrs, + other_attrs, + colon_token, + ty, + struct_ty, + }) + } } impl ToTokens for FnArg { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - for attr in &self.cfg_attrs { - attr.to_tokens(tokens); - } - - self.ident.to_tokens(tokens); - self.colon_token.to_tokens(tokens); - self.ty.to_tokens(tokens); - } + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + for attr in &self.cfg_attrs { + attr.to_tokens(tokens); + } + + self.ident.to_tokens(tokens); + self.colon_token.to_tokens(tokens); + self.ty.to_tokens(tokens); + } } fn metric_function( - input: proc_macro2::TokenStream, - module_name: Option<&syn::Ident>, - options: &Options, + input: proc_macro2::TokenStream, + module_name: Option<&syn::Ident>, + options: &Options, ) -> syn::Result { - let item = syn::parse2::(input)?; - - let crate_path = &options - .crate_path - .clone() - .unwrap_or_else(|| syn::parse_quote!(scuffle_foundations)); - - let ident = &item.ident; - let vis = &item.vis; - let ret = &item.ret; - - let const_assert_ret = quote::quote_spanned! { ret.span() => - __assert_impl_collector::<#ret>(); - }; - - let const_assert_args = item.args.iter().map(|arg| { - let ty = arg.struct_ty.ty(); - quote::quote_spanned! { ty.span() => - __assert_arg_static::<#ty>(); - } - }); - - let attrs = &item.attrs.cfg_attrs; - let docs = &item.attrs.docs; - let fn_token = &item.fn_token; - let arrow_token = &item.arrow_token; - let args = &item.args; - - let struct_fields = args - .iter() - .map(|arg| { - let ident = &arg.ident; - let ty = &arg.struct_ty.ty(); - let attrs = &arg.other_attrs; - - Ok(quote::quote! { - #(#attrs)* - #ident: #ty - }) - }) - .collect::>>()?; - - let has_args = !struct_fields.is_empty(); - - let build_args = args - .iter() - .map(|arg| { - let ident = &arg.ident; - let arg = match &arg.struct_ty { - StructTy::Clone(_) => quote::quote! { - ::core::clone::Clone::clone(#ident) - }, - StructTy::Into(_) => quote::quote! { - ::core::convert::Into::into(#ident) - }, - StructTy::Raw(_) => quote::quote! { - #ident - }, - StructTy::Str(_) => quote::quote! { - ::std::sync::Arc::from(#ident) - }, - }; - - Ok(quote::quote! { - #ident: #arg - }) - }) - .collect::>>()?; - - let metric_ident = if has_args { - quote::quote! { - #crate_path::telementry::metrics::serde::Family<__Args, #ret> - } - } else { - quote::quote! { - #ret - } - }; - - let make_metric = { - let make_metric = if let Some(builder) = item - .attrs - .options - .builder - .as_ref() - .or(options.builder.as_ref()) - { - let constructor = quote::quote! { - { - (|| { - #crate_path::telementry::metrics::MetricBuilder::build(&#builder) - }) as fn() -> #ret - } - }; - - if has_args { - quote::quote! { - #crate_path::telementry::metrics::serde::Family::new_with_constructor(#constructor) - } - } else { - quote::quote! { - #constructor() - } - } - } else { - if has_args { - quote::quote! { - #crate_path::telementry::metrics::serde::Family::default() - } - } else { - quote::quote! { - Default::default() - } - } - }; - - let registry = if item.attrs.options.optional || options.optional { - quote::quote! { - #crate_path::telementry::metrics::registries::Registries::get_optional_sub_registry(stringify!(#module_name)) - } - } else { - quote::quote! { - #crate_path::telementry::metrics::registries::Registries::get_main_sub_registry(stringify!(#module_name)) - } - }; - - let help = if docs.is_empty() { - quote::quote! { - "No documentation provided" - } - } else { - quote::quote! { - ::core::primitive::str::trim_end_matches(&[ - #(::core::primitive::str::trim(#docs)),* - ].join(" "), ".") - } - }; - - quote::quote! { - let metric = #make_metric; - - #registry.register( - stringify!(#ident), - #help, - ::std::clone::Clone::clone(&metric), - ); - - return metric; - } - }; - - let serde_path_str = format!( - "{}", - quote::quote! { - #crate_path::macro_reexports::serde - } - ); - - let args_struct = if has_args { - quote::quote! { - #[derive(::std::fmt::Debug, ::std::clone::Clone, PartialEq, Eq, Hash, #crate_path::macro_reexports::serde::Serialize)] - #[serde(crate = #serde_path_str)] - struct __Args { - #(#struct_fields,)* - } - } - } else { - quote::quote! {} - }; - - let assert_collector_fn = quote::quote! { - const fn __assert_impl_collector() {} - }; - let assert_arg_fn = if has_args { - quote::quote! { - const fn __assert_arg_static() {} - } - } else { - quote::quote! {} - }; - - let get_metric = if has_args { - quote::quote! { - ::std::clone::Clone::clone(&__METRIC.get_or_create(&__Args { - #(#build_args,)* - })) - } - } else { - quote::quote! { - ::std::clone::Clone::clone(&__METRIC) - } - }; - - let fn_body = quote::quote! { - #args_struct - #assert_collector_fn - #assert_arg_fn - #const_assert_ret - #(#const_assert_args)* - - static __METRIC: #crate_path::macro_reexports::once_cell::sync::Lazy<#metric_ident> = #crate_path::macro_reexports::once_cell::sync::Lazy::new(|| { - #make_metric; - }); - - #get_metric - }; - - Ok(quote::quote! { - #(#attrs)* - #(#[doc = #docs])* - #[must_use] - #vis #fn_token #ident(#args) #arrow_token #ret { - #fn_body - } - }) + let item = syn::parse2::(input)?; + + let crate_path = &options + .crate_path + .clone() + .unwrap_or_else(|| syn::parse_quote!(scuffle_foundations)); + + let ident = &item.ident; + let vis = &item.vis; + let ret = &item.ret; + + let const_assert_ret = quote::quote_spanned! { ret.span() => + __assert_impl_collector::<#ret>(); + }; + + let const_assert_args = item.args.iter().map(|arg| { + let ty = arg.struct_ty.ty(); + quote::quote_spanned! { ty.span() => + __assert_arg_static::<#ty>(); + } + }); + + let attrs = &item.attrs.cfg_attrs; + let docs = &item.attrs.docs; + let fn_token = &item.fn_token; + let arrow_token = &item.arrow_token; + let args = &item.args; + + let struct_fields = args + .iter() + .map(|arg| { + let ident = &arg.ident; + let ty = &arg.struct_ty.ty(); + let attrs = &arg.other_attrs; + + Ok(quote::quote! { + #(#attrs)* + #ident: #ty + }) + }) + .collect::>>()?; + + let has_args = !struct_fields.is_empty(); + + let build_args = args + .iter() + .map(|arg| { + let ident = &arg.ident; + let arg = match &arg.struct_ty { + StructTy::Clone(_) => quote::quote! { + ::core::clone::Clone::clone(#ident) + }, + StructTy::Into(_) => quote::quote! { + ::core::convert::Into::into(#ident) + }, + StructTy::Raw(_) => quote::quote! { + #ident + }, + StructTy::Str(_) => quote::quote! { + ::std::sync::Arc::from(#ident) + }, + }; + + Ok(quote::quote! { + #ident: #arg + }) + }) + .collect::>>()?; + + let metric_ident = if has_args { + quote::quote! { + #crate_path::telementry::metrics::serde::Family<__Args, #ret> + } + } else { + quote::quote! { + #ret + } + }; + + let make_metric = { + let make_metric = if let Some(builder) = item.attrs.options.builder.as_ref().or(options.builder.as_ref()) { + let constructor = quote::quote! { + { + (|| { + #crate_path::telementry::metrics::MetricBuilder::build(&#builder) + }) as fn() -> #ret + } + }; + + if has_args { + quote::quote! { + #crate_path::telementry::metrics::serde::Family::new_with_constructor(#constructor) + } + } else { + quote::quote! { + #constructor() + } + } + } else { + if has_args { + quote::quote! { + #crate_path::telementry::metrics::serde::Family::default() + } + } else { + quote::quote! { + Default::default() + } + } + }; + + let registry = if item.attrs.options.optional || options.optional { + quote::quote! { + #crate_path::telementry::metrics::registries::Registries::get_optional_sub_registry(stringify!(#module_name)) + } + } else { + quote::quote! { + #crate_path::telementry::metrics::registries::Registries::get_main_sub_registry(stringify!(#module_name)) + } + }; + + let help = if docs.is_empty() { + quote::quote! { + "No documentation provided" + } + } else { + quote::quote! { + ::core::primitive::str::trim_end_matches(&[ + #(::core::primitive::str::trim(#docs)),* + ].join(" "), ".") + } + }; + + quote::quote! { + let metric = #make_metric; + + #registry.register( + stringify!(#ident), + #help, + ::std::clone::Clone::clone(&metric), + ); + + return metric; + } + }; + + let serde_path_str = format!( + "{}", + quote::quote! { + #crate_path::macro_reexports::serde + } + ); + + let args_struct = if has_args { + quote::quote! { + #[derive(::std::fmt::Debug, ::std::clone::Clone, PartialEq, Eq, Hash, #crate_path::macro_reexports::serde::Serialize)] + #[serde(crate = #serde_path_str)] + struct __Args { + #(#struct_fields,)* + } + } + } else { + quote::quote! {} + }; + + let assert_collector_fn = quote::quote! { + const fn __assert_impl_collector() {} + }; + let assert_arg_fn = if has_args { + quote::quote! { + const fn __assert_arg_static() {} + } + } else { + quote::quote! {} + }; + + let get_metric = if has_args { + quote::quote! { + ::std::clone::Clone::clone(&__METRIC.get_or_create(&__Args { + #(#build_args,)* + })) + } + } else { + quote::quote! { + ::std::clone::Clone::clone(&__METRIC) + } + }; + + let fn_body = quote::quote! { + #args_struct + #assert_collector_fn + #assert_arg_fn + #const_assert_ret + #(#const_assert_args)* + + static __METRIC: #crate_path::macro_reexports::once_cell::sync::Lazy<#metric_ident> = #crate_path::macro_reexports::once_cell::sync::Lazy::new(|| { + #make_metric; + }); + + #get_metric + }; + + Ok(quote::quote! { + #(#attrs)* + #(#[doc = #docs])* + #[must_use] + #vis #fn_token #ident(#args) #arrow_token #ret { + #fn_body + } + }) } -pub fn metrics( - args: proc_macro::TokenStream, - input: proc_macro::TokenStream, -) -> syn::Result { - let args = syn::parse::(args)?; - - let module = match syn::parse::(input)? { - syn::Item::Mod(module) => module, - syn::Item::Verbatim(tokens) => return metric_function(tokens, None, &args), - item => { - return Err(syn::Error::new_spanned( - item, - "expected module or bare function", - )) - } - }; - - if args.builder.is_some() { - return Err(syn::Error::new_spanned( - args.builder.as_ref().unwrap(), - "builder attribute is only allowed on functions", - )); - } - - let module_name = &module.ident; - let vis = &module.vis; - - let items = module - .content - .into_iter() - .flat_map(|(_, item)| item) - .map(|item| match item { - syn::Item::Verbatim(verbatim) => { - metric_function(verbatim, Some(module_name), &args).map(ModuleItem::Function) - } - item => Ok(ModuleItem::Other(item)), - }) - .collect::>>()?; - - let items = items.into_iter().map(|item| match item { - ModuleItem::Other(item) => item, - ModuleItem::Function(item) => syn::Item::Verbatim(item), - }); - - Ok(quote::quote! { - #vis mod #module_name { - #(#items)* - } - }) +pub fn metrics(args: proc_macro::TokenStream, input: proc_macro::TokenStream) -> syn::Result { + let args = syn::parse::(args)?; + + let module = match syn::parse::(input)? { + syn::Item::Mod(module) => module, + syn::Item::Verbatim(tokens) => return metric_function(tokens, None, &args), + item => return Err(syn::Error::new_spanned(item, "expected module or bare function")), + }; + + if args.builder.is_some() { + return Err(syn::Error::new_spanned( + args.builder.as_ref().unwrap(), + "builder attribute is only allowed on functions", + )); + } + + let module_name = &module.ident; + let vis = &module.vis; + + let items = module + .content + .into_iter() + .flat_map(|(_, item)| item) + .map(|item| match item { + syn::Item::Verbatim(verbatim) => metric_function(verbatim, Some(module_name), &args).map(ModuleItem::Function), + item => Ok(ModuleItem::Other(item)), + }) + .collect::>>()?; + + let items = items.into_iter().map(|item| match item { + ModuleItem::Other(item) => item, + ModuleItem::Function(item) => syn::Item::Verbatim(item), + }); + + Ok(quote::quote! { + #vis mod #module_name { + #(#items)* + } + }) } diff --git a/foundations/macros/src/settings/mod.rs b/foundations/macros/src/settings/mod.rs index 481115ee..cff3ec90 100644 --- a/foundations/macros/src/settings/mod.rs +++ b/foundations/macros/src/settings/mod.rs @@ -1,6 +1,9 @@ -use darling::{ast::NestedMeta, FromMeta}; +use darling::ast::NestedMeta; +use darling::FromMeta; use proc_macro::TokenStream; -use syn::{parse::Parse, punctuated::Punctuated, Token}; +use syn::parse::Parse; +use syn::punctuated::Punctuated; +use syn::Token; use self::types::EnumOrStruct; @@ -8,117 +11,117 @@ mod types; #[derive(darling::FromMeta)] struct Options { - #[darling(default = "Options::default_crate_path")] - crate_path: syn::Path, - #[darling(default = "Options::default_impl_default")] - impl_default: bool, - #[darling(default = "Options::default_impl_debug")] - impl_debug: bool, - #[darling(default = "Options::default_impl_clone")] - impl_clone: bool, + #[darling(default = "Options::default_crate_path")] + crate_path: syn::Path, + #[darling(default = "Options::default_impl_default")] + impl_default: bool, + #[darling(default = "Options::default_impl_debug")] + impl_debug: bool, + #[darling(default = "Options::default_impl_clone")] + impl_clone: bool, } impl Default for Options { - fn default() -> Self { - Options { - crate_path: Options::default_crate_path(), - impl_default: Options::default_impl_default(), - impl_debug: Options::default_impl_debug(), - impl_clone: Options::default_impl_clone(), - } - } + fn default() -> Self { + Options { + crate_path: Options::default_crate_path(), + impl_default: Options::default_impl_default(), + impl_debug: Options::default_impl_debug(), + impl_clone: Options::default_impl_clone(), + } + } } impl Options { - fn default_crate_path() -> syn::Path { - syn::parse_quote!(scuffle_foundations) - } + fn default_crate_path() -> syn::Path { + syn::parse_quote!(scuffle_foundations) + } - fn default_impl_default() -> bool { - true - } + fn default_impl_default() -> bool { + true + } - fn default_impl_debug() -> bool { - true - } + fn default_impl_debug() -> bool { + true + } - fn default_impl_clone() -> bool { - true - } + fn default_impl_clone() -> bool { + true + } } impl Parse for Options { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - if input.is_empty() { - Ok(Options::default()) - } else { - let meta_list = Punctuated::::parse_terminated(input)? - .into_iter() - .collect::>(); - Ok(Options::from_list(&meta_list)?) - } - } + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.is_empty() { + Ok(Options::default()) + } else { + let meta_list = Punctuated::::parse_terminated(input)? + .into_iter() + .collect::>(); + Ok(Options::from_list(&meta_list)?) + } + } } pub fn settings(args: TokenStream, input: TokenStream) -> syn::Result { - let args = syn::parse::(args)?; - let input = syn::parse::(input)?; - - let mut derives = Vec::new(); - - if args.impl_clone { - derives.push(quote::quote!(Clone)); - } - - if args.impl_debug { - derives.push(quote::quote!(Debug)); - } - - let crate_path = args.crate_path; - - derives.push(quote::quote!(#crate_path::macro_reexports::serde::Deserialize)); - derives.push(quote::quote!(#crate_path::macro_reexports::serde::Serialize)); - derives.push(quote::quote!(#crate_path::settings::Settings)); - - let attr = { - let impl_default = args.impl_default; - let crate_path = quote::quote!(#crate_path).to_string(); - quote::quote! { - #[settings(impl_default = #impl_default, crate_path = #crate_path)] - } - }; - - let serde_path = format!( - "{}", - quote::quote! { - #crate_path::macro_reexports::serde - } - ); - - Ok(quote::quote! { - #[derive(#(#derives),*)] - #[serde(crate = #serde_path)] - #attr - #input - }) + let args = syn::parse::(args)?; + let input = syn::parse::(input)?; + + let mut derives = Vec::new(); + + if args.impl_clone { + derives.push(quote::quote!(Clone)); + } + + if args.impl_debug { + derives.push(quote::quote!(Debug)); + } + + let crate_path = args.crate_path; + + derives.push(quote::quote!(#crate_path::macro_reexports::serde::Deserialize)); + derives.push(quote::quote!(#crate_path::macro_reexports::serde::Serialize)); + derives.push(quote::quote!(#crate_path::settings::Settings)); + + let attr = { + let impl_default = args.impl_default; + let crate_path = quote::quote!(#crate_path).to_string(); + quote::quote! { + #[settings(impl_default = #impl_default, crate_path = #crate_path)] + } + }; + + let serde_path = format!( + "{}", + quote::quote! { + #crate_path::macro_reexports::serde + } + ); + + Ok(quote::quote! { + #[derive(#(#derives),*)] + #[serde(crate = #serde_path)] + #attr + #input + }) } pub fn derive_settings(input: TokenStream) -> syn::Result { - let input = syn::parse::(input)?; + let input = syn::parse::(input)?; - let item = EnumOrStruct::new(input)?; + let item = EnumOrStruct::new(input)?; - let args = item.args(); + let args = item.args(); - let docs = item.docs_impl(&args.crate_path)?; - let default = if args.impl_default { - item.default_impl()? - } else { - quote::quote! {} - }; + let docs = item.docs_impl(&args.crate_path)?; + let default = if args.impl_default { + item.default_impl()? + } else { + quote::quote! {} + }; - Ok(quote::quote! { - #docs - #default - }) + Ok(quote::quote! { + #docs + #default + }) } diff --git a/foundations/macros/src/settings/types/enum_ty.rs b/foundations/macros/src/settings/types/enum_ty.rs index 5e85bf69..9f5e6140 100644 --- a/foundations/macros/src/settings/types/enum_ty.rs +++ b/foundations/macros/src/settings/types/enum_ty.rs @@ -1,221 +1,210 @@ -use syn::{punctuated::Punctuated, ItemEnum, LitStr, Meta}; +use syn::punctuated::Punctuated; +use syn::{ItemEnum, LitStr, Meta}; +use super::serde::RenameAll; +use super::variant_ty::Variant; +use super::{Args, GlobalArgs}; use crate::helpers::parse_docs; -use super::{serde::RenameAll, variant_ty::Variant, Args, GlobalArgs}; - #[derive(Debug, Clone)] pub enum EnumTagged { - NotSpecified, - Untagged, - Tagged { - tag: String, - content: Option, - }, + NotSpecified, + Untagged, + Tagged { tag: String, content: Option }, } impl EnumTagged { - /// #[serde(tag = "tag")] or #[serde(untagged)] or #[serde(tag = "tag", content = "content")] - fn parse(attr: &[syn::Attribute]) -> Self { - let mut tagged = Self::NotSpecified; - - attr.iter() - .filter(|attr| attr.path().is_ident("serde")) - .filter_map(|attr| match &attr.meta { - Meta::List(meta) => meta - .parse_args_with(Punctuated::::parse_terminated) - .ok(), - _ => None, - }) - .flatten() - .for_each(|meta| match meta { - Meta::NameValue(meta) if meta.path.is_ident("tag") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - match &mut tagged { - Self::Tagged { tag, .. } => *tag = lit.value(), - _ => { - tagged = Self::Tagged { - tag: lit.value(), - content: None, - } - } - } - } - } - Meta::NameValue(meta) if meta.path.is_ident("content") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - match &mut tagged { - Self::Tagged { tag, .. } => *tag = lit.value(), - _ => { - tagged = Self::Tagged { - tag: String::new(), - content: Some(lit.value()), - } - } - } - } - } - Meta::Path(path) if path.is_ident("untagged") => { - tagged = Self::Untagged; - } - _ => {} - }); - - tagged - } - - fn tag(&self) -> Option<&str> { - match self { - Self::Tagged { tag, .. } => Some(tag), - _ => None, - } - } - - fn content(&self) -> Option<&str> { - match self { - Self::Tagged { content, .. } => content.as_deref(), - _ => None, - } - } + /// #[serde(tag = "tag")] or #[serde(untagged)] or #[serde(tag = "tag", content = "content")] + fn parse(attr: &[syn::Attribute]) -> Self { + let mut tagged = Self::NotSpecified; + + attr.iter() + .filter(|attr| attr.path().is_ident("serde")) + .filter_map(|attr| match &attr.meta { + Meta::List(meta) => meta + .parse_args_with(Punctuated::::parse_terminated) + .ok(), + _ => None, + }) + .flatten() + .for_each(|meta| match meta { + Meta::NameValue(meta) if meta.path.is_ident("tag") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + match &mut tagged { + Self::Tagged { tag, .. } => *tag = lit.value(), + _ => { + tagged = Self::Tagged { + tag: lit.value(), + content: None, + } + } + } + } + } + Meta::NameValue(meta) if meta.path.is_ident("content") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + match &mut tagged { + Self::Tagged { tag, .. } => *tag = lit.value(), + _ => { + tagged = Self::Tagged { + tag: String::new(), + content: Some(lit.value()), + } + } + } + } + } + Meta::Path(path) if path.is_ident("untagged") => { + tagged = Self::Untagged; + } + _ => {} + }); + + tagged + } + + fn tag(&self) -> Option<&str> { + match self { + Self::Tagged { tag, .. } => Some(tag), + _ => None, + } + } + + fn content(&self) -> Option<&str> { + match self { + Self::Tagged { content, .. } => content.as_deref(), + _ => None, + } + } } #[derive(Debug, Clone)] pub struct Enum { - // pub name: Name, - pub tagged: EnumTagged, - pub variants: Vec, - pub docs: Vec, - pub args: EnumArgs, - pub item: ItemEnum, + // pub name: Name, + pub tagged: EnumTagged, + pub variants: Vec, + pub docs: Vec, + pub args: EnumArgs, + pub item: ItemEnum, } impl Enum { - pub fn new(item: ItemEnum) -> syn::Result { - let variant_rename = RenameAll::parse(&item.attrs)?; - - let tagged = EnumTagged::parse(&item.attrs); - - Ok(Self { - // name: Name::parse(&item.attrs, item.ident.to_string().as_str(), None)?, - docs: parse_docs(&item.attrs), - variants: item - .variants - .iter() - .cloned() - .map(|variant| Variant::new(variant, variant_rename)) - .collect::>()?, - tagged, - args: EnumArgs::parse(&item.attrs)?, - item, - }) - } - - pub fn default_impl(&self) -> syn::Result { - let ident = &self.item.ident; - let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); - - let default_variants = self - .variants - .iter() - .filter(|item| item.args.default) - .collect::>(); - - if default_variants.len() > 1 { - return Err(syn::Error::new_spanned( - &default_variants[1].item, - "only one variant can be marked as default", - )); - } - - let Some(default_variant) = default_variants.first() else { - return Err(syn::Error::new_spanned( - &self.item, - "no default variant specified", - )); - }; - - let default_impl = default_variant.default_impl()?; - - Ok(quote::quote! { - #[automatically_derived] - impl #impl_generics Default for #ident #ty_generics #where_clause { - #[inline] - fn default() -> Self { - #default_impl - } - } - }) - } - - pub fn docs_impl(&self, crate_path: &syn::Path) -> syn::Result { - let ident = &self.item.ident; - let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); - - let fields = self - .variants - .iter() - .map(|variant| { - let (match_token, variant_docs) = - variant.docs_impl(crate_path, self.tagged.tag(), self.tagged.content())?; - - // Part of a match block - Ok(quote::quote! { - #ident::#match_token => { - #variant_docs - } - }) - }) - .collect::>>()?; - - let self_docs = if !self.docs.is_empty() { - let docs = &self.docs; - quote::quote! { - { - let mut parent_key = parent_key.to_vec(); - if !docs.contains_key(&parent_key) { - docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); - } - } - } - } else { - quote::quote! {} - }; - - Ok(quote::quote! { - #[automatically_derived] - impl #impl_generics #crate_path::settings::Settings for #ident #ty_generics #where_clause { - fn add_docs( - &self, - parent_key: &[::std::borrow::Cow<'static, str>], - docs: &mut ::std::collections::HashMap<::std::vec::Vec<::std::borrow::Cow<'static, str>>, ::std::borrow::Cow<'static, [::std::borrow::Cow<'static, str>]>>, - ) { - #self_docs - match self { - #(#fields)* - } - } - } - }) - } + pub fn new(item: ItemEnum) -> syn::Result { + let variant_rename = RenameAll::parse(&item.attrs)?; + + let tagged = EnumTagged::parse(&item.attrs); + + Ok(Self { + // name: Name::parse(&item.attrs, item.ident.to_string().as_str(), None)?, + docs: parse_docs(&item.attrs), + variants: item + .variants + .iter() + .cloned() + .map(|variant| Variant::new(variant, variant_rename)) + .collect::>()?, + tagged, + args: EnumArgs::parse(&item.attrs)?, + item, + }) + } + + pub fn default_impl(&self) -> syn::Result { + let ident = &self.item.ident; + let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); + + let default_variants = self.variants.iter().filter(|item| item.args.default).collect::>(); + + if default_variants.len() > 1 { + return Err(syn::Error::new_spanned( + &default_variants[1].item, + "only one variant can be marked as default", + )); + } + + let Some(default_variant) = default_variants.first() else { + return Err(syn::Error::new_spanned(&self.item, "no default variant specified")); + }; + + let default_impl = default_variant.default_impl()?; + + Ok(quote::quote! { + #[automatically_derived] + impl #impl_generics Default for #ident #ty_generics #where_clause { + #[inline] + fn default() -> Self { + #default_impl + } + } + }) + } + + pub fn docs_impl(&self, crate_path: &syn::Path) -> syn::Result { + let ident = &self.item.ident; + let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); + + let fields = self + .variants + .iter() + .map(|variant| { + let (match_token, variant_docs) = variant.docs_impl(crate_path, self.tagged.tag(), self.tagged.content())?; + + // Part of a match block + Ok(quote::quote! { + #ident::#match_token => { + #variant_docs + } + }) + }) + .collect::>>()?; + + let self_docs = if !self.docs.is_empty() { + let docs = &self.docs; + quote::quote! { + { + let mut parent_key = parent_key.to_vec(); + if !docs.contains_key(&parent_key) { + docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); + } + } + } + } else { + quote::quote! {} + }; + + Ok(quote::quote! { + #[automatically_derived] + impl #impl_generics #crate_path::settings::Settings for #ident #ty_generics #where_clause { + fn add_docs( + &self, + parent_key: &[::std::borrow::Cow<'static, str>], + docs: &mut ::std::collections::HashMap<::std::vec::Vec<::std::borrow::Cow<'static, str>>, ::std::borrow::Cow<'static, [::std::borrow::Cow<'static, str>]>>, + ) { + #self_docs + match self { + #(#fields)* + } + } + } + }) + } } #[derive(Debug, Clone, Default)] pub struct EnumArgs { - pub global: GlobalArgs, + pub global: GlobalArgs, } impl Args for EnumArgs { - fn apply_meta(&mut self, meta: &Meta) -> syn::Result { - match meta { - meta => self.global.apply_meta(meta), - } - } + fn apply_meta(&mut self, meta: &Meta) -> syn::Result { + match meta { + meta => self.global.apply_meta(meta), + } + } } diff --git a/foundations/macros/src/settings/types/field_ty.rs b/foundations/macros/src/settings/types/field_ty.rs index 1b8e0498..d5273c48 100644 --- a/foundations/macros/src/settings/types/field_ty.rs +++ b/foundations/macros/src/settings/types/field_ty.rs @@ -1,75 +1,72 @@ use syn::{LitStr, Meta}; +use super::serde::{parse_default_fn, serde_flatten, Name, RenameAll}; +use super::Args; use crate::helpers::parse_docs; -use super::{ - serde::{parse_default_fn, serde_flatten, Name, RenameAll}, - Args, -}; - #[derive(Debug, Clone)] pub struct Field { - pub name: Option, - pub docs: Vec, - pub default_fn: Option, - pub args: FieldArgs, - pub item: syn::Field, - pub flatten: bool, + pub name: Option, + pub docs: Vec, + pub default_fn: Option, + pub args: FieldArgs, + pub item: syn::Field, + pub flatten: bool, } impl Field { - pub fn new(item: syn::Field, rename: Option) -> syn::Result { - Ok(Self { - name: item - .ident - .as_ref() - .map(|ident| Name::parse(&item.attrs, ident.to_string().as_str(), rename)) - .transpose()?, - docs: parse_docs(&item.attrs), - default_fn: parse_default_fn(&item.attrs)?, - args: FieldArgs::parse(&item.attrs)?, - flatten: serde_flatten(&item.attrs)?, - item, - }) - } + pub fn new(item: syn::Field, rename: Option) -> syn::Result { + Ok(Self { + name: item + .ident + .as_ref() + .map(|ident| Name::parse(&item.attrs, ident.to_string().as_str(), rename)) + .transpose()?, + docs: parse_docs(&item.attrs), + default_fn: parse_default_fn(&item.attrs)?, + args: FieldArgs::parse(&item.attrs)?, + flatten: serde_flatten(&item.attrs)?, + item, + }) + } - pub fn default_impl(&self) -> proc_macro2::TokenStream { - let default = self - .args - .default - .as_ref() - .map(|default| { - quote::quote! { #default } - }) - .unwrap_or_else(|| { - let func = self - .default_fn - .as_ref() - .map(|default_fn| quote::quote! { #default_fn }) - .unwrap_or_else(|| quote::quote! { Default::default }); + pub fn default_impl(&self) -> proc_macro2::TokenStream { + let default = self + .args + .default + .as_ref() + .map(|default| { + quote::quote! { #default } + }) + .unwrap_or_else(|| { + let func = self + .default_fn + .as_ref() + .map(|default_fn| quote::quote! { #default_fn }) + .unwrap_or_else(|| quote::quote! { Default::default }); - quote::quote! { #func() } - }); + quote::quote! { #func() } + }); - quote::quote! { - #default - } - } + quote::quote! { + #default + } + } } #[derive(Debug, Clone, Default)] pub struct FieldArgs { - default: Option, + default: Option, } impl Args for FieldArgs { - fn apply_meta(&mut self, meta: &Meta) -> syn::Result { - match meta { - Meta::NameValue(meta) if meta.path.is_ident("default") => { - self.default = Some(meta.value.clone()); - Ok(true) - } - _ => Ok(false), - } - } + fn apply_meta(&mut self, meta: &Meta) -> syn::Result { + match meta { + Meta::NameValue(meta) if meta.path.is_ident("default") => { + self.default = Some(meta.value.clone()); + Ok(true) + } + _ => Ok(false), + } + } } diff --git a/foundations/macros/src/settings/types/mod.rs b/foundations/macros/src/settings/types/mod.rs index d14c2b86..8db03813 100644 --- a/foundations/macros/src/settings/types/mod.rs +++ b/foundations/macros/src/settings/types/mod.rs @@ -1,6 +1,8 @@ -use syn::{punctuated::Punctuated, Meta}; +use syn::punctuated::Punctuated; +use syn::Meta; -use self::{enum_ty::Enum, struct_ty::Struct}; +use self::enum_ty::Enum; +use self::struct_ty::Struct; mod enum_ty; mod field_ty; @@ -10,114 +12,112 @@ mod variant_ty; #[derive(Debug, Clone)] pub enum EnumOrStruct { - Enum(Enum), - Struct(Struct), + Enum(Enum), + Struct(Struct), } impl EnumOrStruct { - pub fn new(item: syn::Item) -> syn::Result { - match item { - syn::Item::Enum(item) => Ok(Self::Enum(Enum::new(item)?)), - syn::Item::Struct(item) => Ok(Self::Struct(Struct::new(item)?)), - item => Err(syn::Error::new_spanned(item, "expected enum or struct")), - } - } + pub fn new(item: syn::Item) -> syn::Result { + match item { + syn::Item::Enum(item) => Ok(Self::Enum(Enum::new(item)?)), + syn::Item::Struct(item) => Ok(Self::Struct(Struct::new(item)?)), + item => Err(syn::Error::new_spanned(item, "expected enum or struct")), + } + } - pub fn args(&self) -> &GlobalArgs { - match self { - Self::Enum(item) => &item.args.global, - Self::Struct(item) => &item.args.global, - } - } + pub fn args(&self) -> &GlobalArgs { + match self { + Self::Enum(item) => &item.args.global, + Self::Struct(item) => &item.args.global, + } + } - pub fn default_impl(&self) -> syn::Result { - match self { - Self::Enum(item) => item.default_impl(), - Self::Struct(item) => item.default_impl(), - } - } + pub fn default_impl(&self) -> syn::Result { + match self { + Self::Enum(item) => item.default_impl(), + Self::Struct(item) => item.default_impl(), + } + } - pub fn docs_impl(&self, crate_path: &syn::Path) -> syn::Result { - match self { - Self::Enum(item) => item.docs_impl(crate_path), - Self::Struct(item) => item.docs_impl(crate_path), - } - } + pub fn docs_impl(&self, crate_path: &syn::Path) -> syn::Result { + match self { + Self::Enum(item) => item.docs_impl(crate_path), + Self::Struct(item) => item.docs_impl(crate_path), + } + } } trait Args: Default { - fn parse(attrs: &[syn::Attribute]) -> syn::Result - where - Self: Sized, - { - attrs - .into_iter() - .filter(|a| a.path().is_ident("settings")) - .try_fold(Self::default(), |mut state, attr| { - let Meta::List(meta) = &attr.meta else { - return Err(syn::Error::new_spanned(&attr, "expected #[settings(...)]")); - }; + fn parse(attrs: &[syn::Attribute]) -> syn::Result + where + Self: Sized, + { + attrs + .into_iter() + .filter(|a| a.path().is_ident("settings")) + .try_fold(Self::default(), |mut state, attr| { + let Meta::List(meta) = &attr.meta else { + return Err(syn::Error::new_spanned(&attr, "expected #[settings(...)]")); + }; - let parsed = - meta.parse_args_with(Punctuated::::parse_terminated)?; + let parsed = meta.parse_args_with(Punctuated::::parse_terminated)?; - for meta in parsed { - if !state.apply_meta(&meta)? { - return Err(syn::Error::new_spanned(meta, "unexpected setting")); - } - } + for meta in parsed { + if !state.apply_meta(&meta)? { + return Err(syn::Error::new_spanned(meta, "unexpected setting")); + } + } - Ok(state) - }) - } + Ok(state) + }) + } - fn apply_meta(&mut self, meta: &Meta) -> syn::Result; + fn apply_meta(&mut self, meta: &Meta) -> syn::Result; } #[derive(Debug, Clone)] pub struct GlobalArgs { - pub impl_default: bool, - pub crate_path: syn::Path, + pub impl_default: bool, + pub crate_path: syn::Path, } impl Default for GlobalArgs { - fn default() -> Self { - Self { - impl_default: true, - crate_path: syn::parse(quote::quote! { scuffle_foundations }.into()).unwrap(), - } - } + fn default() -> Self { + Self { + impl_default: true, + crate_path: syn::parse(quote::quote! { scuffle_foundations }.into()).unwrap(), + } + } } impl Args for GlobalArgs { - fn apply_meta(&mut self, meta: &Meta) -> syn::Result { - match meta { - Meta::NameValue(meta) if meta.path.is_ident("impl_default") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Bool(lit), - .. - }) = &meta.value - { - self.impl_default = lit.value(); - Ok(true) - } else { - Err(syn::Error::new_spanned(&meta.value, "expected boolean")) - } - } - Meta::NameValue(meta) if meta.path.is_ident("crate_path") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - self.crate_path = syn::parse_str(&lit.value()) - .map_err(|_| syn::Error::new_spanned(&lit, "expected valid path"))?; - Ok(true) - } else { - Err(syn::Error::new_spanned(&meta.value, "expected string")) - } - } - _ => Ok(false), - } - } + fn apply_meta(&mut self, meta: &Meta) -> syn::Result { + match meta { + Meta::NameValue(meta) if meta.path.is_ident("impl_default") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(lit), + .. + }) = &meta.value + { + self.impl_default = lit.value(); + Ok(true) + } else { + Err(syn::Error::new_spanned(&meta.value, "expected boolean")) + } + } + Meta::NameValue(meta) if meta.path.is_ident("crate_path") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + self.crate_path = + syn::parse_str(&lit.value()).map_err(|_| syn::Error::new_spanned(&lit, "expected valid path"))?; + Ok(true) + } else { + Err(syn::Error::new_spanned(&meta.value, "expected string")) + } + } + _ => Ok(false), + } + } } diff --git a/foundations/macros/src/settings/types/serde.rs b/foundations/macros/src/settings/types/serde.rs index 207116e8..21122733 100644 --- a/foundations/macros/src/settings/types/serde.rs +++ b/foundations/macros/src/settings/types/serde.rs @@ -1,193 +1,177 @@ use std::str::FromStr; use convert_case::{Case, Casing}; -use syn::{punctuated::Punctuated, Meta}; +use syn::punctuated::Punctuated; +use syn::Meta; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RenameAll { - LowerCase, - UpperCase, - PascalCase, - CamelCase, - SnakeCase, - ScreamingSnakeCase, - KebabCase, - ScreamingKebabCase, + LowerCase, + UpperCase, + PascalCase, + CamelCase, + SnakeCase, + ScreamingSnakeCase, + KebabCase, + ScreamingKebabCase, } impl FromStr for RenameAll { - type Err = (); + type Err = (); - fn from_str(s: &str) -> Result { - match s { - "lowercase" => Ok(Self::LowerCase), - "UPPERCASE" => Ok(Self::UpperCase), - "PascalCase" => Ok(Self::PascalCase), - "camelCase" => Ok(Self::CamelCase), - "snake_case" => Ok(Self::SnakeCase), - "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase), - "kebab-case" => Ok(Self::KebabCase), - "SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebabCase), - _ => Err(()), - } - } + fn from_str(s: &str) -> Result { + match s { + "lowercase" => Ok(Self::LowerCase), + "UPPERCASE" => Ok(Self::UpperCase), + "PascalCase" => Ok(Self::PascalCase), + "camelCase" => Ok(Self::CamelCase), + "snake_case" => Ok(Self::SnakeCase), + "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase), + "kebab-case" => Ok(Self::KebabCase), + "SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebabCase), + _ => Err(()), + } + } } impl RenameAll { - /// #[serde(rename_all = "name")] or #[serde(rename_all(serialize = "name", deserialize = "name"))] - pub fn parse(attr: &[syn::Attribute]) -> syn::Result> { - Ok(parse_serde_attrs(attr, None, |state, meta| match &meta { - Meta::NameValue(meta) if meta.path.is_ident("rename_all") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - *state = Some( - lit.value() - .parse() - .map_err(|_| syn::Error::new_spanned(lit, "invalid rename_all value")), - ); - } - } - _ => {} - })? - .transpose()?) - } + /// #[serde(rename_all = "name")] or #[serde(rename_all(serialize = "name", deserialize = "name"))] + pub fn parse(attr: &[syn::Attribute]) -> syn::Result> { + Ok(parse_serde_attrs(attr, None, |state, meta| match &meta { + Meta::NameValue(meta) if meta.path.is_ident("rename_all") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + *state = Some( + lit.value() + .parse() + .map_err(|_| syn::Error::new_spanned(lit, "invalid rename_all value")), + ); + } + } + _ => {} + })? + .transpose()?) + } - pub fn apply(&self, name: &str) -> String { - let case = match self { - Self::LowerCase => Case::Lower, - Self::UpperCase => Case::Upper, - Self::PascalCase => Case::Pascal, - Self::CamelCase => Case::Camel, - Self::SnakeCase => Case::Snake, - Self::ScreamingSnakeCase => Case::ScreamingSnake, - Self::KebabCase => Case::Kebab, - Self::ScreamingKebabCase => Case::UpperKebab, - }; + pub fn apply(&self, name: &str) -> String { + let case = match self { + Self::LowerCase => Case::Lower, + Self::UpperCase => Case::Upper, + Self::PascalCase => Case::Pascal, + Self::CamelCase => Case::Camel, + Self::SnakeCase => Case::Snake, + Self::ScreamingSnakeCase => Case::ScreamingSnake, + Self::KebabCase => Case::Kebab, + Self::ScreamingKebabCase => Case::UpperKebab, + }; - name.to_case(case) - } + name.to_case(case) + } } #[derive(Debug, Clone)] pub struct Name { - pub serialize: String, - // pub deserialize: String, + pub serialize: String, + // pub deserialize: String, } impl Name { - /// #[serde(rename = "name")] or #[serde(rename(serialize = "name", deserialize = "name"))] - pub fn parse( - attr: &[syn::Attribute], - name: &str, - rename: Option, - ) -> syn::Result { - parse_serde_attrs(attr, None, |state, meta| match &meta { - Meta::NameValue(meta) if meta.path.is_ident("rename") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - *state = Some(Name { - serialize: lit.value(), - // deserialize: lit.value(), - }) - } - } - Meta::List(meta) if meta.path.is_ident("rename") => { - let mut serialize = name.to_string(); - let mut deserialize = name.to_string(); + /// #[serde(rename = "name")] or #[serde(rename(serialize = "name", deserialize = "name"))] + pub fn parse(attr: &[syn::Attribute], name: &str, rename: Option) -> syn::Result { + parse_serde_attrs(attr, None, |state, meta| match &meta { + Meta::NameValue(meta) if meta.path.is_ident("rename") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + *state = Some(Name { + serialize: lit.value(), + // deserialize: lit.value(), + }) + } + } + Meta::List(meta) if meta.path.is_ident("rename") => { + let mut serialize = name.to_string(); + let mut deserialize = name.to_string(); - meta.parse_args_with(Punctuated::::parse_terminated) - .unwrap_or_default() - .iter() - .for_each(|nested| match nested { - Meta::NameValue(meta) if meta.path.is_ident("serialize") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - serialize = lit.value(); - } - } - Meta::NameValue(meta) if meta.path.is_ident("deserialize") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - deserialize = lit.value(); - } - } - _ => {} - }); + meta.parse_args_with(Punctuated::::parse_terminated) + .unwrap_or_default() + .iter() + .for_each(|nested| match nested { + Meta::NameValue(meta) if meta.path.is_ident("serialize") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + serialize = lit.value(); + } + } + Meta::NameValue(meta) if meta.path.is_ident("deserialize") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + deserialize = lit.value(); + } + } + _ => {} + }); - *state = Some(Self { - serialize, - // deserialize, - }) - } - _ => {} - }) - .transpose() - .unwrap_or_else(|| { - let name = rename - .map(|rename| rename.apply(name)) - .unwrap_or(name.to_string()); + *state = Some(Self { + serialize, + // deserialize, + }) + } + _ => {} + }) + .transpose() + .unwrap_or_else(|| { + let name = rename.map(|rename| rename.apply(name)).unwrap_or(name.to_string()); - Ok(Self { - serialize: name.clone(), - // deserialize: name, - }) - }) - } + Ok(Self { + serialize: name.clone(), + // deserialize: name, + }) + }) + } } /// #[serde(default = "default_fn")] pub fn parse_default_fn(attrs: &[syn::Attribute]) -> syn::Result> { - parse_serde_attrs(attrs, None, |state, meta| match meta { - Meta::NameValue(meta) if meta.path.is_ident("default") => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &meta.value - { - *state = Some(lit.parse::().unwrap()) - } - } - _ => {} - }) + parse_serde_attrs(attrs, None, |state, meta| match meta { + Meta::NameValue(meta) if meta.path.is_ident("default") => { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), .. + }) = &meta.value + { + *state = Some(lit.parse::().unwrap()) + } + } + _ => {} + }) } pub fn serde_flatten(attrs: &[syn::Attribute]) -> syn::Result { - parse_serde_attrs(attrs, false, |state, meta| match meta { - Meta::Path(meta) if meta.is_ident("flatten") => *state = true, - _ => {} - }) + parse_serde_attrs(attrs, false, |state, meta| match meta { + Meta::Path(meta) if meta.is_ident("flatten") => *state = true, + _ => {} + }) } -fn parse_serde_attrs( - attr: &[syn::Attribute], - state: T, - mut fold: impl FnMut(&mut T, Meta), -) -> syn::Result { - attr.iter() - .filter(|attr| attr.path().is_ident("serde")) - .filter_map(|attr| match &attr.meta { - Meta::List(meta) => { - Some(meta.parse_args_with(Punctuated::::parse_terminated)) - } - _ => None, - }) - .collect::>>()? - .into_iter() - .flatten() - .try_fold(state, |mut state, meta| { - fold(&mut state, meta); - Ok(state) - }) +fn parse_serde_attrs(attr: &[syn::Attribute], state: T, mut fold: impl FnMut(&mut T, Meta)) -> syn::Result { + attr.iter() + .filter(|attr| attr.path().is_ident("serde")) + .filter_map(|attr| match &attr.meta { + Meta::List(meta) => Some(meta.parse_args_with(Punctuated::::parse_terminated)), + _ => None, + }) + .collect::>>()? + .into_iter() + .flatten() + .try_fold(state, |mut state, meta| { + fold(&mut state, meta); + Ok(state) + }) } diff --git a/foundations/macros/src/settings/types/struct_ty.rs b/foundations/macros/src/settings/types/struct_ty.rs index 2c283ef0..a150edf4 100644 --- a/foundations/macros/src/settings/types/struct_ty.rs +++ b/foundations/macros/src/settings/types/struct_ty.rs @@ -1,179 +1,180 @@ use syn::{ItemStruct, LitStr, Meta}; +use super::field_ty::Field; +use super::serde::RenameAll; +use super::{Args, GlobalArgs}; use crate::helpers::parse_docs; -use super::{field_ty::Field, serde::RenameAll, Args, GlobalArgs}; - #[derive(Debug, Clone)] pub struct Struct { - // pub name: Name, - pub fields: Vec, - pub docs: Vec, - pub args: StructArgs, - pub item: syn::ItemStruct, + // pub name: Name, + pub fields: Vec, + pub docs: Vec, + pub args: StructArgs, + pub item: syn::ItemStruct, } impl Struct { - pub fn new(item: ItemStruct) -> syn::Result { - let field_rename = RenameAll::parse(&item.attrs)?; - - Ok(Self { - // name: Name::parse(&item.attrs, item.ident.to_string().as_str(), None)?, - fields: item - .fields - .iter() - .cloned() - .map(|field| Field::new(field, field_rename)) - .collect::>()?, - docs: parse_docs(&item.attrs), - args: StructArgs::parse(&item.attrs)?, - item, - }) - } - - pub fn default_impl(&self) -> syn::Result { - let ident = &self.item.ident; - let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); - - let fields = self.fields.iter().map(|field| { - let default = field.default_impl(); - match field.item.ident.as_ref() { - // named field - Some(ident) => { - quote::quote! { - #ident: #default, - } - } - // tuple struct - None => { - quote::quote! { #default } - } - } - }); - - let default_impl = match &self.item.fields { - syn::Fields::Named(_) => { - quote::quote! { - Self { - #(#fields)* - } - } - } - syn::Fields::Unnamed(_) => { - quote::quote! { - Self( - #(#fields,)* - ) - } - } - syn::Fields::Unit => { - quote::quote! { - Self - } - } - }; - - Ok(quote::quote! { - #[automatically_derived] - impl #impl_generics Default for #ident #ty_generics #where_clause { - fn default() -> Self { - #default_impl - } - } - }) - } - - pub fn docs_impl(&self, crate_path: &syn::Path) -> syn::Result { - let ident = &self.item.ident; - let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); - - let fields = self.fields.iter().map(|field| { - let Some(name) = field.name.as_ref() else { - return quote::quote! {}; - }; - - let docs = field - .docs - .iter() - .map(|doc| { - quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } - }) - .collect::>(); - - let insert = if docs.is_empty() || field.flatten { - quote::quote! {} - } else { - quote::quote! { - docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); - } - }; - - let name = &name.serialize; - let ident = field.item.ident.as_ref().unwrap(); - - let name_push = if field.flatten { - quote::quote! {} - } else { - quote::quote! { - parent_key.push(::std::borrow::Cow::Borrowed(#name)); - } - }; - - quote::quote! { - { - let mut parent_key = parent_key.to_vec(); - #name_push - (&&&&#crate_path::settings::Wrapped(&self.#ident)).add_docs(&parent_key, docs); - #insert - } - } - }); - - let struct_doc = if !self.docs.is_empty() { - let docs = self - .docs - .iter() - .map(|doc| { - quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } - }) - .collect::>(); - - quote::quote! { - { - let mut parent_key = parent_key.to_vec(); - parent_key.push(::std::borrow::Cow::Borrowed(">")); - docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); - } - } - } else { - quote::quote! {} - }; - - Ok(quote::quote! { - #[automatically_derived] - impl #impl_generics #crate_path::settings::Settings for #ident #ty_generics #where_clause { - fn add_docs( - &self, - parent_key: &[::std::borrow::Cow<'static, str>], - docs: &mut ::std::collections::HashMap<::std::vec::Vec<::std::borrow::Cow<'static, str>>, ::std::borrow::Cow<'static, [::std::borrow::Cow<'static, str>]>>, - ) { - #struct_doc - #(#fields)* - } - } - }) - } + pub fn new(item: ItemStruct) -> syn::Result { + let field_rename = RenameAll::parse(&item.attrs)?; + + Ok(Self { + // name: Name::parse(&item.attrs, item.ident.to_string().as_str(), None)?, + fields: item + .fields + .iter() + .cloned() + .map(|field| Field::new(field, field_rename)) + .collect::>()?, + docs: parse_docs(&item.attrs), + args: StructArgs::parse(&item.attrs)?, + item, + }) + } + + pub fn default_impl(&self) -> syn::Result { + let ident = &self.item.ident; + let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); + + let fields = self.fields.iter().map(|field| { + let default = field.default_impl(); + match field.item.ident.as_ref() { + // named field + Some(ident) => { + quote::quote! { + #ident: #default, + } + } + // tuple struct + None => { + quote::quote! { #default } + } + } + }); + + let default_impl = match &self.item.fields { + syn::Fields::Named(_) => { + quote::quote! { + Self { + #(#fields)* + } + } + } + syn::Fields::Unnamed(_) => { + quote::quote! { + Self( + #(#fields,)* + ) + } + } + syn::Fields::Unit => { + quote::quote! { + Self + } + } + }; + + Ok(quote::quote! { + #[automatically_derived] + impl #impl_generics Default for #ident #ty_generics #where_clause { + fn default() -> Self { + #default_impl + } + } + }) + } + + pub fn docs_impl(&self, crate_path: &syn::Path) -> syn::Result { + let ident = &self.item.ident; + let (impl_generics, ty_generics, where_clause) = self.item.generics.split_for_impl(); + + let fields = self.fields.iter().map(|field| { + let Some(name) = field.name.as_ref() else { + return quote::quote! {}; + }; + + let docs = field + .docs + .iter() + .map(|doc| { + quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } + }) + .collect::>(); + + let insert = if docs.is_empty() || field.flatten { + quote::quote! {} + } else { + quote::quote! { + docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); + } + }; + + let name = &name.serialize; + let ident = field.item.ident.as_ref().unwrap(); + + let name_push = if field.flatten { + quote::quote! {} + } else { + quote::quote! { + parent_key.push(::std::borrow::Cow::Borrowed(#name)); + } + }; + + quote::quote! { + { + let mut parent_key = parent_key.to_vec(); + #name_push + (&&&&#crate_path::settings::Wrapped(&self.#ident)).add_docs(&parent_key, docs); + #insert + } + } + }); + + let struct_doc = if !self.docs.is_empty() { + let docs = self + .docs + .iter() + .map(|doc| { + quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } + }) + .collect::>(); + + quote::quote! { + { + let mut parent_key = parent_key.to_vec(); + parent_key.push(::std::borrow::Cow::Borrowed(">")); + docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); + } + } + } else { + quote::quote! {} + }; + + Ok(quote::quote! { + #[automatically_derived] + impl #impl_generics #crate_path::settings::Settings for #ident #ty_generics #where_clause { + fn add_docs( + &self, + parent_key: &[::std::borrow::Cow<'static, str>], + docs: &mut ::std::collections::HashMap<::std::vec::Vec<::std::borrow::Cow<'static, str>>, ::std::borrow::Cow<'static, [::std::borrow::Cow<'static, str>]>>, + ) { + #struct_doc + #(#fields)* + } + } + }) + } } #[derive(Debug, Clone, Default)] pub struct StructArgs { - pub global: GlobalArgs, + pub global: GlobalArgs, } impl Args for StructArgs { - fn apply_meta(&mut self, meta: &Meta) -> syn::Result { - match meta { - meta => self.global.apply_meta(meta), - } - } + fn apply_meta(&mut self, meta: &Meta) -> syn::Result { + match meta { + meta => self.global.apply_meta(meta), + } + } } diff --git a/foundations/macros/src/settings/types/variant_ty.rs b/foundations/macros/src/settings/types/variant_ty.rs index 472020b9..e62c137e 100644 --- a/foundations/macros/src/settings/types/variant_ty.rs +++ b/foundations/macros/src/settings/types/variant_ty.rs @@ -1,221 +1,223 @@ -use syn::{spanned::Spanned, LitStr, Meta}; +use syn::spanned::Spanned; +use syn::{LitStr, Meta}; +use super::field_ty::Field; +use super::serde::RenameAll; +use super::Args; use crate::helpers::parse_docs; -use super::{field_ty::Field, serde::RenameAll, Args}; - #[derive(Debug, Clone)] pub struct Variant { - // pub name: Name, - pub fields: Vec, - pub docs: Vec, - pub args: VariantArgs, - pub item: syn::Variant, + // pub name: Name, + pub fields: Vec, + pub docs: Vec, + pub args: VariantArgs, + pub item: syn::Variant, } impl Variant { - pub fn new(item: syn::Variant, _rename: Option) -> syn::Result { - let field_rename = RenameAll::parse(&item.attrs)?; - - Ok(Self { - // name: Name::parse(&item.attrs, item.ident.to_string().as_str(), rename)?, - fields: match &item.fields { - syn::Fields::Named(fields) => fields - .named - .iter() - .cloned() - .map(|field| Field::new(field, field_rename)) - .collect::>()?, - syn::Fields::Unnamed(fields) => fields - .unnamed - .iter() - .cloned() - .map(|field| Field::new(field, field_rename)) - .collect::>()?, - syn::Fields::Unit => Vec::new(), - }, - docs: parse_docs(&item.attrs), - args: VariantArgs::parse(&item.attrs)?, - item, - }) - } - - pub fn default_impl(&self) -> syn::Result { - let ident = &self.item.ident; - let fields = self.fields.iter().map(|field| { - let default = field.default_impl(); - - match field.item.ident.as_ref() { - // named field - Some(ident) => { - quote::quote! { - #ident: #default, - } - } - // tuple struct - None => { - quote::quote! { #default } - } - } - }); - - match &self.item.fields { - syn::Fields::Named(_) => Ok(quote::quote! { - Self::#ident { - #(#fields)* - } - }), - syn::Fields::Unnamed(_) => Ok(quote::quote! { - Self::#ident( - #(#fields,)* - ) - }), - syn::Fields::Unit => Ok(quote::quote! { - Self::#ident - }), - } - } - - pub fn docs_impl( - &self, - crate_path: &syn::Path, - kind_tag: Option<&str>, - content_tag: Option<&str>, - ) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> { - let fields = self.fields.iter().enumerate().map(|(idx, field)| { - let docs = field - .docs - .iter() - .map(|doc| { - quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } - }) - .collect::>(); - - let insert = if docs.is_empty() || field.flatten { - quote::quote! {} - } else { - quote::quote! { - docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); - } - }; - - let name_push = if (self.fields.len() == 1 && field.name.is_none()) || field.flatten { - quote::quote! {} - } else { - let name = field - .name - .as_ref() - .map(|name| name.serialize.clone()) - .unwrap_or_else(|| format!("{idx}")); - - quote::quote! { - parent_key.push(::std::borrow::Cow::Borrowed(#name)); - } - }; - - let name_push = if let Some(content_tag) = content_tag { - quote::quote! { - parent_key.push(::std::borrow::Cow::Borrowed(#content_tag)); - #name_push - } - } else { - name_push - }; - - let ident = syn::Ident::new(&format!("__field{}", idx), self.item.span()); - - quote::quote! { - { - let mut parent_key = parent_key.to_vec(); - #name_push - (&&&&#crate_path::settings::Wrapped(#ident)).add_docs(&parent_key, docs); - #insert - } - } - }); - - let variant_doc = if !self.docs.is_empty() { - let docs = self - .docs - .iter() - .map(|doc| { - quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } - }) - .collect::>(); - - let kind_tag = kind_tag.unwrap_or(">"); - - quote::quote! { - { - let mut parent_key = parent_key.to_vec(); - parent_key.push(::std::borrow::Cow::Borrowed(#kind_tag)); - docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); - } - } - } else { - quote::quote! {} - }; - - let match_fields = self.fields.iter().enumerate().map(|(idx, field)| { - let name = syn::Ident::new(&format!("__field{}", idx), self.item.span()); - match &self.item.fields { - syn::Fields::Named(_) => { - let ident = field.item.ident.as_ref().unwrap(); - quote::quote! { - #ident: #name - } - } - syn::Fields::Unnamed(_) => { - quote::quote! { - #name - } - } - syn::Fields::Unit => { - quote::quote! {} - } - } - }); - - let ident = &self.item.ident; - let match_token = match &self.item.fields { - syn::Fields::Named(_) => { - quote::quote! { #ident{ #(#match_fields),* } } - } - syn::Fields::Unnamed(_) => { - quote::quote! { #ident( #(#match_fields),* ) } - } - syn::Fields::Unit => { - quote::quote! { #ident } - } - }; - - let fields = quote::quote! { - #variant_doc - #(#fields)* - }; - - Ok((match_token, fields)) - } + pub fn new(item: syn::Variant, _rename: Option) -> syn::Result { + let field_rename = RenameAll::parse(&item.attrs)?; + + Ok(Self { + // name: Name::parse(&item.attrs, item.ident.to_string().as_str(), rename)?, + fields: match &item.fields { + syn::Fields::Named(fields) => fields + .named + .iter() + .cloned() + .map(|field| Field::new(field, field_rename)) + .collect::>()?, + syn::Fields::Unnamed(fields) => fields + .unnamed + .iter() + .cloned() + .map(|field| Field::new(field, field_rename)) + .collect::>()?, + syn::Fields::Unit => Vec::new(), + }, + docs: parse_docs(&item.attrs), + args: VariantArgs::parse(&item.attrs)?, + item, + }) + } + + pub fn default_impl(&self) -> syn::Result { + let ident = &self.item.ident; + let fields = self.fields.iter().map(|field| { + let default = field.default_impl(); + + match field.item.ident.as_ref() { + // named field + Some(ident) => { + quote::quote! { + #ident: #default, + } + } + // tuple struct + None => { + quote::quote! { #default } + } + } + }); + + match &self.item.fields { + syn::Fields::Named(_) => Ok(quote::quote! { + Self::#ident { + #(#fields)* + } + }), + syn::Fields::Unnamed(_) => Ok(quote::quote! { + Self::#ident( + #(#fields,)* + ) + }), + syn::Fields::Unit => Ok(quote::quote! { + Self::#ident + }), + } + } + + pub fn docs_impl( + &self, + crate_path: &syn::Path, + kind_tag: Option<&str>, + content_tag: Option<&str>, + ) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> { + let fields = self.fields.iter().enumerate().map(|(idx, field)| { + let docs = field + .docs + .iter() + .map(|doc| { + quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } + }) + .collect::>(); + + let insert = if docs.is_empty() || field.flatten { + quote::quote! {} + } else { + quote::quote! { + docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); + } + }; + + let name_push = if (self.fields.len() == 1 && field.name.is_none()) || field.flatten { + quote::quote! {} + } else { + let name = field + .name + .as_ref() + .map(|name| name.serialize.clone()) + .unwrap_or_else(|| format!("{idx}")); + + quote::quote! { + parent_key.push(::std::borrow::Cow::Borrowed(#name)); + } + }; + + let name_push = if let Some(content_tag) = content_tag { + quote::quote! { + parent_key.push(::std::borrow::Cow::Borrowed(#content_tag)); + #name_push + } + } else { + name_push + }; + + let ident = syn::Ident::new(&format!("__field{}", idx), self.item.span()); + + quote::quote! { + { + let mut parent_key = parent_key.to_vec(); + #name_push + (&&&&#crate_path::settings::Wrapped(#ident)).add_docs(&parent_key, docs); + #insert + } + } + }); + + let variant_doc = if !self.docs.is_empty() { + let docs = self + .docs + .iter() + .map(|doc| { + quote::quote! { ::std::borrow::Cow::Borrowed(#doc) } + }) + .collect::>(); + + let kind_tag = kind_tag.unwrap_or(">"); + + quote::quote! { + { + let mut parent_key = parent_key.to_vec(); + parent_key.push(::std::borrow::Cow::Borrowed(#kind_tag)); + docs.insert(parent_key, ::std::borrow::Cow::Borrowed(&[#(#docs),*])); + } + } + } else { + quote::quote! {} + }; + + let match_fields = self.fields.iter().enumerate().map(|(idx, field)| { + let name = syn::Ident::new(&format!("__field{}", idx), self.item.span()); + match &self.item.fields { + syn::Fields::Named(_) => { + let ident = field.item.ident.as_ref().unwrap(); + quote::quote! { + #ident: #name + } + } + syn::Fields::Unnamed(_) => { + quote::quote! { + #name + } + } + syn::Fields::Unit => { + quote::quote! {} + } + } + }); + + let ident = &self.item.ident; + let match_token = match &self.item.fields { + syn::Fields::Named(_) => { + quote::quote! { #ident{ #(#match_fields),* } } + } + syn::Fields::Unnamed(_) => { + quote::quote! { #ident( #(#match_fields),* ) } + } + syn::Fields::Unit => { + quote::quote! { #ident } + } + }; + + let fields = quote::quote! { + #variant_doc + #(#fields)* + }; + + Ok((match_token, fields)) + } } #[derive(Debug, Clone, Default)] pub struct VariantArgs { - pub default: bool, + pub default: bool, } impl Args for VariantArgs { - fn apply_meta(&mut self, meta: &Meta) -> syn::Result { - match meta { - Meta::Path(path) if path.is_ident("default") => { - if self.default { - return Err(syn::Error::new_spanned(path, "duplicate setting")); - } - - self.default = true; - - Ok(true) - } - _ => Ok(false), - } - } + fn apply_meta(&mut self, meta: &Meta) -> syn::Result { + match meta { + Meta::Path(path) if path.is_ident("default") => { + if self.default { + return Err(syn::Error::new_spanned(path, "duplicate setting")); + } + + self.default = true; + + Ok(true) + } + _ => Ok(false), + } + } } diff --git a/foundations/macros/src/wrapped.rs b/foundations/macros/src/wrapped.rs index d1101f49..9a2fccec 100644 --- a/foundations/macros/src/wrapped.rs +++ b/foundations/macros/src/wrapped.rs @@ -3,76 +3,75 @@ use quote::{format_ident, quote, ToTokens}; use syn::{parse_quote, FnArg, Ident, ItemFn, Pat, Visibility}; pub fn wrapped(args: TokenStream, input: TokenStream) -> syn::Result { - let func_name = syn::parse::(args)?; - let mut input = syn::parse::(input)?; + let func_name = syn::parse::(args)?; + let mut input = syn::parse::(input)?; - let mut has_self_argument = false; - // remove types from args for use when calling the inner function - let mut args_without_types = vec![]; - let mut args_without_types_including_self = vec![]; - for arg in &input.sig.inputs { - match arg { - FnArg::Receiver(_) => { - has_self_argument = true; - args_without_types_including_self.push(quote!(self)); - } - FnArg::Typed(arg) => { - let tokens = if let Pat::Ident(mut a) = *arg.pat.clone() { - a.attrs.clear(); - a.mutability = None; - a.into_token_stream() - } else { - arg.pat.clone().into_token_stream() - }; - args_without_types.push(tokens.clone()); - args_without_types_including_self.push(tokens); - } - } - } + let mut has_self_argument = false; + // remove types from args for use when calling the inner function + let mut args_without_types = vec![]; + let mut args_without_types_including_self = vec![]; + for arg in &input.sig.inputs { + match arg { + FnArg::Receiver(_) => { + has_self_argument = true; + args_without_types_including_self.push(quote!(self)); + } + FnArg::Typed(arg) => { + let tokens = if let Pat::Ident(mut a) = *arg.pat.clone() { + a.attrs.clear(); + a.mutability = None; + a.into_token_stream() + } else { + arg.pat.clone().into_token_stream() + }; + args_without_types.push(tokens.clone()); + args_without_types_including_self.push(tokens); + } + } + } - let self_dot = if has_self_argument { - quote!(self.) - } else { - quote!() - }; + let self_dot = if has_self_argument { quote!(self.) } else { quote!() }; - let asyncness_await = match input.sig.asyncness { - Some(_) => quote!(.await), - None => quote!(), - }; + let asyncness_await = match input.sig.asyncness { + Some(_) => quote!(.await), + None => quote!(), + }; - let attrs = input.attrs.clone(); - let vis = input.vis.clone(); - let sig = input.sig.clone(); + let attrs = input.attrs.clone(); + let vis = input.vis.clone(); + let sig = input.sig.clone(); - let orig_name = input.sig.ident.clone(); - let inner_name = format_ident!("_wrappped_inner_{orig_name}"); + let orig_name = input.sig.ident.clone(); + let inner_name = format_ident!("_wrappped_inner_{orig_name}"); - input.sig.ident = inner_name.clone(); - input.vis = Visibility::Inherited; // make sure the inner function isn't leaked to the public - input.attrs = vec![ - // we will put the original attributes on the function we make - // we also don't want the inner function to appear in docs or autocomplete (if they do, they should be deprecated and give a warning if they are used) - parse_quote!(#[doc(hidden)]), - parse_quote!(#[deprecated = "internal wrapper function, please do not use!"]), - parse_quote!(#[inline(always)]), // let's make sure we don't produce more overhead than we need to, the output should produce similar assembly to the input (besides the end) - ]; + input.sig.ident = inner_name.clone(); + input.vis = Visibility::Inherited; // make sure the inner function isn't leaked to the public + input.attrs = vec![ + // we will put the original attributes on the function we make + // we also don't want the inner function to appear in docs or autocomplete (if they do, they should be deprecated + // and give a warning if they are used) + parse_quote!(#[doc(hidden)]), + parse_quote!(#[deprecated = "internal wrapper function, please do not use!"]), + parse_quote!(#[inline(always)]), /* let's make sure we don't produce more overhead than we need to, the output + * should produce similar assembly to the input (besides the end) */ + ]; - // for functions that take a self argument, we will need to put the inner function outside of our new function since we don't know what type self is - let (outer_input, inner_input) = if has_self_argument { - (Some(input), None) - } else { - (None, Some(input)) - }; + // for functions that take a self argument, we will need to put the inner + // function outside of our new function since we don't know what type self is + let (outer_input, inner_input) = if has_self_argument { + (Some(input), None) + } else { + (None, Some(input)) + }; - Ok(quote! { - #outer_input + Ok(quote! { + #outer_input - #(#attrs)* #vis #sig { - #inner_input + #(#attrs)* #vis #sig { + #inner_input - #[allow(deprecated)] - #func_name(#self_dot #inner_name(#(#args_without_types),*) #asyncness_await) - } - }) + #[allow(deprecated)] + #func_name(#self_dot #inner_name(#(#args_without_types),*) #asyncness_await) + } + }) } diff --git a/foundations/src/dataloader/batch_loader.rs b/foundations/src/dataloader/batch_loader.rs new file mode 100644 index 00000000..5a838476 --- /dev/null +++ b/foundations/src/dataloader/batch_loader.rs @@ -0,0 +1,22 @@ +use std::collections::HashSet; +use std::sync::Arc; + +use super::types::BatchState; +use super::Loader; + +pub(super) struct BatchLoader { + pub id: u64, + pub loader: Arc, + pub keys: HashSet, + pub start: tokio::time::Instant, + pub state: BatchState, +} + +impl BatchLoader { + pub async fn load(self, sephamore: Arc) { + let _ticket = sephamore.acquire().await.unwrap(); + let keys = self.keys.into_iter().collect::>(); + let result = self.loader.load(keys).await; + self.state.notify(Some(result)); + } +} diff --git a/foundations/src/dataloader/mod.rs b/foundations/src/dataloader/mod.rs new file mode 100644 index 00000000..a4075d9a --- /dev/null +++ b/foundations/src/dataloader/mod.rs @@ -0,0 +1,358 @@ +mod batch_loader; +mod types; +mod utils; + +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; +use std::time::Duration; + +use futures::FutureExt; + +use self::batch_loader::BatchLoader; +pub use self::types::LoaderOutput; +use self::types::{BatchState, DataLoaderInner}; +use self::utils::new_auto_loader; +use crate::runtime; + +pub trait Loader: Send + Sync + 'static { + type Key: Eq + std::hash::Hash + Clone + Send + Sync; + type Value: Clone + Send + Sync; + type Error: Clone + Send + Sync; + + fn load(&self, key: Vec) -> impl std::future::Future> + Send; +} + +pub struct DataLoader { + batch_id: AtomicU64, + loader: Arc, + max_batch_size: usize, + inner: DataLoaderInner, + _auto_loader_abort: CancelOnDrop, + name: String, +} + +struct CancelOnDrop(tokio::task::AbortHandle); + +impl Drop for CancelOnDrop { + fn drop(&mut self) { + self.0.abort(); + } +} + +impl Default for DataLoader { + fn default() -> Self { + Self::new(std::any::type_name::(), L::default()) + } +} + +impl DataLoader { + pub fn new(name: impl ToString, loader: L) -> Self { + Self::with_concurrency_limit(name, loader, 10) + } + + pub fn with_concurrency_limit(name: impl ToString, loader: L, concurrency_limit: usize) -> Self { + let duration = Duration::from_millis(5); + + let inner = DataLoaderInner::new(concurrency_limit, duration); + + Self { + batch_id: AtomicU64::new(0), + loader: Arc::new(loader), + max_batch_size: 1000, + _auto_loader_abort: new_auto_loader(inner.clone()), + inner, + name: name.to_string(), + } + } + + pub fn set_max_batch_size(mut self, max_batch_size: usize) -> Self { + self.max_batch_size = max_batch_size; + self + } + + pub fn set_duration(self, duration: Duration) -> Self { + self.inner + .duration + .store(duration.as_nanos() as u64, std::sync::atomic::Ordering::Relaxed); + self + } + + async fn extend_loader(&self, keys: impl Iterator) -> Vec<(Vec, BatchState)> { + let mut batches = Vec::new(); + let mut current_batch = None; + + let mut active_batch = self.inner.active_batch.write().await; + + for key in keys { + if active_batch + .as_ref() + .map(|b| b.keys.len() >= self.max_batch_size) + .unwrap_or(true) + { + let batch = BatchLoader { + id: self.batch_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), + loader: self.loader.clone(), + keys: Default::default(), + start: tokio::time::Instant::now(), + state: Default::default(), + }; + + if let Some(current_batch) = current_batch.replace((Vec::new(), batch.state.clone())) { + batches.push(current_batch); + } + + if let Some(batch) = active_batch.replace(batch) { + runtime::spawn(batch.load(self.inner.semaphore.clone())); + } + + self.inner.notify.notify_waiters(); + } else if current_batch.is_none() { + current_batch = Some((Vec::new(), active_batch.as_ref().unwrap().state.clone())); + } + + let (Some(active_batch), Some((current_batch, _))) = (active_batch.as_mut(), current_batch.as_mut()) else { + unreachable!(); + }; + + active_batch.keys.insert(key.clone()); + current_batch.push(key); + } + + if let Some(current_batch) = current_batch.take() { + batches.push(current_batch); + } + + if let Some(batch) = active_batch.as_mut() { + if batch.keys.len() > self.max_batch_size { + let batch = active_batch.take().unwrap(); + runtime::spawn(batch.load(self.inner.semaphore.clone())); + } + } + + batches + } + + async fn internal_load(&self, keys: impl IntoIterator) -> LoaderOutput { + let key_set = keys.into_iter().collect::>().into_iter().collect::>(); + + if key_set.is_empty() { + return Ok(Default::default()); + } + + let batches = self.extend_loader(key_set.iter().cloned()).await; + + let batches = futures::future::join_all( + batches + .iter() + .map(|(keys, batch)| batch.wait().map(move |result| result.map(|result| (keys, result)))), + ) + .await; + + batches + .into_iter() + .flatten() + .try_fold(HashMap::new(), |mut acc, (keys, batch)| match batch { + Ok(batch) => { + acc.extend( + keys.into_iter() + .cloned() + .filter_map(|key| batch.get(&key).cloned().map(|value| (key, value))), + ); + + Ok(acc) + } + Err(err) => Err(err.clone()), + }) + } + + #[tracing::instrument(skip(self, keys), fields(name = self.name.as_str()))] + pub async fn load_many(&self, keys: impl IntoIterator) -> LoaderOutput { + self.internal_load(keys).await + } + + #[tracing::instrument(skip(self, key), fields(name = self.name.as_str()))] + pub async fn load(&self, key: L::Key) -> Result, L::Error> { + Ok(self.internal_load(std::iter::once(key.clone())).await?.remove(&key)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::hash_map::RandomState; + use std::collections::HashMap; + + use crate::dataloader::LoaderOutput; + + type DynBoxLoader = Box) -> HashMap + Sync + Send>; + + struct LoaderTest { + results: DynBoxLoader, + } + + impl crate::dataloader::Loader for LoaderTest { + type Error = (); + type Key = u64; + type Value = u64; + + async fn load(&self, keys: Vec) -> LoaderOutput { + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + Ok((self.results)(keys)) + } + } + + #[tokio::test] + async fn test_data_loader() { + let run_count = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)); + + let loader = LoaderTest { + results: Box::new(move |keys| { + let mut results = HashMap::new(); + + assert!(run_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) == 0); + + assert!(keys.len() <= 1000); + + for key in keys { + assert!(!results.contains_key(&key)); + + results.insert(key, key * 2); + } + + results + }), + }; + + let dataloader = crate::dataloader::DataLoader::new("test", loader); + + let futures = (0..250) + .map(|i| dataloader.load(i as u64)) + .chain((0..250).map(|i| dataloader.load(i as u64))); + + let results = futures::future::join_all(futures).await; + + let expected = (0..250) + .map(|i| Ok(Some(i * 2))) + .chain((0..250).map(|i| Ok(Some(i * 2)))) + .collect::>(); + + assert_eq!(results, expected); + } + + #[tokio::test] + async fn test_data_loader_larger() { + let loader = LoaderTest { + results: Box::new(move |keys| { + let mut results = HashMap::new(); + + assert!(keys.len() <= 1000); + + for key in keys { + assert!(!results.contains_key(&key)); + + results.insert(key, key * 2); + } + + results + }), + }; + + let dataloader = crate::dataloader::DataLoader::new("test", loader); + + const LIMIT: usize = 10_000; + + let results = futures::future::join_all((0..LIMIT).map(|i| dataloader.load(i as u64))).await; + + let expected = (0..LIMIT).map(|i| Ok(Some(i as u64 * 2))).collect::>(); + + assert_eq!(results, expected); + } + + #[tokio::test] + async fn test_data_loader_change_batch_size() { + let loader = LoaderTest { + results: Box::new(move |keys| { + let mut results = HashMap::new(); + + assert!(keys.len() <= 3000); + + for key in keys { + assert!(!results.contains_key(&key)); + + results.insert(key, key * 2); + } + + results + }), + }; + + let dataloader = crate::dataloader::DataLoader::new("test", loader).set_max_batch_size(3000); + + let futures = (0..5000).map(|i| dataloader.load(i as u64)); + + let results = futures::future::join_all(futures).await; + + let expected = (0..5000).map(|i| Ok(Some(i * 2))).collect::>(); + + assert_eq!(results, expected); + } + + #[tokio::test] + async fn test_data_loader_change_duration() { + let loader = LoaderTest { + results: Box::new(move |keys| { + let mut results = HashMap::new(); + + assert!(keys.len() <= 1000); + + for key in keys { + assert!(!results.contains_key(&key)); + + results.insert(key, key * 2); + } + + results + }), + }; + + let dataloader = + crate::dataloader::DataLoader::new("test", loader).set_duration(tokio::time::Duration::from_millis(100)); + + let futures = (0..250) + .map(|i| dataloader.load(i as u64)) + .chain((0..250).map(|i| dataloader.load(i as u64))); + + let results = futures::future::join_all(futures).await; + + let expected = (0..250) + .map(|i| Ok(Some(i * 2))) + .chain((0..250).map(|i| Ok(Some(i * 2)))) + .collect::>(); + + assert_eq!(results, expected); + } + + #[tokio::test] + async fn test_data_loader_value_deduplication() { + let run_count = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)); + + let loader = LoaderTest { + results: Box::new({ + let run_count = run_count.clone(); + move |keys| { + run_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + keys.iter().map(|&key| (key, key * 2)).collect() + } + }), + }; + + let dataloader = crate::dataloader::DataLoader::new("test", loader); + + let futures = vec![dataloader.load(5), dataloader.load(5), dataloader.load(5)]; + + let results: Vec<_> = futures::future::join_all(futures).await; + + assert_eq!(results, vec![Ok(Some(10)), Ok(Some(10)), Ok(Some(10))]); + assert_eq!(run_count.load(std::sync::atomic::Ordering::SeqCst), 1); // Ensure the loader was only called once + } +} diff --git a/foundations/src/dataloader/types.rs b/foundations/src/dataloader/types.rs new file mode 100644 index 00000000..bec057c0 --- /dev/null +++ b/foundations/src/dataloader/types.rs @@ -0,0 +1,101 @@ +use std::collections::HashMap; +use std::sync::atomic::AtomicU64; +use std::sync::{Arc, OnceLock}; + +use tokio::sync::Notify; +use tokio::time::Instant; + +use super::batch_loader::BatchLoader; +use super::Loader; + +#[allow(type_alias_bounds)] +pub type LoaderOutput = Result, L::Error>; + +pub struct DataLoaderInternal { + pub active_batch: tokio::sync::RwLock>>, + pub notify: tokio::sync::Notify, + pub semaphore: Arc, + pub duration: AtomicU64, +} + +pub(super) struct DataLoaderInner(Arc>); + +impl Clone for DataLoaderInner { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl DataLoaderInner { + pub fn new(concurrency: usize, duration: std::time::Duration) -> Self { + Self(Arc::new(DataLoaderInternal { + active_batch: Default::default(), + notify: Default::default(), + semaphore: Arc::new(tokio::sync::Semaphore::new(concurrency)), + duration: AtomicU64::new(duration.as_nanos() as u64), + })) + } + + pub async fn load_active_batch(&self) -> Option<(u64, Instant)> { + self.active_batch.read().await.as_ref().map(|b| (b.id, b.start)) + } + + pub fn duration(&self) -> std::time::Duration { + std::time::Duration::from_nanos(self.0.duration.load(std::sync::atomic::Ordering::Relaxed)) + } +} + +impl std::ops::Deref for DataLoaderInner { + type Target = DataLoaderInternal; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +struct InternalBatchState { + notify: Notify, + result: OnceLock>>, +} + +pub(super) struct BatchState(Arc>); + +impl Default for BatchState { + fn default() -> Self { + Self(Arc::new(InternalBatchState { + notify: Notify::new(), + result: OnceLock::new(), + })) + } +} + +impl Clone for BatchState { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl BatchState { + pub fn notify(&self, result: Option>) -> bool { + if self.0.result.set(result).is_ok() { + self.0.notify.notify_waiters(); + true + } else { + false + } + } + + pub async fn wait(&self) -> Option<&LoaderOutput> { + let notify = self.0.notify.notified(); + if let Some(result) = self.0.result.get() { + return result.as_ref(); + } + + notify.await; + + match self.0.result.get() { + Some(result) => result.as_ref(), + None => None, + } + } +} diff --git a/foundations/src/dataloader/utils.rs b/foundations/src/dataloader/utils.rs new file mode 100644 index 00000000..34761b7a --- /dev/null +++ b/foundations/src/dataloader/utils.rs @@ -0,0 +1,33 @@ +use super::types::DataLoaderInner; +use super::{CancelOnDrop, Loader}; +use crate::runtime; + +pub(super) fn new_auto_loader(inner: DataLoaderInner) -> CancelOnDrop { + CancelOnDrop( + runtime::spawn(async move { + loop { + let notify = inner.notify.notified(); + let Some((batch_id, start)) = inner.load_active_batch().await else { + notify.await; + continue; + }; + + drop(notify); + + let duration = inner.duration(); + if start.elapsed() < duration { + tokio::time::sleep_until(start + duration).await; + } + + let mut active_batch = inner.active_batch.write().await; + + if active_batch.as_ref().map(|b| b.id != batch_id).unwrap_or(true) { + continue; + } + + runtime::spawn(active_batch.take().unwrap().load(inner.semaphore.clone())); + } + }) + .abort_handle(), + ) +} diff --git a/foundations/src/lib.rs b/foundations/src/lib.rs index 0427d3fb..7fcf0df0 100644 --- a/foundations/src/lib.rs +++ b/foundations/src/lib.rs @@ -39,6 +39,9 @@ pub mod signal; #[cfg(feature = "context")] pub mod context; +#[cfg(feature = "dataloader")] +pub mod dataloader; + #[derive(Debug, Clone, Copy, Default)] /// Information about the service. pub struct ServiceInfo {