feat: Metadata and semaphore store

This commit is contained in:
LynithDev 2025-04-07 19:53:33 +02:00
parent 54d16e5d69
commit 1f2d560983
25 changed files with 448 additions and 96 deletions

View file

@ -6,7 +6,7 @@ reorder_imports = true
imports_granularity = "Module"
reorder_modules = true
remove_nested_parens = true
edition = "2021"
edition = "2024"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false

1
Cargo.lock generated
View file

@ -2989,6 +2989,7 @@ name = "onelauncher_entity"
version = "0.2.0-alpha.2"
dependencies = [
"chrono",
"interpulse",
"onelauncher_macro",
"sea-orm",
"serde",

View file

@ -21,7 +21,7 @@ tauri = [
"specta",
"gui"
]
specta = [ "dep:specta", "onelauncher_entity/specta" ]
specta = [ "dep:specta", "interpulse/specta", "onelauncher_entity/specta" ]
cli = [ "dep:indicatif" ]
gui = []

View file

@ -4,8 +4,8 @@ use onelauncher_core::store::Dirs;
async fn main() {
let dirs = Dirs::get().await.expect("couldn't initialize dirs");
println!(" Base dir : {:?}", dirs.base_dir());
println!("Launcher logs dir : {:?}", dirs.launcher_logs_dir());
println!(" Metadata dir : {:?}", dirs.metadata_dir());
println!(" DB file : {:?}", dirs.db_file());
println!(" Base dir : {}", dirs.base_dir().display());
println!("Launcher logs dir : {}", dirs.launcher_logs_dir().display());
println!(" Metadata dir : {}", dirs.metadata_dir().display());
println!(" DB file : {}", dirs.db_file().display());
}

View file

@ -0,0 +1,20 @@
use onelauncher_core::{api::proxy::ProxyDynamic, error::LauncherResult, initialize_core, store::{CoreOptions, State}};
use onelauncher_entity::loader::GameLoader;
#[tokio::main]
async fn main() -> LauncherResult<()> {
initialize_core(CoreOptions::default(), ProxyDynamic::default()).await?;
let state = State::get().await?;
let metadata = &mut state.metadata.write().await;
println!("Initialized: {}", metadata.initialized());
println!("Minecraft: {:?}", metadata.get_vanilla(GameLoader::Vanilla).await.is_ok());
println!("Forge: {:?}", metadata.get_modded(GameLoader::Forge).await.is_ok());
println!("Initialized: {}", metadata.initialized());
println!("NeoForge: {:?}", metadata.get_modded(GameLoader::NeoForge).await.is_ok());
println!("Fabric: {:?}", metadata.get_modded(GameLoader::Fabric).await.is_ok());
println!("Quilt: {:?}", metadata.get_modded(GameLoader::Quilt).await.is_ok());
Ok(())
}

View file

@ -65,7 +65,7 @@ pub async fn sync_clusters() -> LauncherResult<Vec<i32>> {
// We have a directory that is not in the database
if let Err(err) = sync_from_fs_to_db(&path).await {
tracing::error!("failed to sync cluster from fs to db: {}", err);
};
}
}
Ok(missing_ids)

View file

@ -30,6 +30,11 @@ pub async fn init_ingress_opt(
#[tracing::instrument]
pub async fn send_ingress(id: impl AsRef<IngressId> + std::fmt::Debug, increment: f64) -> LauncherResult<()> {
if !State::initialized() {
tracing::debug!("attempted to send ingress when state is not initialized");
return Ok(())
}
let state = State::get().await?;
let processor = &state.ingress_processor;

View file

@ -4,6 +4,8 @@ pub mod message;
#[cfg(feature = "cli")] mod proxy_cli;
mod proxy_empty;
use std::fmt::Debug;
#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri;
#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli;
pub use proxy_empty::ProxyEmpty;
@ -12,7 +14,7 @@ pub use proxy_empty::ProxyEmpty;
#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty;
#[async_trait::async_trait]
pub trait LauncherProxy: Send + Sync {
pub trait LauncherProxy: Send + Sync + Debug {
async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>;
async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>;
@ -22,4 +24,3 @@ pub trait LauncherProxy: Send + Sync {
#[cfg(feature = "gui")]
fn show_main_window(&self) -> crate::LauncherResult<()>;
}

View file

@ -8,7 +8,7 @@ use crate::{constants::CLI_TOTAL_INGRESS, store::ingress::IngressPayload, Launch
use super::LauncherProxy;
#[derive(Default)]
#[derive(Default, Debug)]
pub struct ProxyCli {
ingress_feeds: RwLock<HashMap<Uuid, ProgressBar>>,
}

View file

@ -2,7 +2,7 @@ use crate::{store::ingress::IngressPayload, LauncherResult};
use super::LauncherProxy;
#[derive(Default)]
#[derive(Default, Debug)]
pub struct ProxyEmpty;
impl ProxyEmpty {

View file

@ -13,6 +13,8 @@ pub enum LauncherError {
#[error(transparent)]
DiscordError(#[from] crate::store::discord::DiscordError),
#[error(transparent)]
MetadataError(#[from] crate::store::metadata::MetadataError),
#[error(transparent)]
DaoError(#[from] DaoError),
#[error(transparent)]

View file

@ -1,5 +1,7 @@
#![feature(let_chains)]
use api::proxy::LauncherProxy;
use store::{proxy::ProxyState, Core, CoreOptions, Dirs, State};
use store::{proxy::ProxyState, semaphore::SemaphoreStore, Core, CoreOptions, Dirs, State};
use error::LauncherResult;
use logger::start_logger;
@ -21,8 +23,11 @@ pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherP
Dirs::get().await?;
start_logger().await;
SemaphoreStore::get().await;
ProxyState::initialize(proxy_backend).await?;
State::get().await?;
let _ = State::get().await?;
tracing::info!("Core initialized successfully");
Ok(())
}

View file

@ -2,7 +2,7 @@
// This will log to the console, and will not log to a file
#[cfg(debug_assertions)]
pub async fn start_logger() {
use tracing_subscriber::{prelude::*, util::SubscriberInitExt};
use tracing_subscriber::{fmt::format::FmtSpan, prelude::*, util::SubscriberInitExt};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME"))));
@ -15,6 +15,7 @@ pub async fn start_logger() {
.with_line_number(true)
.with_level(true)
.with_thread_names(true)
.with_span_events(FmtSpan::FULL)
.pretty()
)
.with(filter)

View file

@ -0,0 +1,229 @@
use std::str::FromStr;
use interpulse::api::minecraft::VersionManifest as VanillaManifest;
use interpulse::api::modded::Manifest as ModdedManifest;
use onelauncher_entity::loader::GameLoader;
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::constants;
use crate::error::LauncherResult;
use crate::utils::http::fetch_json;
use crate::utils::io;
use super::Dirs;
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Debug, Default)]
pub struct Metadata {
initialized: bool,
inner: MetadataInner,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct MetadataInner {
minecraft: Option<VanillaManifest>,
forge: Option<ModdedManifest>,
neo: Option<ModdedManifest>, // TODO(metadata): change to neoforge
fabric: Option<ModdedManifest>,
quilt: Option<ModdedManifest>,
// legacyfabric: Option<ModdedManifest>,
}
#[derive(Debug, thiserror::Error)]
pub enum MetadataError {
#[error("failed to fetch metadata")]
FetchError,
#[error("loader {0} does not use a modded manifest")]
NotModdedManifest(GameLoader),
#[error("loader {0} does not use a vanilla manifest")]
NotVanillaManifest(GameLoader),
#[error("failed to parse metadata")]
ParseError(#[from] serde_json::Error),
}
impl Metadata {
#[must_use]
pub const fn initialized(&self) -> bool {
self.initialized
}
#[must_use]
pub fn initialize() -> Self {
Self::default()
}
pub async fn get_vanilla(&mut self, loader: GameLoader) -> LauncherResult<&VanillaManifest> {
if loader.is_mod_loader() {
return Err(MetadataError::NotVanillaManifest(loader).into());
}
if !self.initialized() {
self.load().await?;
}
match loader {
GameLoader::Vanilla => self.inner.minecraft.as_ref(),
_ => None,
}.ok_or_else(|| MetadataError::FetchError.into())
}
pub async fn get_modded(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> {
if !loader.is_mod_loader() {
return Err(MetadataError::NotModdedManifest(loader).into());
}
if !self.initialized() {
self.load().await?;
}
match loader {
GameLoader::Forge => self.inner.forge.as_ref(),
GameLoader::NeoForge => self.inner.neo.as_ref(),
GameLoader::Fabric => self.inner.fabric.as_ref(),
GameLoader::Quilt => self.inner.quilt.as_ref(),
_ => None,
}.ok_or_else(|| MetadataError::FetchError.into())
}
async fn load(&mut self) -> LauncherResult<()> {
let path = Dirs::get_caches_dir().await?.join("metadata.json");
let mut save_file = false;
let mut metadata = Self::default();
if let Ok(metadata_json) = io::read_json::<MetadataInner>(&path).await {
metadata.inner = metadata_json;
if metadata.refetch_errored().await > 0 {
save_file = true;
}
} else {
metadata.fetch_all().await;
save_file = true;
}
if save_file {
io::create_dir_all(&path.parent().expect("couldn't get metadata.json parent")).await?;
io::write_json(&path, &metadata.inner).await?;
}
*self = metadata;
self.initialized = true;
Ok(())
}
/// Fetches the all missing metadata and returns the amount that were a success
#[tracing::instrument(skip_all)]
pub async fn refetch_errored(&mut self) -> u8 {
let mut changed: u8 = 0;
macro_rules! check {
($var:tt, $is_modded:tt) => {
if self.inner.$var.is_none() {
if let Ok(loader) = GameLoader::from_str(stringify!($var)) {
match check!(_fetch, loader, $is_modded) {
Ok(data) => {
self.inner.$var = Some(data);
changed += 1;
}
Err(err) => {
tracing::error!("failed to fetch manifest for {}: {}", loader, err);
}
}
};
}
};
(_fetch, $loader:expr, false) => {
fetch_vanilla_manifest($loader).await
};
(_fetch, $loader:expr, true) => {
fetch_modded_manifest($loader).await
};
}
check!(minecraft, false);
check!(forge, true);
check!(neo, true);
check!(fabric, true);
check!(quilt, true);
changed
}
/// Force fetches all the manifests and sets them in the current instance
#[tracing::instrument(skip_all)]
pub async fn fetch_all(&mut self) {
macro_rules! fetch_manifest {
($loader:expr, false) => {
fetch_vanilla_manifest($loader).await
};
($loader:expr, true) => {
fetch_modded_manifest($loader).await
};
}
macro_rules! task {
($(($var:tt, $is_modded:tt)),*) => {
let ($($var),*) = tokio::join!(
$(
tokio::spawn(async move {
if let Ok(loader) = GameLoader::from_str(stringify!(minecraft)) {
fetch_manifest!(loader, $is_modded).map_err(|e| $crate::send_error!("couldn't fetch {loader} manifest: {e}")).ok()
} else {
tracing::error!("failed to fetch {} manifest", stringify!($var));
None
}
}),
)*
);
$(
if let Ok(data) = $var {
self.inner.$var = data;
}
)*
};
}
task! {
// (name, is_modded)
(minecraft, false),
(forge, true),
(neo, true),
(fabric, true),
(quilt, true)
};
}
}
pub async fn fetch_vanilla_manifest(loader: GameLoader) -> LauncherResult<VanillaManifest> {
fetch_manifest(loader).await
}
pub async fn fetch_modded_manifest(loader: GameLoader) -> LauncherResult<ModdedManifest> {
fetch_manifest(loader).await
}
pub async fn fetch_manifest<T: DeserializeOwned>(loader: GameLoader) -> LauncherResult<T> {
fetch_json::<T>(
Method::GET,
format!(
"{}/{}/v{}/manifest.json",
constants::METADATA_API_URL,
loader.get_format_name(),
loader.get_format_version()
)
.as_str(),
None,
None,
)
.await
}

View file

@ -2,6 +2,8 @@ pub(super) mod db;
pub mod discord;
pub mod ingress;
pub mod proxy;
pub mod semaphore;
pub mod metadata;
mod core_options;
mod dirs;
mod settings;

View file

@ -20,6 +20,7 @@ impl Deref for ProxyState {
}
impl ProxyState {
#[tracing::instrument(level = "debug")]
pub async fn initialize(proxy: impl LauncherProxy + 'static) -> LauncherResult<Arc<Self>> {
PROXY_STATE
.get_or_try_init(|| async {

View file

@ -0,0 +1,26 @@
use std::sync::Arc;
use tokio::sync::{OnceCell, Semaphore};
static STORE_STATE: OnceCell<Arc<SemaphoreStore>> = OnceCell::const_new();
pub struct SemaphoreStore {
fetch: Arc<Semaphore>
}
impl SemaphoreStore {
pub async fn get() -> Arc<Self> {
STORE_STATE.get_or_init(|| async { Self::initialize() }).await.clone()
}
#[tracing::instrument]
fn initialize() -> Arc<Self> {
Arc::new(Self {
fetch: Arc::new(Semaphore::new(10)), // Set the limit to 10
})
}
pub async fn fetch() -> Arc<Semaphore> {
Self::get().await.fetch.clone()
}
}

View file

@ -3,42 +3,32 @@ use std::sync::Arc;
use sea_orm::DatabaseConnection;
use tokio::sync::{OnceCell, RwLock};
use crate::{utils::http::{self, FetchSemaphore}, LauncherResult};
use crate::LauncherResult;
use super::{discord::DiscordRPC, ingress::IngressProcessor, Settings};
use super::Settings;
use super::discord::DiscordRPC;
use super::ingress::IngressProcessor;
use super::metadata::Metadata;
/// The static [`OnceCell<RwLock<State>>`] for storing the global runtime launcher state.
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
pub struct State {
pub ingress_processor: IngressProcessor,
pub settings: RwLock<Settings>,
pub db: DatabaseConnection,
pub metadata: RwLock<Metadata>,
pub rpc: Option<DiscordRPC>,
pub client: Arc<reqwest::Client>,
pub(crate) fetch_semaphore: FetchSemaphore,
}
impl State {
/// Get the current global launcher state (or initialize it)
pub async fn get() -> LauncherResult<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
Ok(Arc::new(
LAUNCHER_STATE
.get_or_try_init(Self::initialize)
.await?
.read()
.await,
))
}
/// Get and lock writing to the current global launcher state (or initalize it)
pub async fn get_and_write() -> LauncherResult<tokio::sync::RwLockWriteGuard<'static, Self>> {
pub async fn get() -> LauncherResult<Arc<Self>> {
Ok(LAUNCHER_STATE
.get_or_try_init(Self::initialize)
.await?
.write()
.await)
.clone()
)
}
pub fn initialized() -> bool {
@ -46,30 +36,26 @@ impl State {
}
#[tracing::instrument]
async fn initialize() -> LauncherResult<RwLock<Self>> {
async fn initialize() -> LauncherResult<Arc<Self>> {
let ingress_processor = IngressProcessor::new();
let settings = Settings::new().await;
let db = super::db::create_pool().await?;
let metadata = Metadata::initialize();
let rpc = match DiscordRPC::initialize() {
Ok(rpc) => Some(rpc),
Err(err) => {
tracing::warn!("{}", err);
None
},
}
};
let reqwest_client = http::create_client()?;
let fetch_semaphore = FetchSemaphore::new(settings.max_concurrent_requests);
Ok(RwLock::new(Self {
Ok(Arc::new(Self {
ingress_processor,
settings: RwLock::new(settings),
db,
metadata: RwLock::new(metadata),
rpc,
client: Arc::new(reqwest_client),
fetch_semaphore,
}))
}
}
}

View file

@ -2,7 +2,7 @@
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use bytes::Bytes;
use reqwest::Method;
@ -13,7 +13,8 @@ use tokio_stream::StreamExt;
use crate::api::ingress::send_ingress;
use crate::error::LauncherResult;
use crate::store::ingress::IngressRef;
use crate::store::{Core, State};
use crate::store::semaphore::SemaphoreStore;
use crate::store::Core;
use super::crypto::{CryptoError, HashAlgorithm};
use super::io;
@ -28,8 +29,8 @@ impl FetchSemaphore {
}
}
pub fn create_client() -> LauncherResult<reqwest::Client> {
Ok(reqwest::ClientBuilder::new()
static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest::ClientBuilder::new()
.tcp_keepalive(Some(std::time::Duration::from_secs(15)))
.use_rustls_tls()
.default_headers({
@ -44,10 +45,12 @@ pub fn create_client() -> LauncherResult<reqwest::Client> {
headers.insert(reqwest::header::USER_AGENT, header);
headers
})
.build()?)
}
.build()
.expect("failed to build reqwest client!")
});
#[tracing::instrument(skip(body, headers, ingress_ref))]
#[tracing::instrument(level = "debug", skip(body, headers, ingress_ref))]
#[onelauncher_macro::pin]
pub async fn fetch_advanced(
method: Method,
url: &str,
@ -56,9 +59,9 @@ pub async fn fetch_advanced(
headers: Option<HashMap<&str, &str>>,
ingress_ref: Option<&IngressRef<'_>>,
) -> LauncherResult<Bytes> {
let state = State::get().await?;
let _permit = state.fetch_semaphore.0.acquire().await?;
let client = &state.client;
let semaphore = SemaphoreStore::fetch().await;
let _permit = semaphore.acquire().await?;
let client = &REQWEST_CLIENT;
for attempt in 0..Core::get().fetch_attempts {
let mut req = client.request(method.clone(), url);

View file

@ -1,4 +1,5 @@
use async_compression::tokio::bufread::GzipDecoder;
use serde::{de::DeserializeOwned, Serialize};
use tempfile::TempDir;
use tokio::io::AsyncReadExt;
@ -18,6 +19,8 @@ pub enum IOError {
ZipError(#[from] zip::result::ZipError),
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error("json deserialization error: {0}")]
DeserializeError(#[from] serde_json::Error),
}
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
@ -119,7 +122,7 @@ pub async fn read_to_string(path: impl AsRef<std::path::Path>) -> Result<String,
})
}
/// Reads the entire contents of a file into a bytes vector.
/// Asynchronously reads the entire contents of a file into a bytes vector.
pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError> {
let path = path.as_ref();
tokio::fs::read(path)
@ -130,6 +133,11 @@ pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError>
})
}
/// Asynchronously read a file as JSON and return the deserialized object
pub async fn read_json<T: DeserializeOwned>(path: impl AsRef<std::path::Path>) -> Result<T, IOError> {
Ok(serde_json::from_slice(&read(path).await?)?)
}
/// Asynchrously write to a tempfile that is then transferred to an official [`AsRef<Path>`].
pub async fn write(
path: impl AsRef<std::path::Path>,
@ -144,6 +152,14 @@ pub async fn write(
})
}
/// Asynchronously write json to a file, creating it if it does not exist.
pub async fn write_json<T: Serialize>(
path: impl AsRef<std::path::Path>,
data: T
) -> Result<(), IOError> {
write(path, serde_json::to_vec(&data)?).await
}
/// Renames a file or directory to a new name, replacing the original file if `to` already exists.
pub async fn rename(
from: impl AsRef<std::path::Path>,

View file

@ -16,7 +16,7 @@ path = "src/lib.rs"
[features]
default = []
specta = [ "dep:specta" ]
specta = [ "dep:specta", "interpulse/specta" ]
[dependencies]
onelauncher_macro = { workspace = true }
@ -26,4 +26,5 @@ serde_json = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
url = { workspace = true }
interpulse = { workspace = true }
specta = { workspace = true, optional = true }

View file

@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
#[serde(rename_all = "lowercase")]
#[sea_orm(rs_type = "u8", db_type = "Integer")]
pub enum GameLoader {
#[default]
Vanilla = 0,
#[default]
Unknown = 1,
Forge = 2,
NeoForge = 3,
@ -25,6 +25,25 @@ impl GameLoader {
matches!(self, Self::Forge | Self::NeoForge | Self::Quilt | Self::Fabric | Self::LegacyFabric)
}
pub const fn get_format_version(&self) -> usize {
match self {
Self::Vanilla | Self::Unknown => interpulse::api::minecraft::CURRENT_FORMAT_VERSION,
Self::Forge => interpulse::api::modded::CURRENT_FORGE_FORMAT_VERSION,
Self::NeoForge => interpulse::api::modded::CURRENT_NEOFORGE_FORMAT_VERSION,
Self::Quilt => interpulse::api::modded::CURRENT_QUILT_FORMAT_VERSION,
Self::Fabric => interpulse::api::modded::CURRENT_FABRIC_FORMAT_VERSION,
Self::LegacyFabric => interpulse::api::modded::CURRENT_LEGACY_FABRIC_FORMAT_VERSION,
}
}
pub fn get_format_name(&self) -> String {
match self {
Self::Vanilla | Self::Unknown => String::from("minecraft"),
Self::NeoForge => String::from("neo"), // TODO(metadata): change to neoforge
_ => self.to_string().to_lowercase().replace(['_', '.', ' '], "")
}
}
}
impl From<String> for GameLoader {

View file

@ -1,5 +1,5 @@
use quote::quote;
use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result};
mod specta;
mod pin;
/// This macro is used to derive the `specta::Type` trait for the annotated item.
/// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes)
@ -10,43 +10,11 @@ use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result};
/// ```
#[proc_macro_attribute]
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mut attrs = SpectaAttributes::default();
let parser = syn::meta::parser(|meta| attrs.parse(meta));
parse_macro_input!(attr with parser);
let input = parse_macro_input!(item as Item);
let specta_event_type = if attrs.with_event {
quote! {
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
}
} else {
quote! {}
};
let specta_type = quote! {
#[cfg_attr(feature = "specta", derive(specta::Type))]
};
let expanded = quote! {
#specta_event_type
#specta_type
#input
};
expanded.into()
specta::specta(attr, item)
}
#[derive(Default)]
struct SpectaAttributes {
with_event: bool,
/// This macro Box::Pin a function
#[proc_macro_attribute]
pub fn pin(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
pin::pin(attr, item)
}
impl SpectaAttributes {
fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("with_event") {
self.with_event = true;
}
Ok(())
}
}

22
packages/macro/src/pin.rs Normal file
View file

@ -0,0 +1,22 @@
use quote::quote;
use syn::{parse_macro_input, ItemFn};
pub fn pin(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as ItemFn);
let attrs = &input.attrs;
let vis = &input.vis;
let sig = &input.sig;
let body = &input.block;
let result = quote! {
#(#attrs)*
#vis #sig {
Box::pin(async move {
#body
}).await
}
};
result.into()
}

View file

@ -0,0 +1,44 @@
use quote::quote;
use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result};
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mut attrs = SpectaAttributes::default();
let parser = syn::meta::parser(|meta| attrs.parse(meta));
parse_macro_input!(attr with parser);
let input = parse_macro_input!(item as Item);
let specta_event_type = if attrs.with_event {
quote! {
#[cfg_attr(feature = "tauri", derive(tauri_specta::Event))]
}
} else {
quote! {}
};
let specta_type = quote! {
#[cfg_attr(feature = "specta", derive(specta::Type))]
};
let expanded = quote! {
#specta_event_type
#specta_type
#input
};
expanded.into()
}
#[derive(Default)]
struct SpectaAttributes {
with_event: bool,
}
impl SpectaAttributes {
fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("with_event") {
self.with_event = true;
}
Ok(())
}
}