diff --git a/Cargo.toml b/Cargo.toml index 34e0fd7168bf6ff80948be8f6cdf080b80358425..505f12987116e088dfa18884991c567a1f29feca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,37 @@ [package] -name = "micro_game_macros" +name = "micro_games_macros" version = "0.1.0" edition = "2021" +authors = ["Louis Capitanchik <contact@louiscap.co>"] +description = "Utility macros to make it easier to build complex systems with Bevy" +license = "apache-2.0" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "micro_games_macros" +path = "src/lib.rs" +proc-macro = true + +[features] +default = ["kayak"] +kayak = [] [dependencies] +proc-macro2 = "1.0.66" +quote = "1.0.33" +syn = "2.0.29" + +[dev-dependencies] +test-case = "3.1.0" +serde = { version = "1.0.176", features = ["derive"]} +serde_json = "1.0.96" +anyhow = "1.0.72" + + +[dev-dependencies.bevy] +version = "0.11.0" +default-features = false +features = [ + "bevy_asset", + "bevy_sprite", + "bevy_core_pipeline", +] diff --git a/src/asset_system/components.rs b/src/asset_system/components.rs new file mode 100644 index 0000000000000000000000000000000000000000..5d9c4e6d61e4f76fd802ca4cab01eced604df896 --- /dev/null +++ b/src/asset_system/components.rs @@ -0,0 +1,198 @@ +use crate::fqpath::*; +use crate::utils::{ident_prefix, ident_suffix}; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{spanned::Spanned, Attribute, Data, DataStruct, DeriveInput, Field, Meta}; + +macro_rules! err_message { + ($spannable: expr, $($tok:tt)*) => { + return quote_spanned!( + $spannable.span() => + compile_error!($($tok)*); + ) + }; +} + +pub fn asset_system( + DeriveInput { + vis, + data, + ident, + attrs, + .. + }: DeriveInput, +) -> TokenStream { + let struct_data = match data { + Data::Struct(data_struct) => data_struct, + Data::Enum(e) => err_message!( + e.enum_token, + "Can't create an asset system from an enum type" + ), + Data::Union(u) => err_message!( + u.union_token, + "Can't create an asset system from a union type" + ), + }; + + let fields = define_fields(&struct_data); + let getters = define_getters(&struct_data); + let loader_properties = define_loader_properties(&attrs); + let loader = define_loader(&ident, &struct_data, &loader_properties); + + quote! { + #[derive(#FQDefault, #BevyResource)] + #vis struct #ident { + #fields + } + + impl #ident { + #getters + } + + #loader + } +} + +fn define_fields(data: &DataStruct) -> TokenStream { + data.fields + .iter() + .map(|Field { ident, ty, .. }| { + quote! { pub #ident: #FQHashMap<#FQString, #BevyHandle<#ty>>, } + }) + .collect() +} + +fn define_getters(data: &DataStruct) -> TokenStream { + data.fields + .iter() + .map(|Field { ident, ty, .. }| { + let string_name = ident.to_token_stream().to_string(); + + quote! { + pub fn #ident<T: ToString>(&self, name: T) -> #BevyHandle<#ty> { + let key = name.to_string(); + match self.#ident.get(&key) { + Some(handle) => handle.clone_weak(), + None => { + let keys = self.#ident.keys(); + panic!( + "\n\nTried to fetch {} asset with a missing key: {}.\nPossible keys: {}\n\n", + #string_name, + name.to_string(), + keys.map(|k| format!("'{}'", k)) + .collect::<#FQVec<#FQString>>() + .join(", ") + ) + } + } + } + } + }) + .collect() +} + +fn define_loader(ident: &Ident, data: &DataStruct, extra_props: &TokenStream) -> TokenStream { + let loader_name = ident_suffix(ident, "Loader"); + let load_functions = define_load_functions(data); + let load_generic = define_load_generic(); + + quote! { + #[derive(#BevySystemParam)] + pub struct #loader_name<'w> { + storage: #BevyResMut<'w, #ident>, + server: #BevyRes<'w, #BevyAssetServer>, + #extra_props + } + + impl <'w>#loader_name<'w> { + #load_generic + #load_functions + } + } +} + +fn define_load_generic() -> TokenStream { + quote! { + pub fn load_list<T, Load>(&mut self, list: #FQVec<(#FQString, #FQString)>, load: Load) -> #FQVec<#BevyHandle<T>> + where + T: #FQSend + #FQSync + #BevyTypePath + #BevyTypeUuid + 'static, + Load: Fn(&mut Self, #FQString, #FQString) -> #BevyHandle<T> + { + list + .iter() + .map(|(path, key)| load(self, path.clone(), key.clone())) + .collect() + } + } +} + +fn define_load_functions(data: &DataStruct) -> TokenStream { + data.fields + .iter() + .filter(|field| !field.attrs.iter().any(|attr| attr.path().is_ident("skip"))) + .map(|Field { ident, ty, .. }| { + let some_ident = match ident { + Some(id) => id, + None => err_message!(ty, "Found asset system type without a property name"), + }; + + let load_single = ident_prefix(some_ident, "load_"); + let load_single_static = ident_suffix(&load_single, "_static"); + let load_list = ident_suffix(&load_single, "_list"); + let load_list_static = ident_suffix(&load_list, "_static"); + + quote! { + pub fn #load_single(&mut self, path: impl #FQDisplay, name: impl #FQDisplay) -> #BevyHandle<#ty> { + let assets = vec![(path.to_string(), name.to_string())]; + let list = self.load_list(assets, |loader, path, key| { + let handle: #BevyHandle<#ty> = loader.server.load(&path); + loader.storage.#ident.insert(key, handle.clone()); + handle + }); + + list.first().expect("Failed to add asset").clone_weak() + } + pub fn #load_single_static(&mut self, path: &'static str, name: &'static str) -> #BevyHandle<#ty> { + let assets = vec![(path.to_string(), name.to_string())]; + let list = self.load_list(assets, |loader, path, key| { + let handle: #BevyHandle<#ty> = loader.server.load(&path); + loader.storage.#ident.insert(key, handle.clone()); + handle + }); + + list.first().expect("Failed to add static asset").clone_weak() + } + pub fn #load_list(&mut self, assets: #FQVec<(#FQString, #FQString)>) -> #FQVec<#BevyHandle<#ty>> { + self.load_list(assets, |loader, path, key| { + let handle: #BevyHandle<#ty> = loader.server.load(&path); + loader.storage.#ident.insert(key, handle.clone()); + handle + }) + } + pub fn #load_list_static(&mut self, assets: &'static [(&'static str, &'static str)]) -> #FQVec<#BevyHandle<#ty>> { + let assets = assets.iter().map(|(a, b)| (a.to_string(), b.to_string())).collect(); + self.load_list(assets, |loader, path, key| { + let handle: #BevyHandle<#ty> = loader.server.load(&path); + loader.storage.#ident.insert(key, handle.clone()); + handle + }) + } + } + }) + .collect() +} + +fn define_loader_properties(attrs: &[Attribute]) -> TokenStream { + attrs + .iter() + .filter(|attr| attr.path().is_ident("loader_property") && attr.meta.require_list().is_ok()) + .filter_map(|attr| { + let list = match &attr.meta { + Meta::List(list) => list, + _ => return None, + }; + let tokens = &list.tokens; + Some(quote!(#tokens,)) + }) + .collect() +} diff --git a/src/asset_system/mod.rs b/src/asset_system/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..f82a5a6f191844c4534c63bd137d988b31e7c66c --- /dev/null +++ b/src/asset_system/mod.rs @@ -0,0 +1,3 @@ +mod components; + +pub use components::asset_system; diff --git a/src/fqpath.rs b/src/fqpath.rs new file mode 100644 index 0000000000000000000000000000000000000000..1fa9809e33d2142e2f1783d3048adab197558300 --- /dev/null +++ b/src/fqpath.rs @@ -0,0 +1,53 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +macro_rules! fq { + ($name:ident => $($tokens:tt)*) => { + pub(crate) struct $name; + impl ToTokens for $name { + fn to_tokens(&self, tokens: &mut TokenStream) { + quote!($($tokens)*).to_tokens(tokens); + } + } + }; +} + +fq!(FQSerialize => ::serde::Serialize); +fq!(FQDeserialize => ::serde::Deserialize); +fq!(FQVec => ::std::vec::Vec); +fq!(FQString => ::std::string::String); +fq!(FQHashMap => ::std::collections::HashMap); +fq!(FQClone => ::core::clone::Clone); +fq!(FQDebug => ::core::fmt::Debug); +fq!(FQDisplay => ::core::fmt::Display); +fq!(FQDefault => ::core::default::Default); +fq!(FQSend => ::core::marker::Send); +fq!(FQSync => ::core::marker::Sync); +fq!(FQFrom => ::std::convert::From); + +fq!(ImportBevyPrelude => use ::bevy::prelude::*); + +fq!(BevyApp => ::bevy::app::App); +fq!(BevyPlugin => ::bevy::app::Plugin); +fq!(BevyUpdate => ::bevy::app::Update); +fq!(BevyRes => ::bevy::ecs::system::Res); +fq!(BevyResMut => ::bevy::ecs::system::ResMut); +fq!(BevyEventReader => ::bevy::ecs::event::EventReader); +fq!(BevySystemParam => ::bevy::ecs::system::SystemParam); +fq!(BevyResource => ::bevy::ecs::system::Resource); +fq!(BevyTypePath=> ::bevy::reflect::TypePath); +fq!(BevyTypeUuid => ::bevy::reflect::TypeUuid); +fq!(BevyDeref => ::bevy::prelude::Deref); +fq!(BevyDerefMut => ::bevy::prelude::DerefMut); +fq!(BevyHandle => ::bevy::asset::Handle); +fq!(BevyAssets => ::bevy::asset::Assets); +fq!(BevyAssetEvent => ::bevy::asset::AssetEvent); +fq!(BevyAssetLoader => ::bevy::asset::AssetLoader); +fq!(BevyBoxedFuture => ::bevy::asset::BoxedFuture); +fq!(BevyLoadContext => ::bevy::asset::LoadContext); +fq!(BevyLoadedAsset => ::bevy::asset::LoadedAsset); +fq!(BevyAssetServer => ::bevy::asset::AssetServer); +fq!(BevyAddAsset => ::bevy::asset::AddAsset); + +#[cfg(feature = "kayak")] +fq!(KayakWidget => ::kayak_ui::prelude::Widget); diff --git a/src/json_loader/components.rs b/src/json_loader/components.rs new file mode 100644 index 0000000000000000000000000000000000000000..2c8cff11c5f19170f2ece37db02c64b4d4841d60 --- /dev/null +++ b/src/json_loader/components.rs @@ -0,0 +1,197 @@ +use super::context::IdentContext; +use crate::fqpath::*; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, ToTokens}; +use syn::DeriveInput; + +pub fn json_loader(input: DeriveInput) -> TokenStream { + let context = match IdentContext::from_derive(input) { + Ok(context) => context, + Err(stream) => return stream, + }; + + let asset_set = define_loading_type(&context); + let index_type = define_index_type(&context); + let plugin = define_plugin(&context); + let loader = define_loader(&context); + let load_handler = define_load_handler(&context); + + quote! { + #asset_set + #index_type + #loader + #load_handler + #plugin + } +} + +pub fn default_asset_storage() -> TokenStream { + Ident::new("AssetHandles", Span::call_site()).to_token_stream() +} + +pub fn define_loading_type( + IdentContext { + asset_name, + set_name, + .. + }: &IdentContext, +) -> TokenStream { + quote! { + #[derive(#FQSerialize, #FQDeserialize)] + #[serde(untagged)] + enum #set_name { + One(#asset_name), + Many(#FQVec<#asset_name>) + } + } +} + +pub fn define_index_type( + IdentContext { + vis, + index_name, + asset_name, + uuid, + .. + }: &IdentContext, +) -> TokenStream { + quote! { + #[derive(#FQDebug, #BevyTypePath, #BevyTypeUuid, #BevyDeref, #BevyDerefMut)] + #[uuid = #uuid] + #vis struct #index_name(pub #FQHashMap<String, #BevyHandle<#asset_name>>); + } +} + +pub fn define_loader( + IdentContext { + vis, + loader_name, + index_name, + set_name, + id_field, + extension, + .. + }: &IdentContext, +) -> TokenStream { + quote! { + #vis struct #loader_name; + impl #BevyAssetLoader for #loader_name { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut #BevyLoadContext, + ) -> #BevyBoxedFuture<'a, ::anyhow::Result<()>> { + Box::pin(async { + let data: #set_name = ::serde_json::from_slice(bytes)?; + + let mut asset_map = #FQHashMap::new(); + match data { + #set_name::One(single_asset) => { + let asset_id = format!("{}", &single_asset.#id_field); + let handle = load_context.set_labeled_asset( + asset_id.as_str(), + #BevyLoadedAsset::new(single_asset), + ); + asset_map.insert(asset_id, handle); + } + #set_name::Many(asset_list) => { + for single_asset in asset_list.into_iter() { + let asset_id = format!("{}", &single_asset.#id_field); + let handle = load_context.set_labeled_asset( + asset_id.as_str(), + #BevyLoadedAsset::new(single_asset), + ); + asset_map.insert(asset_id, handle); + } + } + } + load_context.set_default_asset(#BevyLoadedAsset::new(#index_name(asset_map))); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &[#extension] + } + } + } +} + +pub fn define_plugin( + IdentContext { + vis, + asset_name, + index_name, + loader_name, + plugin_name, + handler_name, + .. + }: &IdentContext, +) -> TokenStream { + quote! { + #vis struct #plugin_name; + impl #BevyPlugin for #plugin_name { + fn build(&self, app: &mut #BevyApp) { + #BevyAddAsset::add_asset::<#asset_name>(app); + #BevyAddAsset::add_asset::<#index_name>(app); + #BevyAddAsset::add_asset_loader(app, #loader_name); + app.add_systems(#BevyUpdate, #handler_name); + } + } + } +} + +pub fn define_load_handler( + IdentContext { + vis, + index_name, + handler_name, + storage_type, + storage_asset_name, + storage_index_name, + .. + }: &IdentContext, +) -> TokenStream { + quote! { + #vis fn #handler_name( + mut events: #BevyEventReader<#BevyAssetEvent<#index_name>>, + mut asset_data: #BevyRes<#BevyAssets<#index_name>>, + mut asset_storage: #BevyResMut<#storage_type>, + ) { + let mut removed_containers = #FQVec::with_capacity(4); + let mut removed_assets = #FQVec::with_capacity(4); + + for event in events.iter() { + match event { + #BevyAssetEvent::Created { handle } | #BevyAssetEvent::Modified { handle } => { + if let Some(asset_container) = asset_data.get(handle) { + for (id, handle) in asset_container.iter() { + asset_storage.#storage_asset_name.insert(id.clone(), handle.clone()); + } + } + } + #BevyAssetEvent::Removed { handle } => { + asset_storage.#storage_index_name.iter().for_each(|(id, stored)| { + if stored == handle { + if let Some(asset_container) = asset_data.get(stored) { + for (id, _) in asset_container.iter() { + removed_assets.push(id.clone()); + } + removed_containers.push(id.clone()); + } + } + }); + } + } + } + + for id in removed_containers { + asset_storage.#storage_index_name.remove(&id); + } + + for id in removed_assets { + asset_storage.#storage_asset_name.remove(&id); + } + } + } +} diff --git a/src/json_loader/context.rs b/src/json_loader/context.rs new file mode 100644 index 0000000000000000000000000000000000000000..a6930d6557cc43c423a638184957c3b0a6713018 --- /dev/null +++ b/src/json_loader/context.rs @@ -0,0 +1,185 @@ +use super::components::default_asset_storage; +use crate::utils::{find_asset_id, find_attribute, ident_suffix, simple_attr_kv, snake_case_ident}; +use proc_macro2::{Ident, TokenStream}; +use quote::quote_spanned; +use syn::{spanned::Spanned, Data, DeriveInput, Visibility}; + +pub struct IdentContext { + pub vis: Visibility, + // -- Type names + pub asset_name: Ident, + pub index_name: Ident, + pub set_name: Ident, + pub plugin_name: Ident, + pub loader_name: Ident, + pub handler_name: Ident, + // -- Property names + pub id_field: Ident, + pub storage_type: TokenStream, + pub storage_asset_name: Ident, + pub storage_index_name: Ident, + // -- Data Values + pub uuid: TokenStream, + pub extension: TokenStream, +} + +macro_rules! span_err { + ($($tok:tt)*) => {Err(quote_spanned!($($tok)*))}; +} + +macro_rules! parse_macro_input_err { + ($tokenstream:ident as $ty:ty) => { + match ::syn::parse::<$ty>($tokenstream) { + Ok(data) => data, + Err(err) => { + return Err(proc_macro2::TokenStream::from(err.to_compile_error())); + } + } + }; +} + +impl IdentContext { + pub fn from_derive(input: DeriveInput) -> Result<Self, TokenStream> { + let data = match input.data { + Data::Struct(input_struct) => input_struct, + Data::Enum(_) => { + return span_err!( + input.ident.span() => + compile_error!("Can't create JSON Loader for Enum type"); + ); + } + Data::Union(_) => { + return span_err!( + input.ident.span() => + compile_error!("Can't create JSON Loader for Union type"); + ); + } + }; + + let loader_attr = match find_attribute(&input.attrs, "loader") { + Some(attribute) => attribute, + None => { + return span_err!( + input.ident.span() => + compile_error!("JSON assets requires a 'loader' attribute that specifies a file extension"); + ); + } + }; + + let attrs = simple_attr_kv(&loader_attr); + + if !attrs.contains_key("extension") { + return span_err!( + loader_attr.span() => + compile_error!("JSON assets must specify a file extension, in the form '#[loader(extension = \"json\")]'"); + ); + } + if !attrs.contains_key("uuid") { + return span_err!( + loader_attr.span() => + compile_error!("JSON assets must specify a uuid for the generated index asset, in the form '#[loader(uuid = \"asset-uuid\")]'"); + ); + } + + let storage_type = match attrs + .get("storage") + .map(|storage| storage.parse::<TokenStream>()) + { + Some(Ok(storage)) => storage, + Some(Err(_err)) => { + return span_err!( + loader_attr.span() => + compile_error!("Invalid storage type, must provide a valid path"); + ); + } + None => default_asset_storage(), + }; + + let DeriveInput { vis, ident, .. } = input; + + Ok(IdentContext { + vis, + + // Type Names + index_name: ident_suffix(&ident, "Index"), + loader_name: ident_suffix(&ident, "Loader"), + set_name: ident_suffix(&ident, "Set"), + plugin_name: ident_suffix(&ident, "Plugin"), + handler_name: ident_suffix(&snake_case_ident(&ident), "_load_handler"), + asset_name: ident.clone(), + + // Property Names + id_field: match find_asset_id(&data) { + Ok(field) => match field.ident { + Some(ident) => ident, + None => { + return span_err!( + field.span() => + compile_error!("asset_id field must have a name"); + ); + } + }, + Err(Some(stream)) => { + return Err(stream); + } + Err(None) => { + return span_err!( + ident.span() => + compile_error!("Asset must contain either a property named 'id', or a named property labelled with the 'asset_id' attribute"); + ); + } + }, + storage_type, + storage_index_name: match attrs.get("index_name") { + Some(index_property) => match index_property.parse::<TokenStream>() { + Ok(ident) => { + let ident = proc_macro::TokenStream::from(ident); + parse_macro_input_err!(ident as Ident) + } + Err(_) => { + return Err(quote_spanned!( + loader_attr.span() => + compile_error!("Failed to parse 'index_name' attribute"); + )); + } + }, + None => ident_suffix(&snake_case_ident(&ident), "_index"), + }, + storage_asset_name: match attrs.get("asset_name") { + Some(asset_property) => match asset_property.parse::<TokenStream>() { + Ok(ident) => { + let ident = proc_macro::TokenStream::from(ident); + parse_macro_input_err!(ident as Ident) + } + Err(_) => { + return span_err!( + loader_attr.span() => + compile_error!("Failed to parse 'asset_name' attribute"); + ); + } + }, + None => snake_case_ident(&ident), + }, + + // -- Data Values + uuid: match attrs["uuid"].parse::<TokenStream>() { + Ok(value) => value, + Err(_) => { + return span_err!( + loader_attr.span() => + compile_error!("Invalid uuid definition"); + ); + } + }, + extension: match attrs["extension"].parse::<TokenStream>() { + Ok(value) => value, + Err(_) => { + return span_err!( + loader_attr.span() => + compile_error!("Invalid extension definition"); + ); + } + }, + }) + } +} diff --git a/src/json_loader/mod.rs b/src/json_loader/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..a53230d2968caca286d052341656417b0a1d3226 --- /dev/null +++ b/src/json_loader/mod.rs @@ -0,0 +1,4 @@ +mod components; +mod context; + +pub use components::json_loader; diff --git a/src/kayak/mod.rs b/src/kayak/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..6377be408a17cf48704013d458231e982c506a2e --- /dev/null +++ b/src/kayak/mod.rs @@ -0,0 +1,3 @@ +mod widget; + +pub use widget::derive_widget; diff --git a/src/kayak/widget.rs b/src/kayak/widget.rs new file mode 100644 index 0000000000000000000000000000000000000000..a3fd7ba6fda3ad8e8c59df425ea1bb45dfb9a75e --- /dev/null +++ b/src/kayak/widget.rs @@ -0,0 +1,12 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use crate::fqpath::*; + +pub fn derive_widget(DeriveInput { ident, .. }: DeriveInput) -> TokenStream { + quote! { + #[automatically_derived] + impl #KayakWidget for #ident {} + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9af8195bf5e19d10c7b592b359ccd014149..d4b5c21763f15f7ca44be76682561d625136f5b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,256 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +use proc_macro::TokenStream; + +use syn::{parse_macro_input, DeriveInput}; + +pub(crate) mod fqpath; +pub(crate) mod utils; + +pub(crate) mod asset_system; +pub(crate) mod json_loader; +#[cfg(feature = "kayak")] +pub(crate) mod kayak; +pub(crate) mod std_traits; + +/// Generate loader and handler implementations for keyed JSON resources. The asset must implement `bevy::asset::Asset`, as well as +/// `serde::Deserialize` and `serde::Serialize` +/// +/// Container Attributes: +/// +/// - `loader(extension = "")`: *Required* - The file extension associated with this asset +/// - `loader(uuid = "")`: *Required* - The uuid to use for the generated index asset. This _must_ be different to the UUID provided for the asset itself +/// - `loader(storage = qualified::path::to::AssetStorage)`: The qualified path to an asset storage instance. Defaults to `AssetHandles` +/// - `loader(asset_name = my_asset)`: The property of the asset storage used to store the asset. Defaults to the snake case version of the struct name & must be a valid identifier +/// - `loader(index_name = my_asset_index)`: The property of the asset storage used to store an index of the asset types. Defaults to `asset_name`, suffixed with `_index` & must be a valid identifier +/// +/// Property Attributes: +/// +/// - `loader(asset_id)`: Label a property of the asset as the identifier, to be used as the key on which it will be matched. Must implement [std::fmt::Display]. +/// If not present, will be assumed to be a property named "id". If no such property is present, and this attribute is not specified, that is considered an error +/// +/// ## Examples +/// +/// By default, it is assumed that each resource is keyed by a property named "id", that the +/// storage +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use bevy::asset::Handle; +/// # use bevy::prelude::Resource; +/// # use bevy::reflect::{TypePath, TypeUuid}; +/// # use serde::{Deserialize, Serialize}; +/// # use micro_games_macros::JsonLoader; +/// +/// #[derive(Resource)] +/// pub struct AssetHandles { +/// simple_asset: HashMap<String, Handle<SimpleAsset>>, +/// simple_asset_index: HashMap<String, Handle<SimpleAssetIndex>>, +/// } +/// +/// #[derive(JsonLoader, TypePath, TypeUuid, Serialize, Deserialize)] +/// #[loader(extension = "satt", uuid = "00000000-0000-0000-0000-000000000000")] +/// #[uuid = "00000000-0000-0000-0000-000000000001"] +/// pub struct SimpleAsset { +/// id: String, +/// widget: usize, +/// } +/// +/// fn main() { +/// bevy::app::App::new() +/// .add_plugins(bevy::prelude::DefaultPlugins) +/// .add_plugins(SimpleAssetPlugin); +/// } +/// ``` +/// +/// Attributes can also be used to customise most of the default assumptions made by this derive: +/// +/// ```rust +/// # use std::collections::HashMap; +/// # use bevy::asset::Handle; +/// # use bevy::prelude::Resource; +/// # use bevy::reflect::{TypePath, TypeUuid}; +/// # use serde::{Deserialize, Serialize}; +/// # use micro_games_macros::JsonLoader; +/// +/// #[derive(JsonLoader, TypePath, TypeUuid, Serialize, Deserialize)] +/// #[loader( +/// extension = "asset.json", uuid = "00000000-0000-0000-0000-000000000000", +/// storage = inner_module::SimpleAssetLocator, +/// asset_name = some_asset_prop, index_name = set_of_assets +/// )] +/// #[uuid = "00000000-0000-0000-0000-000000000001"] +/// pub struct MyAsset { +/// /// The asset identifier needs to implement [std::fmt::Display] +/// #[asset_id] +/// uniq_ident: usize, +/// } +/// +/// pub mod inner_module { +/// # use bevy::prelude::{Handle, Resource}; +/// # use std::collections::HashMap; +/// +/// #[derive(Resource)] +/// pub struct SimpleAssetLocator { +/// pub some_asset_prop: HashMap<String, Handle<super::MyAsset>>, +/// pub set_of_assets: HashMap<String, Handle<super::MyAssetIndex>>, +/// } +/// } +/// +/// fn main() { +/// bevy::app::App::new() +/// .add_plugins(bevy::prelude::DefaultPlugins) +/// .add_plugins(MyAssetPlugin); +/// } +/// ``` +#[proc_macro_derive(JsonLoader, attributes(loader, asset_id))] +pub fn json_loader(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + json_loader::json_loader(input).into() +} + +/// Convert a mapping of name -> Asset type into an asset loading system +/// +/// ## Examples +/// +/// Convert a struct that represents mappings between names and asset types +/// into an asset system to handle loading, and storing asset handles +/// +/// ```rust +/// use bevy::prelude::{Image, Res, Resource}; +/// use micro_games_macros::asset_system; +/// +/// #[asset_system] +/// pub struct AssetHandles { +/// my_asset: Image, +/// } +/// +/// pub fn loading_system(mut loader: AssetHandlesLoader) { +/// loader.load_my_asset("path/to/asset.png", "Asset"); +/// } +/// pub fn use_asset_system(assets: Res<AssetHandles>) { +/// let handle = assets.my_asset("Asset"); +/// } +/// ``` +/// +/// Annotate any property with a `skip` attribute to omit it from the resulting loader +/// +/// ```rust +/// use bevy::prelude::{Image, TextureAtlas}; +/// use micro_games_macros::asset_system; +/// +/// #[asset_system] +/// pub struct AssetHandles { +/// image: Image, +/// #[skip] +/// spritesheet: TextureAtlas +/// } +/// +/// impl <'w>AssetHandlesLoader<'w> { +/// pub fn load_spritesheet(&mut self, path: String, name: String) { +/// let image_handle = self.load_image(path, name.clone()); +/// // let sheet_handle = .. more code .. +/// // self.spritesheet.insert(name, sheet_handle); +/// } +/// } +/// ``` +/// +/// Include extra properties in the generated loader with one or more `loader_property` attributes. +/// The included property must be a `SystemParam`, and has access to the `'w` lifetime +/// +/// ```rust +/// use bevy::prelude::{EventWriter, Event, Image, TextureAtlas, Assets, ResMut, Vec2, Handle}; +/// use micro_games_macros::{asset_system, loader_property}; +/// +/// #[derive(Event)] +/// pub struct LoadingEvent { +/// pub event_id: usize, +/// } +/// +/// #[asset_system] +/// #[loader_property(pub load_events: EventWriter<'w, LoadingEvent>)] +/// #[loader_property(pub sheets: ResMut<'w, Assets<TextureAtlas>>)] +/// pub struct AssetHandles { +/// image: Image, +/// #[skip] +/// spritesheet: TextureAtlas +/// } +/// +/// impl <'w>AssetHandlesLoader<'w> { +/// pub fn load_spritesheet(&mut self, path: String, name: String) -> Handle<TextureAtlas> { +/// let image_handle = self.load_image(path, name.clone()); +/// let sheet = TextureAtlas::new_empty(image_handle, Vec2::ZERO); +/// let sheet_handle = self.sheets.add(sheet); +/// +/// self.storage.spritesheet.insert(name.clone(), sheet_handle.clone()); +/// self.load_events.send(LoadingEvent { event_id: 123 }); +/// +/// sheet_handle +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn asset_system(_: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + asset_system::asset_system(input).into() } -#[cfg(test)] -mod tests { - use super::*; +/// Marker attribute, used exclusively by other proc macros +#[proc_macro_attribute] +#[doc(hidden)] +pub fn skip(_: TokenStream, input: TokenStream) -> TokenStream { + input +} + +/// Marker attribute, used exclusively by other proc macros +#[proc_macro_attribute] +#[doc(hidden)] +pub fn loader_property(_: TokenStream, input: TokenStream) -> TokenStream { + input +} + +#[proc_macro_derive(Widget)] +#[cfg(feature = "kayak")] +pub fn derive_kayak_wigdet(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + kayak::derive_widget(input).into() +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +/// Derive [std::convert::From] for single property tuple or named struct types +/// +/// ## Examples +/// +/// `FromInner` can derive `From` for single-property tuple structs +/// +/// ```rust +/// use micro_games_macros::FromInner; +/// +/// #[derive(PartialEq, Eq, Debug, FromInner)] +/// struct MyValue(i32); +/// +/// let value = 123i32.into(); +/// +/// assert_eq!(MyValue(123), value); +/// assert_eq!(MyValue::from(123), value); +/// ``` +/// +/// As well as single-property named structs +/// +/// +/// ```rust +/// use micro_games_macros::FromInner; +/// +/// #[derive(PartialEq, Eq, Debug, FromInner)] +/// struct MyValue { +/// foo_bar: usize, +/// } +/// +/// let value = MyValue::from(2000); +/// +/// assert_eq!(value.foo_bar, 2000); +/// assert_eq!(MyValue { foo_bar: 2000 }, value); +/// assert_eq!(MyValue::from(2000), value); +/// ``` +#[proc_macro_derive(FromInner)] +pub fn derive_from_inner(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + std_traits::from_inner::derive(input).into() } diff --git a/src/std_traits/from_inner.rs b/src/std_traits/from_inner.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba6b7113cc66ac0daba9e4f0550c93f9c3528b10 --- /dev/null +++ b/src/std_traits/from_inner.rs @@ -0,0 +1,87 @@ +use crate::fqpath::*; +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned; +use syn::{Data, DeriveInput, Fields, FieldsNamed, FieldsUnnamed}; + +fn exactly_one<'a, T: Spanned>(mut it: impl Iterator<Item = &'a T>) -> Result<&'a T, Option<Span>> { + let first = it.next(); + let second = it.next(); + + if let Some(second) = second { + Err(Some(second.span())) + } else if let Some(first) = first { + Ok(first) + } else { + Err(None) + } +} + +fn tuple_variant(input: &DeriveInput, fields: &FieldsUnnamed) -> TokenStream { + match exactly_one(fields.unnamed.iter()) { + Ok(field) => { + let base_type = &input.ident; + let field_type = &field.ty; + + quote! { + #[automatically_derived] + impl #FQFrom<#field_type> for #base_type { + fn from(value: #field_type) -> #base_type { + #base_type(value) + } + } + } + } + Err(Some(field)) => { + quote_spanned!(field.span() => compile_error!("Can't derive 'From' for a tuple struct with more than one inner property");) + } + Err(None) => { + quote_spanned!(input.ident.span() => compile_error!("Must have at least one property to derive 'From'");) + } + } +} + +fn named_variant(input: &DeriveInput, fields: &FieldsNamed) -> TokenStream { + match exactly_one(fields.named.iter()) { + Ok(field) => { + let base_type = &input.ident; + let field_type = &field.ty; + let name = &field.ident; + + quote! { + #[automatically_derived] + impl #FQFrom<#field_type> for #base_type { + fn from(value: #field_type) -> #base_type { + #base_type { + #name: value, + } + } + } + } + } + Err(Some(field)) => { + quote_spanned!(field.span() => compile_error!("Can't derive 'From' for a tuple struct with more than one inner property");) + } + Err(None) => { + quote_spanned!(input.ident.span() => compile_error!("Must have at least one property to derive 'From'");) + } + } +} + +pub fn derive(input: DeriveInput) -> TokenStream { + match &input.data { + Data::Struct(at) => match &at.fields { + Fields::Unnamed(fields) => tuple_variant(&input, fields), + Fields::Named(fields) => named_variant(&input, fields), + Fields::Unit => { + quote_spanned!(input.ident.span() => compile_error!("'From' struct must contain at least one field");) + } + }, + Data::Enum(en) => { + quote_spanned!(en.enum_token.span() => compile_error!("Deriving 'From' for enum is not supported");) + } + Data::Union(un) => { + quote_spanned!(un.union_token.span() => compile_error!("Deriving 'From' for union is not supported");) + } + } +} diff --git a/src/std_traits/mod.rs b/src/std_traits/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..dc20209f83319ad5e56c5e57264cb8463248399c --- /dev/null +++ b/src/std_traits/mod.rs @@ -0,0 +1 @@ +pub mod from_inner; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..bcd173f9e8c8621583cdce176fe6914c35f19ba0 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,183 @@ +use proc_macro2::{Ident, TokenStream, TokenTree}; +use quote::{quote, quote_spanned, ToTokens}; +use std::collections::HashMap; +use std::fmt::Display; +use syn::{spanned::Spanned, Attribute, DataStruct, Field, Meta, MetaNameValue, Visibility}; + +/// Convert a list of characters to snake case +pub fn snake_case(chars: impl Iterator<Item = char>) -> String { + let mut acc = String::new(); + let mut prev = '_'; + for ch in chars { + if ch.is_uppercase() && prev != '_' { + acc.push('_'); + } + acc.push(ch); + prev = ch; + } + acc.to_lowercase() +} + +/// Convert an identifier name to snake case +pub fn snake_case_ident(ident: &Ident) -> Ident { + Ident::new(&snake_case(ident.to_string().chars()), ident.span()) +} + +pub fn ident_suffix(ident: &Ident, suffix: impl Display) -> Ident { + Ident::new(&format!("{}{}", ident, suffix), ident.span()) +} +pub fn ident_prefix(ident: &Ident, prefix: impl Display) -> Ident { + Ident::new(&format!("{}{}", prefix, ident), ident.span()) +} + +macro_rules! to_str { + ($expr: expr) => { + $expr.to_token_stream().to_string() + }; +} + +#[allow(unused)] +pub fn unquote(value: String) -> String { + match (value.starts_with('"'), value.ends_with('"')) { + (true, true) => value[1..value.len() - 1].to_string(), + (false, true) => value[..value.len() - 1].to_string(), + (true, false) => value[1..].to_string(), + (false, false) => value, + } +} + +pub fn find_asset_id(data: &DataStruct) -> Result<Field, Option<TokenStream>> { + let mut found = None; + for field in &data.fields { + if field.ident.as_ref().map(|id| format!("{}", id)) == Some("id".into()) { + if found.is_some() { + return Err(Some(quote_spanned!( + field.span() => + compile_error!("Asset must have only one of: property named 'id', property labelled with 'asset_id' attribute"); + ))); + } else { + found = Some(field); + } + continue; + } + + for attr in &field.attrs { + if attr.path().is_ident("asset_id") { + if found.is_some() { + return Err(Some(quote_spanned!( + field.span() => + compile_error!("Asset must have only one of: property named 'id', property labelled with 'asset_id' attribute"); + ))); + } else { + found = Some(field); + } + } + } + } + + found.cloned().ok_or_else(|| None) +} + +pub fn find_attribute(attrs: &Vec<Attribute>, path: impl ToString) -> Option<Attribute> { + let attr_name = path.to_string(); + for attr in attrs { + if attr.path().is_ident(&attr_name) { + return Some(attr.clone()); + } + } + None +} + +pub fn simple_attr_kv(attr: &Attribute) -> HashMap<String, String> { + let mut attr_kv = HashMap::new(); + + match &attr.meta { + Meta::NameValue(mnv) => { + attr_kv.insert(to_str!(mnv.path), to_str!(mnv.value)); + } + Meta::List(ml) => { + let mut buffer = Vec::with_capacity(4); + for tok in ml.tokens.clone() { + match tok { + TokenTree::Punct(punct) if punct.as_char() == ',' => { + let tokens = std::mem::take(&mut buffer) + .iter() + .fold(String::default(), |access, current| { + format!("{}{}", access, current) + }); + + let mnv = tokens + .parse::<TokenStream>() + .expect("Invalid list of tokens for attribute"); + + let mnv = syn::parse::<MetaNameValue>(mnv.into()) + .expect("Invalid list of tokens for attribute"); + + attr_kv.insert(to_str!(mnv.path), to_str!(mnv.value)); + } + other => { + buffer.push(other); + } + } + } + + if !buffer.is_empty() { + let tokens = std::mem::take(&mut buffer) + .iter() + .fold(String::default(), |access, current| { + format!("{}{}", access, current) + }); + + let mnv = tokens + .parse::<TokenStream>() + .expect("Invalid list of tokens for attribute"); + + let mnv = syn::parse::<MetaNameValue>(mnv.into()) + .expect("Invalid list of tokens for attribute"); + + attr_kv.insert(to_str!(mnv.path), to_str!(mnv.value)); + } + } + Meta::Path(pt) => { + attr_kv.insert(to_str!(pt), String::new()); + } + } + + attr_kv +} + +#[allow(unused)] +pub fn module_wrapped( + vis: &Visibility, + mod_name: impl ToTokens, + inner: TokenStream, +) -> TokenStream { + quote! { + mod #mod_name { + #inner + } + #vis use #mod_name::*; + } +} + +#[cfg(test)] +mod tests { + use crate::utils::{snake_case, unquote}; + use test_case::test_case; + + #[test_case("MyIdentifier", "my_identifier")] + #[test_case("already_snaked", "already_snaked")] + #[test_case("Some_Identifier", "some_identifier")] + fn it_converts_identifiers(input: &str, expected: &str) { + assert_eq!(snake_case(input.chars()), expected); + } + + #[test_case(r#""Foo""#, "Foo")] + #[test_case(r#""Left only"#, "Left only")] + #[test_case(r#"Right only""#, "Right only")] + #[test_case("No Quotes", "No Quotes")] + #[test_case(r#"Middle " Unaffected"#, r#"Middle " Unaffected"#)] + fn it_removes_quotes(input: &str, expected: &str) { + assert_eq!(unquote(input.to_string()), expected); + } +}