From 32fb25168209f08d4a49bd3c8e443989bea4d10c Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Sun, 22 Oct 2023 02:11:10 +0100
Subject: [PATCH] Import macros from advent_realms

---
 Cargo.toml                     |  33 ++++-
 src/asset_system/components.rs | 198 +++++++++++++++++++++++++
 src/asset_system/mod.rs        |   3 +
 src/fqpath.rs                  |  53 +++++++
 src/json_loader/components.rs  | 197 +++++++++++++++++++++++++
 src/json_loader/context.rs     | 185 +++++++++++++++++++++++
 src/json_loader/mod.rs         |   4 +
 src/kayak/mod.rs               |   3 +
 src/kayak/widget.rs            |  12 ++
 src/lib.rs                     | 262 +++++++++++++++++++++++++++++++--
 src/std_traits/from_inner.rs   |  87 +++++++++++
 src/std_traits/mod.rs          |   1 +
 src/utils.rs                   | 183 +++++++++++++++++++++++
 13 files changed, 1209 insertions(+), 12 deletions(-)
 create mode 100644 src/asset_system/components.rs
 create mode 100644 src/asset_system/mod.rs
 create mode 100644 src/fqpath.rs
 create mode 100644 src/json_loader/components.rs
 create mode 100644 src/json_loader/context.rs
 create mode 100644 src/json_loader/mod.rs
 create mode 100644 src/kayak/mod.rs
 create mode 100644 src/kayak/widget.rs
 create mode 100644 src/std_traits/from_inner.rs
 create mode 100644 src/std_traits/mod.rs
 create mode 100644 src/utils.rs

diff --git a/Cargo.toml b/Cargo.toml
index 34e0fd7..505f129 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 0000000..5d9c4e6
--- /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 0000000..f82a5a6
--- /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 0000000..1fa9809
--- /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 0000000..2c8cff1
--- /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 0000000..a6930d6
--- /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 0000000..a53230d
--- /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 0000000..6377be4
--- /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 0000000..a3fd7ba
--- /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 7d12d9a..d4b5c21 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 0000000..ba6b711
--- /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 0000000..dc20209
--- /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 0000000..bcd173f
--- /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);
+	}
+}
-- 
GitLab