diff --git a/Cargo.lock b/Cargo.lock index e626722..1555a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,8 +376,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +dependencies = [ + "darling_core 0.14.1", + "darling_macro 0.14.1", ] [[package]] @@ -394,13 +404,38 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +dependencies = [ + "darling_core 0.14.1", "quote", "syn", ] @@ -1173,6 +1208,7 @@ name = "lan_party_macros" version = "0.1.0" dependencies = [ "convert_case", + "darling 0.14.1", "paste", "quote", "sycamore", @@ -1184,7 +1220,9 @@ name = "lan_party_web" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "console_log", + "futures", "gloo-timers", "js-sys", "lan_party_core", @@ -1965,7 +2003,7 @@ version = "0.8.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f94d1ffe41472e08463d7a2674f1db04dc4df745285e8369b33d3cfd6b0308" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", "rocket_http", @@ -2213,7 +2251,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", "syn", diff --git a/backend/src/api/event.rs b/backend/src/api/event.rs index b5c229c..115ae43 100644 --- a/backend/src/api/event.rs +++ b/backend/src/api/event.rs @@ -21,7 +21,7 @@ pub async fn apply_outcome(outcome: &EventOutcome, db: &Connection) -> Resul for (player, reward) in outcome.points.iter() { db.users() .update_one( - doc! { "id": player }, + doc! { "id": player.to_string() }, doc! { "$inc": { "score": reward } }, None, ) diff --git a/core/src/components.rs b/core/src/components.rs index 8a22eec..c8751fe 100644 --- a/core/src/components.rs +++ b/core/src/components.rs @@ -67,7 +67,7 @@ pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View { let children = props.children.call(cx); view! { cx, - details { + details(open=true) { summary { (props.title) } p { (children) } } diff --git a/core/src/edit.rs b/core/src/edit.rs index 20e1eee..b92e525 100644 --- a/core/src/edit.rs +++ b/core/src/edit.rs @@ -5,7 +5,11 @@ use std::{ str::FromStr, }; -use crate::components::Block; +use crate::{ + components::Block, + util::{ContextOptions, WithContext}, + view::Viewable, +}; use log::debug; use paste::paste; use sycamore::prelude::*; @@ -263,7 +267,7 @@ pub struct VecEdit; impl<'a, G, T, I> Editor<'a, G, I> for VecEdit where G: Html, - T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a, + T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + std::fmt::Debug + 'a, I: IntoIterator + FromIterator + Clone, { fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View { @@ -294,6 +298,7 @@ where let onremove = move |item: &'a Signal| { move |_| { let cloned = vec.get().as_ref().clone(); + debug!("{:#?}", item.get()); vec.set( cloned .into_iter() @@ -332,7 +337,7 @@ where impl<'a, G, T> Editable<'a, G> for Vec where G: Html, - T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a, + T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + std::fmt::Debug + 'a, { type Editor = VecEdit; } @@ -340,7 +345,7 @@ where impl<'a, G, T> Editable<'a, G> for HashSet where G: Html, - T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a, + T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + std::fmt::Debug + 'a, { type Editor = VecEdit; } @@ -350,7 +355,8 @@ where G: Html, K: Clone + Hash + Eq, V: Clone, - (K, V): for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a, + (K, V): + for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + std::fmt::Debug + 'a, { type Editor = VecEdit; } @@ -458,6 +464,58 @@ impl From for WithLabel { } } +pub struct ContextEdit; + +impl<'a, G, Ctx, T> Editor<'a, G, WithContext> for ContextEdit +where + G: Html, + Ctx: ContextOptions + 'static, + T: FromStr + ToString + Default + Clone + PartialEq, +{ + fn edit(cx: Scope<'a>, props: EditProps<'a, WithContext>) -> View { + let value = create_signal(cx, props.state.get_untracked().to_string()); + + let ctx = use_context::(cx); + + create_effect(cx, || { + debug!( + "{}", + (*value.get()) + .parse::() + .unwrap_or(T::default()) + .to_string() + ); + props + .state + .set((*value.get()).parse::().unwrap_or(T::default()).into()) + }); + + view! { cx, + select(bind:value=value) { + Indexed( + iterable=ctx.options(cx), + view=move |cx: BoundedScope<'_, 'a>, x| { + let x = create_ref(cx, x); + view! { cx, + option(value=x.to_string()) { (x.to_string()) } + } + }, + ) + } + " " + } + } +} + +impl<'a, G, Ctx, T> Editable<'a, G> for WithContext +where + G: Html, + Ctx: ContextOptions + 'static, + T: FromStr + ToString + Default + Clone + PartialEq, +{ + type Editor = ContextEdit; +} + #[derive(Default, Clone, Debug)] pub struct Test { inner: TestInner, diff --git a/core/src/event.rs b/core/src/event.rs index 8ea4f1b..8a17539 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -1,10 +1,13 @@ #[cfg(feature = "sycamore")] use crate::edit::prelude::*; -use crate::util::PartyError; #[cfg(feature = "sycamore")] use crate::view::prelude::*; +use crate::{ + state::Users, + util::{PartyError, WithContext}, +}; #[cfg(feature = "sycamore")] -use lan_party_macros::{web_view_attr, WebEdit, WebView}; +use lan_party_macros::{WebEdit, WebView}; use paste::paste; #[cfg(feature = "openapi")] use schemars::JsonSchema; @@ -15,11 +18,14 @@ use std::{ hash::Hash, }; +type User = WithContext; +type Team = String; + #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] -pub enum Ranking { +pub enum Ranking { /// Ranking of participants by user name or team name (first element is first place, second element is second /// place, etc.) Ranking(Vec), @@ -27,13 +33,13 @@ pub enum Ranking { Scores(HashMap), } -impl Default for Ranking { +impl Default for Ranking { fn default() -> Self { Self::Ranking(Vec::default()) } } -impl Ranking { +impl Ranking { pub fn is_valid(&self, participants: &HashSet) -> bool { match self { Self::Ranking(v) => v.iter().all(|p| participants.contains(p)), @@ -42,11 +48,19 @@ impl Ranking { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct EventOutcome { - pub points: HashMap, + pub points: HashMap, +} + +impl Default for EventOutcome { + fn default() -> Self { + Self { + points: HashMap::::default(), + } + } } /// # Event @@ -56,7 +70,7 @@ pub struct EventOutcome { #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebView))] -#[cfg_attr(feature = "sycamore", web_view_attr(title = "name"))] +#[cfg_attr(feature = "sycamore", view(title = "name"))] pub struct Event { /// Has this event concluded? #[cfg_attr(feature = "serde", serde(default))] @@ -239,13 +253,15 @@ pub mod test { fn outcome(&self) -> EventOutcome { let mut points = HashMap::new(); - points.insert("420".into(), self.num_players); + points.insert("420".to_string().into(), self.num_players); EventOutcome { points } } } } pub mod team_game { + use std::ops::Deref; + use super::{ free_for_all_game::{FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate}, *, @@ -257,7 +273,7 @@ pub mod team_game { #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TeamGame { /// Map of teams with a name as key and an array of players as value - pub teams: HashMap>, + pub teams: HashMap>, #[cfg_attr(feature = "serde", serde(flatten))] pub ffa_game: FreeForAllGame, @@ -269,7 +285,7 @@ pub mod team_game { #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TeamGameSpec { /// Map of teams with a name as key and an array of players as value - pub teams: HashMap>, + pub teams: HashMap>, /// Rewards for winning the game (first element for first place, second element for second /// place, etc.) @@ -292,16 +308,8 @@ pub mod team_game { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TeamGameUpdateSetTeam { - pub team: String, - pub members: Vec, - } - - #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] - #[cfg_attr(feature = "openapi", derive(JsonSchema))] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] - pub struct Team { - name: String, + pub team: Team, + pub members: Vec, } #[derive(Clone, Debug)] @@ -313,11 +321,11 @@ pub mod team_game { /// Add or replace a team with the given name and array of members SetTeam(TeamGameUpdateSetTeam), /// Remove team with given name - RemoveTeam(String), + RemoveTeam(Team), /// Replace the current ranking with the given ranking - SetRanking(Ranking), + SetRanking(Ranking), /// If the current ranking is of type `Scores`, apply the given score deltas - ScoreDelta(HashMap), + ScoreDelta(HashMap), /// Set rewards for winning the game SetWinRewards(Vec), /// Set rewards for losing the game @@ -330,13 +338,24 @@ pub mod team_game { } } + impl From> for Ranking { + fn from(r: Ranking) -> Self { + match r { + Ranking::Scores(s) => { + Ranking::Scores(s.into_iter().map(|(k, v)| (k.into(), v)).collect()) + } + Ranking::Ranking(s) => Ranking::Ranking(s.into_iter().map(|k| k.into()).collect()), + } + } + } + impl EventTrait for TeamGame { type Spec = TeamGameSpec; type Update = TeamGameUpdate; fn from_spec(spec: TeamGameSpec) -> Self { let ffa_game_spec = FreeForAllGameSpec { - participants: spec.teams.keys().cloned().collect(), + participants: spec.teams.keys().map(|k| User::from(k.clone())).collect(), win_rewards: spec.win_rewards, lose_rewards: spec.lose_rewards, }; @@ -351,10 +370,12 @@ pub mod team_game { match update { TeamGameUpdate::SetRanking(x) => self .ffa_game - .apply_update(FreeForAllGameUpdate::SetRanking(x)), - TeamGameUpdate::ScoreDelta(x) => self - .ffa_game - .apply_update(FreeForAllGameUpdate::ScoreDelta(x)), + .apply_update(FreeForAllGameUpdate::SetRanking(x.into())), + TeamGameUpdate::ScoreDelta(x) => { + self.ffa_game.apply_update(FreeForAllGameUpdate::ScoreDelta( + x.into_iter().map(|(k, v)| (k.into(), v)).collect(), + )) + } TeamGameUpdate::SetWinRewards(x) => self .ffa_game .apply_update(FreeForAllGameUpdate::SetWinRewards(x)), @@ -363,13 +384,23 @@ pub mod team_game { .apply_update(FreeForAllGameUpdate::SetLoseRewards(x)), TeamGameUpdate::SetTeam(u) => { self.ffa_game - .apply_update(FreeForAllGameUpdate::AddParticipant(u.team.clone()))?; - self.teams.insert(u.team, u.members); + .apply_update(FreeForAllGameUpdate::AddParticipant( + u.team.clone().into(), + ))?; + self.teams.insert( + u.team, + u.members + .into_iter() + .map(|t| (t.clone()).deref().clone().into()) + .collect(), + ); Ok(()) } TeamGameUpdate::RemoveTeam(team) => { self.ffa_game - .apply_update(FreeForAllGameUpdate::RemoveParticipant(team.clone()))?; + .apply_update(FreeForAllGameUpdate::RemoveParticipant( + team.clone().into(), + ))?; self.teams.remove(&team); Ok(()) } @@ -382,10 +413,10 @@ pub mod team_game { let mut points = HashMap::new(); for (team, reward) in ffa_outcome.points.iter() { - if let Some(team) = self.teams.get(team) { + if let Some(team) = self.teams.get(&*team.to_string()) { for player in team { - let score = points.get(player).unwrap_or(&0); - points.insert(player.clone(), score + reward); + let score = points.get(&player.clone().into()).unwrap_or(&0); + points.insert(User::from(player.clone()), score + reward); } } } @@ -400,6 +431,7 @@ pub mod free_for_all_game { use super::*; + /* #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -407,6 +439,7 @@ pub mod free_for_all_game { pub struct User { name: String, } + */ #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] @@ -415,7 +448,7 @@ pub mod free_for_all_game { pub struct FreeForAllGame { /// Ranking of participants by user name or team name (first element is first place, second element is second /// place, etc.) - pub ranking: Option>, + pub ranking: Option>, /// Specification of the game #[cfg_attr(feature = "serde", serde(flatten))] @@ -436,7 +469,7 @@ pub mod free_for_all_game { #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct FreeForAllGameSpec { /// Array of user ids that participate in the game - pub participants: HashSet, + pub participants: HashSet, /// Rewards for winning the game (first element for first place, second element for second /// place, etc.) @@ -455,24 +488,24 @@ pub mod free_for_all_game { #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum FreeForAllGameUpdate { /// Replace the current ranking with the given ranking - SetRanking(Ranking), + SetRanking(Ranking), /// If the current ranking is of type `Scores`, apply the given score deltas - ScoreDelta(HashMap), + ScoreDelta(HashMap), /// Set rewards for winning the game SetWinRewards(Vec), /// Set rewards for losing the game SetLoseRewards(Vec), /// Set list of participants participating in the game - SetParticipants(HashSet), + SetParticipants(HashSet), /// Add participant by name - AddParticipant(String), + AddParticipant(User), /// Remove participant by name - RemoveParticipant(String), + RemoveParticipant(User), } impl Default for FreeForAllGameUpdate { fn default() -> Self { - Self::AddParticipant(String::new()) + Self::AddParticipant(String::new().into()) } } diff --git a/core/src/lib.rs b/core/src/lib.rs index c6d979a..0cb8516 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,5 @@ pub mod event; +pub mod state; pub mod user; pub mod util; diff --git a/core/src/state.rs b/core/src/state.rs new file mode 100644 index 0000000..825838c --- /dev/null +++ b/core/src/state.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "sycamore")] +use sycamore::prelude::*; + +use crate::{event::Event, user::User}; + +#[cfg(feature = "sycamore")] +use crate::util::ContextOptions; + +#[cfg(feature = "sycamore")] +#[derive(Clone, PartialEq, Default)] +pub struct Users(pub RcSignal>); + +#[cfg(not(feature = "sycamore"))] +#[derive(Clone, PartialEq, Default)] +pub struct Users; + +#[cfg(feature = "sycamore")] +impl Users { + pub fn get(&self) -> &RcSignal> { + &self.0 + } +} + +#[cfg(feature = "sycamore")] +impl ContextOptions for Users { + fn options<'a>(&'a self, cx: Scope<'a>) -> &'a ReadSignal> { + self.get() + .map(cx, |v| v.iter().map(|u| u.name.clone()).collect()) + } +} + +#[cfg(feature = "sycamore")] +#[derive(Clone, PartialEq, Default)] +pub struct Events(pub RcSignal>); + +#[cfg(not(feature = "sycamore"))] +#[derive(Clone, PartialEq, Default)] +pub struct Events; + +#[cfg(feature = "sycamore")] +impl Events { + pub fn get(&self) -> &RcSignal> { + &self.0 + } +} + +#[cfg(feature = "sycamore")] +impl ContextOptions for Events { + fn options<'a>(&'a self, cx: Scope<'a>) -> &'a ReadSignal> { + self.get() + .map(cx, |v| v.iter().map(|u| u.name.clone()).collect()) + } +} diff --git a/core/src/util.rs b/core/src/util.rs index 0ada2ec..6b9c98b 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -4,6 +4,12 @@ use rocket::FromFormField; use schemars::JsonSchema; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use std::{ + fmt::Debug, + hash::Hash, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; use thiserror::Error; #[derive(Error, Debug)] @@ -42,3 +48,86 @@ impl ToString for Ordering { .into() } } + +#[derive(Clone, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[serde(transparent)] +pub struct WithContext { + #[serde(skip)] + _ctx: PhantomData, + inner: T, +} + +#[cfg(feature = "openapi")] +impl JsonSchema for WithContext +where + T: JsonSchema, +{ + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + T::json_schema(gen) + } + + fn schema_name() -> String { + T::schema_name() + } +} + +impl Debug for WithContext +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.fmt(f) + } +} + +impl Hash for WithContext +where + T: Hash, +{ + fn hash(&self, state: &mut H) { + self.inner.hash(state) + } +} + +impl PartialEq for WithContext +where + T: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for WithContext where T: Eq {} + +impl Deref for WithContext { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for WithContext { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl From for WithContext { + fn from(inner: T) -> Self { + Self { + _ctx: PhantomData, + inner, + } + } +} + +#[cfg(feature = "sycamore")] +use sycamore::reactive::{ReadSignal, Scope}; + +#[cfg(feature = "sycamore")] +pub trait ContextOptions { + fn options<'a>(&'a self, cx: Scope<'a>) -> &'a ReadSignal>; +} diff --git a/core/src/view.rs b/core/src/view.rs index 0b2fbab..e134626 100644 --- a/core/src/view.rs +++ b/core/src/view.rs @@ -2,6 +2,7 @@ use std::{ collections::{HashMap, HashSet}, hash::Hash, marker::PhantomData, + ops::Deref, }; use sycamore::view::IntoView as _; @@ -14,7 +15,10 @@ pub mod prelude { pub use sycamore::prelude::*; } -use crate::components::{Block, BlockProps}; +use crate::{ + components::{Block, BlockProps}, + util::WithContext, +}; #[macro_export] macro_rules! viewable { @@ -204,3 +208,17 @@ impl<'a, G: Html, T: for<'b> Viewable<'b, G> + Clone> Viewer<'a, G, Option> f impl<'a, G: Html, T: for<'b> Viewable<'b, G> + Clone> Viewable<'a, G> for Option { type Viewer = OptionView; } + +pub struct ContextView; + +impl<'a, G: Html, Ctx, T: for<'b> Viewable<'b, G> + Clone> Viewer<'a, G, WithContext> + for ContextView +{ + fn view(cx: Scope<'a>, props: ViewProps<'a, WithContext>) -> View { + props.state.deref().view(cx) + } +} + +impl<'a, G: Html, Ctx, T: for<'b> Viewable<'b, G> + Clone> Viewable<'a, G> for WithContext { + type Viewer = ContextView; +} diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 656f9c1..9b65f93 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -14,4 +14,5 @@ quote = "1.0" sycamore = { version = "0.8.1", features = ["serde", "suspense"] } paste = "1.0" convert_case = "0.6" +darling = "0.14" #lan_party_core = { path = "../core", features = ["sycamore"] } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 0030314..309198c 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -6,6 +6,9 @@ use syn::{ LifetimeDef, Lit, MetaNameValue, Path, PredicateType, Type, TypeParam, }; +#[macro_use] +extern crate darling; + enum ParsedAttribute { Documentation(Documentation), View(ViewAttribute), @@ -50,7 +53,7 @@ impl Documentation { impl ViewAttribute { fn parse(attr: &Attribute) -> ViewAttribute { - if !attr.path.is_ident("web_view_attr") { + if !attr.path.is_ident("view") { return Self::None; } @@ -96,7 +99,7 @@ impl ParsedAttribute { fn parse(attr: &Attribute) -> ParsedAttribute { match attr.path.get_ident() { Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)), - Some(i) if i.to_string() == "web_view_attr" => Self::View(ViewAttribute::parse(attr)), + Some(i) if i.to_string() == "view" => Self::View(ViewAttribute::parse(attr)), Some(i) if i.to_string() == "serde" => Self::Serde(SerdeAttribute::parse(attr)), _ => Self::None, } @@ -154,11 +157,6 @@ impl Attributes { } } -#[proc_macro_attribute] -pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { - item -} - struct ItemProps { name: Ident, attributes: Attributes, @@ -494,32 +492,6 @@ fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 { } } -fn enum_fields<'a>(e: &'a DataEnum) -> impl Iterator + 'a { - 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 Attributes { - title, - title_field: _, - description, - .. - } = Attributes::parse(&v.attrs); - let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); - EnumVariant { - variant_lower, - variant, - inner, - title, - description, - } - }) -} - fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 { let ItemProps { name, @@ -667,7 +639,7 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream { TokenStream::from(res) } -#[proc_macro_derive(WebView)] +#[proc_macro_derive(WebView, attributes(view))] pub fn web_view(tokens: TokenStream) -> TokenStream { let input: syn::DeriveInput = syn::parse(tokens).unwrap(); diff --git a/start_mongodb.sh b/start_mongodb.sh index c3d7bc6..e52f5f0 100755 --- a/start_mongodb.sh +++ b/start_mongodb.sh @@ -1,7 +1,7 @@ #!/bin/sh podman run -d --rm --name lan_party_db \ - -v lan-party-db:/data/db:z \ + -v lan-party-db-2:/data/db:z \ -p "27017:27017" \ -e MONGO_INITDB_ROOT_USERNAME=root \ -e MONGO_INITDB_ROOT_PASSWORD=example \ diff --git a/web/Cargo.toml b/web/Cargo.toml index 75182c0..81bf772 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -28,5 +28,5 @@ reqwasm = "0.5" console_log = "0.2" log = "0.4" gloo-timers = "0.2" - - +async-trait = "0.1" +futures = "0.3" diff --git a/web/dist/index.html b/web/dist/index.html index 9404915..0ca1993 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/main.rs b/web/src/main.rs index 2e3ad3b..af20c66 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -4,9 +4,11 @@ pub mod state; pub mod util; use components::messages::{Messages, Messenger}; +use futures::join; +use lan_party_core::state::{Events, Users}; use pages::{EventsPage, UsersPage}; -use state::{Events, Users}; -use sycamore::prelude::*; +use state::{EventsExt, UsersExt}; +use sycamore::{futures::spawn_local_scoped, prelude::*}; use sycamore_router::{HistoryIntegration, Route, Router}; #[derive(Route)] @@ -74,6 +76,23 @@ fn main() { let events = Events::default(); provide_context(cx, events); + spawn_local_scoped(cx, async move { + let (users_res, events_res) = join!( + use_context::(cx).load(), + use_context::(cx).load(), + ); + use_context::(cx).add_result( + users_res, + Option::::None, + Some("failed to load users"), + ); + use_context::(cx).add_result( + events_res, + Option::::None, + Some("failed to load events"), + ); + }); + /* let messages = use_context::(cx); diff --git a/web/src/pages/events.rs b/web/src/pages/events.rs index b348ef4..16c90df 100644 --- a/web/src/pages/events.rs +++ b/web/src/pages/events.rs @@ -11,9 +11,10 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use crate::{ components::{messages::Messenger, Button, Modal, Table}, - state::Events, + state::{EventsExt, UsersExt}, util::api_request, }; +use lan_party_core::state::{Events, Users}; pub enum Msg { Reload, @@ -113,7 +114,7 @@ pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View { view=move |cx, (k, v)| { view! { cx, tr { - td { (k.clone()) } + td { (*k.clone()) } td { (v.clone()) } } } diff --git a/web/src/pages/users.rs b/web/src/pages/users.rs index 3eed0ab..10b7ab9 100644 --- a/web/src/pages/users.rs +++ b/web/src/pages/users.rs @@ -1,7 +1,8 @@ use crate::{ components::{messages::Messenger, Button, Table}, - state::Users, + state::{EventsExt, UsersExt}, }; +use lan_party_core::state::{Events, Users}; use sycamore::{futures::spawn_local_scoped, prelude::*}; #[component] diff --git a/web/src/state.rs b/web/src/state.rs index 51ff4fb..c25afdd 100644 --- a/web/src/state.rs +++ b/web/src/state.rs @@ -1,6 +1,8 @@ use anyhow::{anyhow, Result}; +use async_trait::async_trait; use lan_party_core::{ event::{Event, EventSpec, EventUpdate}, + state::{Events, Users}, user::User, }; use reqwasm::http::Method; @@ -8,15 +10,20 @@ use sycamore::prelude::*; use crate::util::api_request; -#[derive(Clone, PartialEq, Default)] -pub struct Users(RcSignal>); +#[async_trait(?Send)] +pub trait UsersExt { + async fn load(&self) -> Result<()>; -impl Users { - pub fn get(&self) -> &RcSignal> { - &self.0 - } + async fn delete(&self, name: &str) -> Result<()>; - pub async fn load(&self) -> Result<()> { + async fn update_score(&self, name: &str, score: i64) -> Result<()>; + + async fn add(&self, name: &str) -> Result<()>; +} + +#[async_trait(?Send)] +impl UsersExt for Users { + async fn load(&self) -> Result<()> { self.0.set( api_request::<_, Vec>( Method::GET, @@ -29,7 +36,7 @@ impl Users { Ok(()) } - pub async fn delete(&self, name: &str) -> Result<()> { + async fn delete(&self, name: &str) -> Result<()> { let users_ref = self.0.get(); let user: &User = users_ref.iter().find(|user| user.name == name).unwrap(); api_request::<_, ()>( @@ -48,7 +55,7 @@ impl Users { Ok(()) } - pub async fn update_score(&self, name: &str, score: i64) -> Result<()> { + async fn update_score(&self, name: &str, score: i64) -> Result<()> { let users_ref = self.0.get(); let user: &User = users_ref.iter().find(|user| &user.name == name).unwrap(); api_request::<_, ()>( @@ -71,22 +78,27 @@ impl Users { Ok(()) } - pub async fn add(&self, name: &str) -> Result<()> { + async fn add(&self, name: &str) -> Result<()> { let user = api_request::<&str, User>(Method::POST, "/user", Some(name)).await?; self.0.modify().push(user.ok_or(anyhow!("missing body"))?); Ok(()) } } -#[derive(Clone, PartialEq, Default)] -pub struct Events(RcSignal>); +#[async_trait(?Send)] +pub trait EventsExt { + async fn load(&self) -> Result<()>; -impl Events { - pub fn get(&self) -> &RcSignal> { - &self.0 - } + async fn add(&self, event_spec: EventSpec) -> Result<()>; - pub async fn load(&self) -> Result<()> { + async fn update_event(&self, event_name: &str, event_update: EventUpdate) -> Result<()>; + + async fn delete(&self, event_name: &str) -> Result<()>; +} + +#[async_trait(?Send)] +impl EventsExt for Events { + async fn load(&self) -> Result<()> { self.0.set( api_request::<_, Vec>(Method::GET, "/event", Option::<()>::None) .await @@ -95,7 +107,7 @@ impl Events { Ok(()) } - pub async fn add(&self, event_spec: EventSpec) -> Result<()> { + async fn add(&self, event_spec: EventSpec) -> Result<()> { let new_event = api_request::(Method::POST, "/event", Some(event_spec)).await?; @@ -105,7 +117,7 @@ impl Events { Ok(()) } - pub async fn update_event(&self, event_name: &str, event_update: EventUpdate) -> Result<()> { + async fn update_event(&self, event_name: &str, event_update: EventUpdate) -> Result<()> { api_request::( Method::POST, &format!("/event/{}", event_name), @@ -118,7 +130,7 @@ impl Events { Ok(()) } - pub async fn delete(&self, event_name: &str) -> Result<()> { + async fn delete(&self, event_name: &str) -> Result<()> { api_request::<(), ()>(Method::DELETE, &format!("/event/{}", event_name), None).await?; self.load().await?;