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 @@
-
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}
+
+
+
+ div>
+
}
}
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: