From b49c6d475b77ae87d860365e8223ae99c651b746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Juul=20Brunsh=C3=B8j?= Date: Sat, 25 Jan 2025 12:56:55 +0100 Subject: [PATCH] layout --- Cargo.lock | 89 ++++++++++++++ README.md | 1 - crates/ascend/Cargo.toml | 4 + crates/ascend/src/components/button.rs | 9 ++ crates/ascend/src/components/header.rs | 22 +++- crates/ascend/src/lib.rs | 1 + crates/ascend/src/pages/routes.rs | 31 ++--- crates/ascend/src/pages/wall.rs | 17 ++- crates/ascend/src/server.rs | 164 ++++++------------------- crates/ascend/src/server/cli.rs | 33 +++++ crates/ascend/src/server/config.rs | 7 ++ crates/ascend/src/server/migrations.rs | 20 +++ crates/ascend/src/server/operations.rs | 54 ++++++++ crates/ascend/src/server/state.rs | 31 +++++ flake.nix | 1 + justfile | 1 - 16 files changed, 333 insertions(+), 152 deletions(-) create mode 100644 crates/ascend/src/components/button.rs create mode 100644 crates/ascend/src/server/cli.rs create mode 100644 crates/ascend/src/server/config.rs create mode 100644 crates/ascend/src/server/migrations.rs create mode 100644 crates/ascend/src/server/operations.rs create mode 100644 crates/ascend/src/server/state.rs diff --git a/Cargo.lock b/Cargo.lock index 3f5207d..d9f64fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,7 @@ dependencies = [ "axum", "camino", "clap", + "confik", "console_error_panic_hook", "derive_more", "http 1.2.0", @@ -130,6 +131,7 @@ dependencies = [ "type-toppings", "wasm-bindgen", "web-sys", + "xdg", ] [[package]] @@ -314,6 +316,9 @@ name = "camino" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] [[package]] name = "cfg-if" @@ -433,6 +438,33 @@ dependencies = [ "toml", ] +[[package]] +name = "confik" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb80c5315aacb4f2a7ef6ddb6b2fe7818191e78098f5ac597372647b9958039" +dependencies = [ + "camino", + "cfg-if", + "confik-macros", + "envious", + "serde", + "thiserror 1.0.69", + "toml", +] + +[[package]] +name = "confik-macros" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f641e4f578df8c52e7c37300d5d9d2b12be6fb6f4732b62a794e2b647d1a2" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -490,6 +522,41 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -577,6 +644,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "envious" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52788a407588138195a40c991f500621fea2cffa87e7345d86dbab77287dc7" +dependencies = [ + "serde", + "thiserror 1.0.69", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1215,6 +1292,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -3077,6 +3160,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/README.md b/README.md index 5e0440c..e787a29 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,3 @@

Logo

- diff --git a/crates/ascend/Cargo.toml b/crates/ascend/Cargo.toml index aac3bb1..974d558 100644 --- a/crates/ascend/Cargo.toml +++ b/crates/ascend/Cargo.toml @@ -34,6 +34,8 @@ ron = { version = "0.8" } rand = { version = "0.8", optional = true } web-sys = { version = "0.3.76", features = ["File", "FileList"] } smart-default = "0.7.1" +confik = { version = "0.12", optional = true, features = ["camino"] } +xdg = { version = "2.5", optional = true } [dev-dependencies.serde_json] version = "1" @@ -47,6 +49,8 @@ ssr = [ "dep:tower", "dep:tower-http", "dep:leptos_axum", + "dep:confik", + "dep:xdg", "dep:camino", "dep:moonboard-parser", "leptos/ssr", diff --git a/crates/ascend/src/components/button.rs b/crates/ascend/src/components/button.rs new file mode 100644 index 0000000..9d2accb --- /dev/null +++ b/crates/ascend/src/components/button.rs @@ -0,0 +1,9 @@ +use leptos::prelude::*; +use web_sys::MouseEvent; + +#[component] +pub fn Button(#[prop(into)] text: String, onclick: impl Fn(MouseEvent) -> () + 'static) -> impl IntoView { + view! { + + } +} diff --git a/crates/ascend/src/components/header.rs b/crates/ascend/src/components/header.rs index ca02208..8db55aa 100644 --- a/crates/ascend/src/components/header.rs +++ b/crates/ascend/src/components/header.rs @@ -14,7 +14,10 @@ pub struct HeaderItem { /// Header with background color etc. #[component] pub fn StyledHeader(items: HeaderItems) -> impl IntoView { - view! { + let fancy = false; + + if fancy { + view! {
// Left gradient chunk
@@ -53,6 +56,17 @@ pub fn StyledHeader(items: HeaderItems) -> impl IntoView { />
+ } + .into_any() + } else { + view! { +
+
+
+
+
+ } + .into_any() } } @@ -84,11 +98,7 @@ pub fn Header(items: HeaderItems) -> impl IntoView { #[component] fn Items(items: Vec) -> impl IntoView { let items = items.into_iter().map(|item| view! { }).collect_view(); - view! { -
- { items } -
- } + view! {
{items}
} } #[component] diff --git a/crates/ascend/src/lib.rs b/crates/ascend/src/lib.rs index fbc89e9..8ea317e 100644 --- a/crates/ascend/src/lib.rs +++ b/crates/ascend/src/lib.rs @@ -5,6 +5,7 @@ pub mod pages { pub mod wall; } pub mod components { + pub mod button; pub mod header; } diff --git a/crates/ascend/src/pages/routes.rs b/crates/ascend/src/pages/routes.rs index 85a3cda..94208aa 100644 --- a/crates/ascend/src/pages/routes.rs +++ b/crates/ascend/src/pages/routes.rs @@ -6,7 +6,6 @@ use leptos::prelude::*; use serde::Deserialize; use serde::Serialize; use std::ops::Deref; -use std::path::PathBuf; #[component] pub fn Routes() -> impl leptos::IntoView { @@ -44,11 +43,15 @@ pub fn Routes() -> impl leptos::IntoView { fn Ready(data: InitialData) -> impl leptos::IntoView { tracing::debug!("ready"); - // let import_from_mini_moonboard = Action::from(ServerAction::::new()); + let import_from_mini_moonboard = Action::from(ServerAction::::new()); + + let onclick = move |_mouse_event| { + import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard {}); + }; view! { - //

"Import problems from"

- // +

"Import problems from"

+ } } @@ -60,26 +63,24 @@ async fn load_initial_data() -> Result, ServerFnError> { // use crate::server::state::State; // let state = expect_context::(); + // TODO: provide info on current routes set + Ok(RonCodec::new(InitialData {})) } #[server(name = ImportFromMiniMoonboard)] #[tracing::instrument] async fn import_from_mini_moonboard() -> Result<(), ServerFnError> { + use crate::server::config::Config; + use crate::server::state::State; + tracing::info!("Importing mini moonboard problems"); - let file_path: PathBuf = todo!(); - - let problems = crate::server::import_mini_moonboard_problems(&file_path).await?; - - use crate::server::state::State; + let config = expect_context::(); let state = expect_context::(); - state - .persistent - .update(|s| { - s.problems.problems.extend(problems); - }) - .await?; + crate::server::operations::import_mini_moonboard_problems(&config, &state).await?; + + // TODO: Return information about what was done Ok(()) } diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index 8a26ca6..64d6cd6 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -1,4 +1,5 @@ use crate::codec::ron::RonCodec; +use crate::components::button::Button; use crate::components::header::HeaderItem; use crate::components::header::HeaderItems; use crate::components::header::StyledHeader; @@ -20,7 +21,7 @@ pub fn Wall() -> impl leptos::IntoView { let header_items = HeaderItems { left: vec![], middle: vec![HeaderItem { - text: "Ascend".to_string(), + text: "ASCEND".to_string(), link: None, }], right: vec![ @@ -37,9 +38,9 @@ pub fn Wall() -> impl leptos::IntoView { leptos::view! {
- + -
+
@@ -74,8 +75,14 @@ fn Ready(data: InitialData) -> impl leptos::IntoView { let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols); view! { -
{cells}
- +
+ // Render the wall +
{cells}
+ +
+
} } diff --git a/crates/ascend/src/server.rs b/crates/ascend/src/server.rs index 111c057..c28e5e8 100644 --- a/crates/ascend/src/server.rs +++ b/crates/ascend/src/server.rs @@ -1,80 +1,24 @@ //! Server-only features -use crate::models; -use models::HoldPosition; -use models::HoldRole; -use models::Problem; +use cli::Cli; +use config::Config; +use confik::Configuration; +use confik::EnvSource; use persistence::Persistent; use state::PersistentState; use state::State; -use std::collections::BTreeMap; use std::path::Path; use std::path::PathBuf; use tower_http::services::ServeDir; use tracing::level_filters::LevelFilter; use type_toppings::ResultExt; -pub mod cli { - //! Server CLI interface - - use std::path::PathBuf; - - #[derive(clap::Parser)] - #[command(version, about, long_about = None)] - pub struct Cli { - #[command(subcommand)] - pub command: Command, - } - - #[derive(clap::Subcommand, Default)] - pub enum Command { - #[default] - Serve, - - /// Resets state, replacing it with defaults - ResetState, - - ImportMiniMoonboardProblems { - file_path: PathBuf, - }, - } -} - -pub mod state { - //! Server state - - const STATE_VERSION: u64 = 1; - - use super::persistence::Persistent; - use crate::models; - use crate::models::Wall; - use serde::Deserialize; - use serde::Serialize; - use smart_default::SmartDefault; - use std::collections::BTreeSet; - - #[derive(Clone, Debug)] - pub struct State { - pub persistent: Persistent, - } - - #[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)] - pub struct PersistentState { - /// State schema version - #[default(STATE_VERSION)] - pub version: u64, - - pub wall: Wall, - pub problems: Problems, - } - - #[derive(Serialize, Deserialize, Clone, Debug, Default)] - pub struct Problems { - pub problems: BTreeSet, - } -} - +mod cli; +pub mod config; +mod migrations; +pub mod operations; pub mod persistence; +pub mod state; pub const STATE_FILE: &str = "datastore/private/state.ron"; @@ -96,19 +40,9 @@ pub async fn main() { .init(); let cli = Cli::parse(); + match cli.command { - Command::Serve => serve().await.unwrap_or_report(), - Command::ImportMiniMoonboardProblems { file_path } => { - let problems = import_mini_moonboard_problems(&file_path).await.unwrap_or_report(); - let state = load_state().await.unwrap_or_report(); - state - .persistent - .update(|s| { - s.problems.problems.extend(problems); - }) - .await - .unwrap_or_report(); - } + Command::Serve => serve(cli).await.unwrap_or_report(), Command::ResetState => { let s = PersistentState::default(); let p = Path::new(STATE_FILE); @@ -118,8 +52,8 @@ pub async fn main() { } } -#[tracing::instrument(err)] -async fn serve() -> Result<(), Error> { +#[tracing::instrument(skip(cli), err)] +async fn serve(cli: Cli) -> Result<(), Error> { use crate::app::App; use crate::app::shell; use axum::Router; @@ -127,25 +61,34 @@ async fn serve() -> Result<(), Error> { use leptos_axum::LeptosRoutes; use leptos_axum::generate_route_list; - run_migrations().await.map_err(self::Error::Migration)?; + migrations::run_migrations().await; // Setting get_configuration(None) means we'll be using cargo-leptos's env values // For deployment these variables are: // // Alternately a file can be specified such as Some("Cargo.toml") // The file would need to be included with the executable when moved to deployment - let conf = get_configuration(None).unwrap_or_report(); - let leptos_options = conf.leptos_options; + let leptos_conf_file = get_configuration(None).unwrap_or_report(); + let leptos_options = leptos_conf_file.leptos_options; let addr = leptos_options.site_addr; let routes = generate_route_list(App); + let config = load_config(cli)?; let server_state = load_state().await?; let app = Router::new() - .leptos_routes_with_context(&leptos_options, routes, move || provide_context(server_state.clone()), { - let leptos_options = leptos_options.clone(); - move || shell(leptos_options.clone()) - }) + .leptos_routes_with_context( + &leptos_options, + routes, + move || { + provide_context(server_state.clone()); + provide_context(config.clone()) + }, + { + let leptos_options = leptos_options.clone(); + move || shell(leptos_options.clone()) + }, + ) .nest_service("/files", file_service("datastore/public")) .fallback(leptos_axum::file_and_error_handler(shell)) .with_state(leptos_options); @@ -179,46 +122,17 @@ async fn load_state() -> Result { Ok(State { persistent }) } -#[tracing::instrument] -pub(crate) async fn import_mini_moonboard_problems(file_path: &Path) -> Result, Error> { - let mut problems = Vec::new(); - - tracing::info!("Parsing mini moonboard problems from {}", file_path.display()); - let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path).await?; - for problem in mini_moonboard.problems { - let mut holds = BTreeMap::::new(); - for mv in problem.moves { - let row = mv.description.row(); - let col = mv.description.column(); - let hold_position = HoldPosition { row, col }; - - let role = match (mv.is_start, mv.is_end) { - (true, true) => unreachable!(), - (true, false) => HoldRole::Start, - (false, true) => HoldRole::End, - (false, false) => HoldRole::Normal, - }; - holds.insert(hold_position, role); - } - let route = Problem { holds }; - problems.push(route); - } - Ok(problems) -} - -async fn run_migrations() -> Result<(), Box> { - // State file moved to datastore/private +fn load_config(cli: Cli) -> Result { + let mut builder = config::Config::builder(); + if cli + .config + .try_exists() + .expect_or_report_with(|| format!("Failed to look up config file at {}", cli.config)) { - let m = PathBuf::from("state.ron"); - if m.try_exists()? { - tracing::warn!("MIGRATING STATE FILE"); - let p = PathBuf::from(STATE_FILE); - tokio::fs::create_dir_all(p.parent().unwrap()).await?; - tokio::fs::rename(m, &p).await?; - } + builder.override_with(confik::FileSource::new(cli.config)); } - - Ok(()) + let config = builder.override_with(EnvSource::new().allow_secrets()).try_build()?; + Ok(config) } #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] @@ -230,4 +144,6 @@ pub enum Error { #[display("Failed migration")] Migration(Box), + + Confik(confik::Error), } diff --git a/crates/ascend/src/server/cli.rs b/crates/ascend/src/server/cli.rs new file mode 100644 index 0000000..4b25089 --- /dev/null +++ b/crates/ascend/src/server/cli.rs @@ -0,0 +1,33 @@ +//! Server CLI interface + +#[derive(clap::Parser)] +#[command(version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Command, + + /// Path to configuration file. + #[arg(long, default_value_t = default_config_location())] + pub config: camino::Utf8PathBuf, +} + +#[derive(clap::Subcommand, Default)] +pub enum Command { + #[default] + Serve, + + /// Resets state, replacing it with defaults + ResetState, +} + +fn default_config_location() -> camino::Utf8PathBuf { + let xdg_dirs = xdg::BaseDirectories::with_prefix("ascend").unwrap(); + let config_path = xdg_dirs.get_config_file("config.toml"); + camino::Utf8PathBuf::from_path_buf(config_path).unwrap() +} + +#[cfg(test)] +#[test] +fn verify_cli() { + ::command().debug_assert() +} diff --git a/crates/ascend/src/server/config.rs b/crates/ascend/src/server/config.rs new file mode 100644 index 0000000..fb7dc14 --- /dev/null +++ b/crates/ascend/src/server/config.rs @@ -0,0 +1,7 @@ +use camino::Utf8PathBuf; + +#[derive(Clone, Debug, confik::Configuration)] +pub struct Config { + /// The location of the moonboard problems directory. + pub moonboard_problems: Utf8PathBuf, +} diff --git a/crates/ascend/src/server/migrations.rs b/crates/ascend/src/server/migrations.rs new file mode 100644 index 0000000..7c8e26f --- /dev/null +++ b/crates/ascend/src/server/migrations.rs @@ -0,0 +1,20 @@ +use crate::server::STATE_FILE; +use std::path::PathBuf; +use type_toppings::ResultExt; + +#[tracing::instrument] +pub async fn run_migrations() { + migrate_state_file().await; +} + +/// State file moved to datastore/private +#[tracing::instrument] +async fn migrate_state_file() { + let m = PathBuf::from("state.ron"); + if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) { + tracing::warn!("MIGRATING STATE FILE"); + let p = PathBuf::from(STATE_FILE); + tokio::fs::create_dir_all(p.parent().unwrap()).await.unwrap_or_report(); + tokio::fs::rename(m, &p).await.unwrap_or_report(); + } +} diff --git a/crates/ascend/src/server/operations.rs b/crates/ascend/src/server/operations.rs new file mode 100644 index 0000000..54063ec --- /dev/null +++ b/crates/ascend/src/server/operations.rs @@ -0,0 +1,54 @@ +//! Server lib module to host re-usable server operations. + +use crate::models::HoldPosition; +use crate::models::HoldRole; +use crate::models::Problem; +use crate::server::config::Config; +use crate::server::persistence; +use crate::server::state::State; +use std::collections::BTreeMap; + +#[tracing::instrument(skip(state))] +pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &State) -> Result<(), Error> { + let mut problems = Vec::new(); + + let file_name = "problems Mini MoonBoard 2020 40.json"; + let file_path = config.moonboard_problems.join(file_name); + + tracing::info!("Parsing mini moonboard problems from {file_path}"); + + let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path.as_std_path()).await?; + for problem in mini_moonboard.problems { + let mut holds = BTreeMap::::new(); + for mv in problem.moves { + let row = mv.description.row(); + let col = mv.description.column(); + let hold_position = HoldPosition { row, col }; + + let role = match (mv.is_start, mv.is_end) { + (true, true) => unreachable!(), + (true, false) => HoldRole::Start, + (false, true) => HoldRole::End, + (false, false) => HoldRole::Normal, + }; + holds.insert(hold_position, role); + } + let route = Problem { holds }; + problems.push(route); + } + + state + .persistent + .update(|s| { + s.problems.problems.extend(problems); + }) + .await?; + + Ok(()) +} + +#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] +pub enum Error { + Parser(moonboard_parser::Error), + Persistence(persistence::Error), +} diff --git a/crates/ascend/src/server/state.rs b/crates/ascend/src/server/state.rs new file mode 100644 index 0000000..cdf6def --- /dev/null +++ b/crates/ascend/src/server/state.rs @@ -0,0 +1,31 @@ +//! Server state + +const STATE_VERSION: u64 = 1; + +use super::persistence::Persistent; +use crate::models; +use crate::models::Wall; +use serde::Deserialize; +use serde::Serialize; +use smart_default::SmartDefault; +use std::collections::BTreeSet; + +#[derive(Clone, Debug)] +pub struct State { + pub persistent: Persistent, +} + +#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)] +pub struct PersistentState { + /// State schema version + #[default(STATE_VERSION)] + pub version: u64, + + pub wall: Wall, + pub problems: Problems, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct Problems { + pub problems: BTreeSet, +} diff --git a/flake.nix b/flake.nix index 669fc31..279377a 100644 --- a/flake.nix +++ b/flake.nix @@ -148,6 +148,7 @@ ]; env.RUST_LOG = "info,ascend=trace"; + env.MOONBOARD_PROBLEMS = "moonboard-problems"; }; }; } diff --git a/justfile b/justfile index 3b7eafe..abccf0b 100644 --- a/justfile +++ b/justfile @@ -25,7 +25,6 @@ run-release: reset-state: cargo run --features ssr -- reset-state - cargo run --features ssr -- import-mini-moonboard-problems "moonboard-problems/problems Mini MoonBoard 2020 40.json" # Open firewall port for development open-firewall: