From 59dfb89ee655b564c5823b477870c413c40d38c5 Mon Sep 17 00:00:00 2001 From: Daan Vanoverloop Date: Mon, 12 Sep 2022 09:49:16 +0200 Subject: [PATCH] Procedural macros --- Cargo.lock | 35 ++++ Cargo.toml | 3 +- core/Cargo.toml | 5 + core/src/components.rs | 75 +++++++ .../components/event.rs => core/src/edit.rs | 190 +++++++++++++----- core/src/event.rs | 102 +++++++++- core/src/lib.rs | 6 + macros/Cargo.toml | 17 ++ macros/src/edit.rs | 0 macros/src/lib.rs | 172 ++++++++++++++++ web/Cargo.toml | 2 +- web/dist/index.html | 6 +- web/src/components/mod.rs | 8 +- web/src/pages/events.rs | 13 +- 14 files changed, 560 insertions(+), 74 deletions(-) create mode 100644 core/src/components.rs rename web/src/components/event.rs => core/src/edit.rs (66%) create mode 100644 macros/Cargo.toml create mode 100644 macros/src/edit.rs create mode 100644 macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 10e9b06..dc828f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -274,6 +274,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.0" @@ -469,6 +478,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1136,11 +1156,26 @@ dependencies = [ name = "lan_party_core" version = "0.1.0" dependencies = [ + "displaydoc", + "lan_party_macros", "paste", "rocket", "schemars", "serde", + "sycamore", "thiserror", + "web-sys", +] + +[[package]] +name = "lan_party_macros" +version = "0.1.0" +dependencies = [ + "convert_case", + "paste", + "quote", + "sycamore", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bf921cd..cbd24df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "backend", "core", - "web" + "web", + "macros" ] diff --git a/core/Cargo.toml b/core/Cargo.toml index 295b8cd..426c65f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,10 +9,15 @@ edition = "2021" serde = ["dep:serde"] openapi = ["dep:schemars"] rocket = ["dep:rocket", "serde"] +sycamore = ["dep:sycamore", "dep:web-sys", "dep:lan_party_macros"] [dependencies] schemars = { version = "0.8", optional = true } serde = { version = "1", optional = true } rocket = { version = "0.5.0-rc.2", features = ["json"], optional = true } +sycamore = { version = "0.8.1", features = ["serde", "suspense"], optional = true } +web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"], optional = true } +lan_party_macros = { path = "../macros", optional = true } paste = "1" thiserror = "1.0" +displaydoc = "0.2" diff --git a/core/src/components.rs b/core/src/components.rs new file mode 100644 index 0000000..8a22eec --- /dev/null +++ b/core/src/components.rs @@ -0,0 +1,75 @@ +//use log::debug; +use sycamore::prelude::*; +use web_sys::Event; + +#[derive(Prop)] +pub struct ButtonProps { + pub onclick: F, + #[builder(default)] + pub text: String, + #[builder(default)] + pub icon: String, +} + +#[component] +pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: ButtonProps) -> View { + let mut icon_class = String::from("mdi "); + + if !props.icon.is_empty() { + icon_class.push_str(&props.icon); + } + + view! { cx, + button(on:click=props.onclick) { + span(class=icon_class) + span { (props.text) } + } + } +} + +#[derive(Prop)] +pub struct TableProps<'a, G: Html> { + pub headers: Vec, + + pub children: Children<'a, G>, +} + +#[component] +pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View { + let children = props.children.call(cx); + + view! { cx, + table { + thead { + tr { + (View::new_fragment(props.headers.iter().cloned().map(|header| view! { cx, + th(scope="col") { + (header) + } + }).collect())) + } + } + tbody { + (children) + } + } + } +} + +#[derive(Prop)] +pub struct BlockProps<'a, G: Html> { + pub title: String, + pub children: Children<'a, G>, +} + +#[component] +pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View { + let children = props.children.call(cx); + + view! { cx, + details { + summary { (props.title) } + p { (children) } + } + } +} diff --git a/web/src/components/event.rs b/core/src/edit.rs similarity index 66% rename from web/src/components/event.rs rename to core/src/edit.rs index d890cfb..f335761 100644 --- a/web/src/components/event.rs +++ b/core/src/edit.rs @@ -6,16 +6,22 @@ use std::{ }; use crate::components::Block; -use lan_party_core::event::{ - free_for_all_game::FreeForAllGameSpec, team_game::TeamGameSpec, test::TestSpec, EventSpec, - EventTypeSpec, -}; -use log::debug; use paste::paste; -use sycamore::{builder::prelude::*, prelude::*}; +use sycamore::prelude::*; -use super::{BlockProps, Button, ButtonProps}; +use crate::components::{BlockProps, Button}; +pub mod prelude { + pub use super::{Edit, EditProps, Editable, Editor, IntoEdit}; + pub use crate::{ + components::Block, edit_enum, edit_fields, edit_struct, editable, link_fields, + link_variants, + }; + pub use paste::paste; + pub use sycamore::prelude::*; +} + +#[macro_export] macro_rules! editable { ($type:ty => $editor:ty) => { impl<'a, G: Html> Editable<'a, G> for $type { @@ -24,6 +30,7 @@ macro_rules! editable { }; } +#[macro_export] macro_rules! edit_fields { ($cx:ident, $(($name:expr, $prop:expr)),* $(,)?) => { view! { $cx, @@ -37,6 +44,7 @@ macro_rules! edit_fields { }; } +#[macro_export] macro_rules! link_fields { ($cx:ident, $($field:ident),* $(,)? => $state:ident as $t:ident) => { $(let $field = create_signal($cx, $state.get_untracked().$field.clone());)* @@ -50,6 +58,7 @@ macro_rules! link_fields { }; } +#[macro_export] macro_rules! edit_struct { ($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => { paste! { @@ -72,8 +81,9 @@ macro_rules! edit_struct { }; } +#[macro_export] macro_rules! link_variants { - ($cx:ident, $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)? => $state:ident as $t:ident) => { + ($cx:ident, $selected:ident => $(($var_name:ident = $variant:ident: $var_type:ty)),* $(,)? => $state:ident as $t:ident) => { let $selected = create_signal($cx, String::from("0")); $(let $var_name = if let $t::$variant(v) = $state.get_untracked().as_ref().clone() { @@ -83,17 +93,18 @@ macro_rules! link_variants { };)* create_effect($cx, || { - debug!("{:#?}", $selected.get()); match $selected.get().as_str() { - $(stringify!($index) => $state.set($t::$variant($var_name.get().as_ref().clone())),)* - _ => unreachable!() + $(stringify!($var_name) => $state.set($t::$variant($var_name.get().as_ref().clone())),)* + //_ => unreachable!() + _ => {} } }); }; } +#[macro_export] macro_rules! edit_enum { - ($enum:ident => $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => { + ($enum:ident => $selected:ident => $($var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => { paste! { pub struct [<$enum Edit>]; @@ -102,18 +113,19 @@ macro_rules! edit_enum { let state = props.state; link_variants!(cx, $selected => - $($index: $var_name = $variant: $var_type,)* + $(($var_name = $variant: $var_type),)* => state as $enum ); view! { cx, Block(title=stringify!($enum).to_string()) { select(bind:value=$selected) { - $(option(value={stringify!($index)}, selected=$index==0) { (stringify!($variant)) })* + $(option(value={stringify!($var_name)}, selected=true) { (stringify!($variant)) })* } (match $selected.get().as_str() { - $(stringify!($index) => $var_name.edit(cx),)* - _ => unreachable!() + $(stringify!($var_name) => $var_name.edit(cx),)* + //_ => unreachable!() + _ => view! { cx, } }) } } @@ -172,18 +184,6 @@ pub trait Editable<'a, G: Html>: Sized { type Editor: Editor<'a, G, Self>; } -edit_struct!(EventSpec => ("Name", name), ("Description", description), ("Event type", event_type)); - -edit_enum!(EventTypeSpec => selected => - 0: test = Test: TestSpec, - 1: team_game = TeamGame: TeamGameSpec, - 2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec -); - -edit_struct!(TestSpec => ("Number of players", num_players)); -edit_struct!(TeamGameSpec => ("Teams", teams), ("Win rewards", win_rewards), ("Lose rewards", lose_rewards)); -edit_struct!(FreeForAllGameSpec => ("Participants", participants), ("Win rewards", win_rewards), ("Lose rewards", lose_rewards)); - pub struct StringEdit; impl<'a, G: Html> Editor<'a, G, String> for StringEdit { @@ -209,6 +209,10 @@ where } } +impl<'a, G: Html, T: for<'b> Editable<'b, G>> Editable<'a, G> for Option { + type Editor = StubEdit; +} + pub struct InputEdit; impl<'a, G: Html, T> Editor<'a, G, T> for InputEdit @@ -258,7 +262,7 @@ pub struct VecEdit; impl<'a, G, T, I> Editor<'a, G, I> for VecEdit where G: Html, - T: Editable<'a, G> + Clone + PartialEq + Default + 'a, + T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a, I: IntoIterator + FromIterator + Clone, { fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View { @@ -287,13 +291,39 @@ where let onadd = move |_| vec.modify().push(create_signal(cx, T::default())); + Block( + cx, + BlockProps { + title: "List".into(), + children: Children::new(cx, move |_| { + view! { cx, + //Block(title="List".into()) { + div { + Indexed( + iterable=vec, + view=|cx: BoundedScope<'_, 'a>, x: &'a Signal| { + view! { cx, + (x.edit(cx)) + br() + } + }, + ) + Button(onclick=onadd, text="Add new".into(), icon="mdi-plus".into()) + } + //} + } + }), + }, + ) + + /* Block( cx, BlockProps { title: "List".into(), children: Children::new(cx, move |_| { div() - .dyn_c(move || { + .dyn_c_scoped(move |cx| { View::new_fragment( vec.get() .as_ref() @@ -315,13 +345,14 @@ where }), }, ) + */ } } impl<'a, G, T> Editable<'a, G> for Vec where G: Html, - T: Editable<'a, G> + Clone + PartialEq + Default + 'a, + T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a, { type Editor = VecEdit; } @@ -329,7 +360,7 @@ where impl<'a, G, T> Editable<'a, G> for HashSet where G: Html, - T: Editable<'a, G> + Clone + PartialEq + Default + Hash + Eq + 'a, + T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a, { type Editor = VecEdit; } @@ -339,15 +370,15 @@ where G: Html, K: Clone + Hash + Eq, V: Clone, - (K, V): Editable<'a, G> + Clone + PartialEq + Default + Hash + Eq + 'a, + (K, V): for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a, { type Editor = VecEdit; } pub struct TupleEdit; -impl<'a, 'b, G: Html, A: Editable<'a, G> + Clone, B: Editable<'a, G> + Clone> Editor<'a, G, (A, B)> - for TupleEdit +impl<'a, G: Html, A: for<'b> Editable<'b, G> + Clone, B: for<'b> Editable<'b, G> + Clone> + Editor<'a, G, (A, B)> for TupleEdit { fn edit(cx: Scope<'a>, props: EditProps<'a, (A, B)>) -> View { let state = props.state; @@ -362,30 +393,91 @@ impl<'a, 'b, G: Html, A: Editable<'a, G> + Clone, B: Editable<'a, G> + Clone> Ed .set((a.get().as_ref().clone(), b.get().as_ref().clone())) }); - Block( - cx, - BlockProps { - title: "Tuple".into(), - children: Children::new(cx, move |_| { - div() - .dyn_c(move || a.edit(cx)) - .dyn_c(move || b.edit(cx)) - .view(cx) - }), - }, - ) + view! { cx, + Block(title="Tuple".into()) { + (a.edit(cx)) + (b.edit(cx)) + } + } } } +pub struct LabeledEdit { + _t: PhantomData, +} + +impl<'a, G, T, U> Editor<'a, G, U> for LabeledEdit +where + G: Html, + T: for<'b> Editable<'b, G> + Clone + 'a, + U: Into> + From> + Clone, +{ + fn edit(cx: Scope<'a>, props: EditProps<'a, U>) -> View { + let cloned: U = props.state.get_untracked().as_ref().clone(); + let state: WithLabel = cloned.into(); + let label = state.label.clone(); + let inner = create_signal(cx, state.inner.clone()); + + { + let label = label.clone(); + create_effect(cx, move || { + props.state.set( + WithLabel { + label: label.clone(), + inner: inner.get().as_ref().clone(), + } + .into(), + ) + }); + } + + let label = create_signal(cx, label); + + view! { cx, + div { + (label.get()) + (inner.edit(cx)) + } + } + } +} + +#[derive(Clone)] +pub struct WithLabel { + label: String, + inner: T, +} + +impl<'a, G: Html, T: for<'b> Editable<'b, G> + Clone + 'a> Editable<'a, G> for WithLabel { + type Editor = LabeledEdit; +} + impl<'a, G, A, B> Editable<'a, G> for (A, B) where G: Html, - A: Editable<'a, G> + Clone, - B: Editable<'a, G> + Clone, + A: for<'b> Editable<'b, G> + Clone, + B: for<'b> Editable<'b, G> + Clone, { type Editor = TupleEdit; } +pub struct User(String); + +impl From> for User { + fn from(l: WithLabel) -> Self { + User(l.inner) + } +} + +impl From for WithLabel { + fn from(u: User) -> Self { + WithLabel { + label: "User".into(), + inner: u.0, + } + } +} + #[derive(Default, Clone, Debug)] pub struct Test { inner: TestInner, diff --git a/core/src/event.rs b/core/src/event.rs index 60756ba..57033d9 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -1,4 +1,8 @@ +#[cfg(feature = "sycamore")] +use crate::edit::prelude::*; use crate::util::PartyError; +#[cfg(feature = "sycamore")] +use lan_party_macros::WebEdit; use paste::paste; #[cfg(feature = "openapi")] use schemars::JsonSchema; @@ -51,6 +55,7 @@ impl Event { #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct EventSpec { pub name: String, pub description: String, @@ -66,6 +71,7 @@ macro_rules! events { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum EventTypeSpec { $($name($module::[<$name Spec>]),)* } @@ -76,6 +82,7 @@ macro_rules! events { #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum EventUpdate { $($name($module::[<$name Update>]),)* } @@ -86,6 +93,7 @@ macro_rules! events { #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum EventType { $($name($module::$name),)* } @@ -149,9 +157,10 @@ pub trait EventTrait { pub mod test { use super::*; - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct Test { pub num_players: i64, } @@ -159,13 +168,15 @@ pub mod test { #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct TestSpec { pub num_players: i64, } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct TestUpdate { pub win_game: bool, } @@ -205,9 +216,10 @@ pub mod team_game { *, }; - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct TeamGame { /// Map of teams with a name as key and an array of players as value pub teams: HashMap>, @@ -219,6 +231,7 @@ pub mod team_game { #[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct TeamGameSpec { /// Map of teams with a name as key and an array of players as value pub teams: HashMap>, @@ -239,21 +252,38 @@ pub mod team_game { pub lose_rewards: Vec, } + #[derive(Clone, Debug, Default)] + #[cfg_attr(feature = "openapi", derive(JsonSchema))] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] + pub struct TeamGameUpdateSetTeam { + pub team: String, + pub members: Vec, + } + #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum TeamGameUpdateInner { /// Add or replace a team with the given name and array of members - SetTeam { team: String, members: Vec }, + SetTeam(TeamGameUpdateSetTeam), /// Remove team with given name RemoveTeam(String), } + impl Default for TeamGameUpdateInner { + fn default() -> Self { + Self::SetTeam(TeamGameUpdateSetTeam::default()) + } + } + #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum TeamGameFfaInheritedUpdate { /// Change the ranking and scores Ranking(FreeForAllGameUpdateRanking), @@ -261,10 +291,17 @@ pub mod team_game { Rewards(FreeForAllGameUpdateRewards), } + impl Default for TeamGameFfaInheritedUpdate { + fn default() -> Self { + Self::Ranking(FreeForAllGameUpdateRanking::default()) + } + } + #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum TeamGameUpdate { /// Team specific updates Team(TeamGameUpdateInner), @@ -272,6 +309,12 @@ pub mod team_game { Ffa(TeamGameFfaInheritedUpdate), } + impl Default for TeamGameUpdate { + fn default() -> Self { + TeamGameUpdate::Team(TeamGameUpdateInner::default()) + } + } + impl EventTrait for TeamGame { type Spec = TeamGameSpec; type Update = TeamGameUpdate; @@ -300,12 +343,12 @@ pub mod team_game { } }, TeamGameUpdate::Team(update) => match update { - TeamGameUpdateInner::SetTeam { team, members } => { + TeamGameUpdateInner::SetTeam(u) => { self.ffa_game .apply_update(FreeForAllGameUpdate::Participants( - FreeForAllGameUpdateParticipants::AddParticipant(team.clone()), + FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()), ))?; - self.teams.insert(team, members); + self.teams.insert(u.team, u.members); Ok(()) } TeamGameUpdateInner::RemoveTeam(team) => { @@ -347,6 +390,7 @@ pub mod free_for_all_game { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum FreeForAllGameRanking { /// Ranking of participants by user name or team name (first element is first place, second element is second /// place, etc.) @@ -355,6 +399,12 @@ pub mod free_for_all_game { Scores(HashMap), } + impl Default for FreeForAllGameRanking { + fn default() -> Self { + Self::Ranking(Vec::default()) + } + } + impl FreeForAllGameRanking { pub fn is_valid(&self, participants: &HashSet) -> bool { match self { @@ -364,9 +414,10 @@ pub mod free_for_all_game { } } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct FreeForAllGame { /// Ranking of participants by user name or team name (first element is first place, second element is second /// place, etc.) @@ -388,6 +439,7 @@ pub mod free_for_all_game { #[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub struct FreeForAllGameSpec { /// Array of user ids that participate in the game pub participants: HashSet, @@ -405,6 +457,7 @@ pub mod free_for_all_game { #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum FreeForAllGameUpdateRanking { /// Replace the current ranking with the given ranking SetRanking(FreeForAllGameRanking), @@ -413,9 +466,16 @@ pub mod free_for_all_game { ScoreDelta(HashMap), } + impl Default for FreeForAllGameUpdateRanking { + fn default() -> Self { + Self::SetRanking(FreeForAllGameRanking::default()) + } + } + #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum FreeForAllGameUpdateRewards { /// Set rewards for winning the game SetWinRewards(Vec), @@ -424,9 +484,16 @@ pub mod free_for_all_game { SetLoseRewards(Vec), } + impl Default for FreeForAllGameUpdateRewards { + fn default() -> Self { + Self::SetWinRewards(Vec::default()) + } + } + #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum FreeForAllGameUpdateParticipants { /// Set list of participants participating in the game SetParticipants(HashSet), @@ -438,10 +505,17 @@ pub mod free_for_all_game { RemoveParticipant(String), } + impl Default for FreeForAllGameUpdateParticipants { + fn default() -> Self { + Self::SetParticipants(HashSet::default()) + } + } + #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] + #[cfg_attr(feature = "sycamore", derive(WebEdit))] pub enum FreeForAllGameUpdate { /// Change the ranking and scores Ranking(FreeForAllGameUpdateRanking), @@ -451,6 +525,12 @@ pub mod free_for_all_game { Participants(FreeForAllGameUpdateParticipants), } + impl Default for FreeForAllGameUpdate { + fn default() -> Self { + Self::Ranking(FreeForAllGameUpdateRanking::default()) + } + } + impl EventTrait for FreeForAllGame { type Spec = FreeForAllGameSpec; type Update = FreeForAllGameUpdate; @@ -560,3 +640,9 @@ impl Default for EventTypeSpec { Self::Test(test::TestSpec::default()) } } + +impl Default for EventUpdate { + fn default() -> Self { + Self::Test(test::TestUpdate::default()) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index ac22b11..39158db 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -2,4 +2,10 @@ pub mod event; pub mod user; pub mod util; +#[cfg(feature = "sycamore")] +pub mod edit; + +#[cfg(feature = "sycamore")] +pub mod components; + pub use util::PartyError; diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..656f9c1 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "lan_party_macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = { version = "1.0", features = ["full", "extra-traits"] } +quote = "1.0" +sycamore = { version = "0.8.1", features = ["serde", "suspense"] } +paste = "1.0" +convert_case = "0.6" +#lan_party_core = { path = "../core", features = ["sycamore"] } diff --git a/macros/src/edit.rs b/macros/src/edit.rs new file mode 100644 index 0000000..e69de29 diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..f506675 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,172 @@ +mod edit; + +use convert_case::{Case, Casing}; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::Fields; + +#[proc_macro_derive(WebEdit)] +pub fn web_edit(tokens: TokenStream) -> TokenStream { + let input: syn::DeriveInput = syn::parse(tokens).unwrap(); + + let name = input.ident; + let edit_ident = format_ident!("{}Edit", name); + + let editable = quote! { + pub struct #edit_ident; + + impl<'a, G: Html> Editable<'a, G> for #name { + type Editor = #edit_ident; + } + }; + + let derived = match input.data { + syn::Data::Struct(s) => { + let fields = s.fields.iter().map(|f| { + let name = f + .ident + .as_ref() + .expect("each struct field must be named") + .clone(); + let name_str = name.to_string(); + (name_str, name) + }); + + let fields_view = fields.clone().map(|(name_str, name)| { + let title = name_str.to_case(Case::Title); + quote! { + p { + label { (#title) } + (#name.edit(cx)) + } + } + }); + + let signals = fields.clone().map(|(_name_str, name)| { + quote! { + let #name = create_signal(cx, state.get_untracked().#name.clone()); + } + }); + + let effect_fields = fields.clone().map(|(_name_str, name)| { + quote! { + #name: #name.get().as_ref().clone() + } + }); + + quote! { + impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident { + fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View { + let state = props.state; + + #(#signals)* + + create_effect(cx, || { + state.set(#name { + #(#effect_fields,)* + ..Default::default() + }); + }); + + view! { cx, + Block(title=stringify!(#name).to_string()) { + #(#fields_view)* + } + } + } + } + } + } + syn::Data::Enum(e) => { + //dbg!(&e); + + let variants = e.variants.iter().map(|v| { + let variant = v.ident.clone(); + + let inner = match &v.fields { + Fields::Unnamed(u) => { + u.unnamed.first().expect("the should be a field").ty.clone() + } + _ => unimplemented!(), + }; + + let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); + (variant_lower, variant, inner) + }); + + let first = variants.clone().next().unwrap().0; + + let options = variants.clone().map(|(lower, variant, _inner)| { + let selected = first == lower; + quote! { option(value={stringify!(#lower)}, selected=#selected) { (stringify!(#variant)) } } + }); + + let view_match = variants.clone().map(|(lower, _variant, _inner)| { + let lower_str = format!("{}", lower); + + quote! { + #lower_str => #lower.edit(cx) + } + }); + + let signals = variants.clone().map(|(lower, variant, inner)| { + quote! { + let #lower = if let #name::#variant(v) = state.get_untracked().as_ref().clone() { + create_signal(cx, v.clone()) + } else { + create_signal(cx, <#inner>::default()) + }; + } + }); + + let effect_match = variants.clone().map(|(lower, variant, _inner)| { + let lower_str = format!("{}", lower); + + quote! { + #lower_str => state.set(#name::#variant(#lower.get().as_ref().clone())) + } + }); + + quote! { + impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident { + fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View { + let state = props.state; + + let selected = create_signal(cx, String::from("0")); + + #(#signals)* + + create_effect(cx, || { + match selected.get().as_str() { + #(#effect_match,)* + _ => {} + //_ => unreachable!() + } + }); + + view! { cx, + Block(title=stringify!(#name).to_string()) { + select(bind:value=selected) { + #(#options)* + } + (match selected.get().as_str() { + #(#view_match,)* + _ => view! { cx, } + //_ => unreachable!() + }) + } + } + } + } + } + } + _ => unimplemented!(), + }; + + println!("{}", &derived.to_string()); + TokenStream::from(quote! { + #derived + + #editable + }) +} diff --git a/web/Cargo.toml b/web/Cargo.toml index 3eeaf6b..8dccf32 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -11,7 +11,7 @@ yew = "0.19" yew-router = "0.16" #yew-router = { git = "https://github.com/yewstack/yew" } web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"] } -lan_party_core = { path = "../core", features = ["serde"] } +lan_party_core = { path = "../core", features = ["serde", "sycamore"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4" serde = "1" diff --git a/web/dist/index.html b/web/dist/index.html index 2e94d33..2f5209d 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -6,7 +6,7 @@ LAN Party - - + + - \ No newline at end of file + \ No newline at end of file diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs index a0d672f..310d6e7 100644 --- a/web/src/components/mod.rs +++ b/web/src/components/mod.rs @@ -1,9 +1,5 @@ -pub mod event; - -use log::debug; use sycamore::prelude::*; use web_sys::Event; -use yew::use_effect; #[derive(Prop)] pub struct ButtonProps { @@ -61,8 +57,8 @@ pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View { #[derive(Prop)] pub struct BlockProps<'a, G: Html> { - title: String, - children: Children<'a, G>, + pub title: String, + pub children: Children<'a, G>, } #[component] diff --git a/web/src/pages/events.rs b/web/src/pages/events.rs index 314e9e9..94bf768 100644 --- a/web/src/pages/events.rs +++ b/web/src/pages/events.rs @@ -1,20 +1,21 @@ -use crate::components::event::{IntoEdit, Test}; -use lan_party_core::event::EventSpec; +use lan_party_core::{ + edit::IntoEdit, + event::{EventSpec, EventUpdate}, +}; use log::debug; use sycamore::prelude::*; -use crate::components::{ - event::{Edit, EditProps}, - Block, Button, -}; +use crate::components::{Block, Button}; #[component] pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View { let event_spec = create_signal(cx, EventSpec::default()); + let event_update = create_signal(cx, EventUpdate::default()); view! { cx, Block(title="Create new event".into()) { (event_spec.edit(cx)) + (event_update.edit(cx)) Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_spec.get())) } }