layout
This commit is contained in:
parent
09d09159f2
commit
b49c6d475b
89
Cargo.lock
generated
89
Cargo.lock
generated
@ -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"
|
||||
|
@ -3,4 +3,3 @@
|
||||
<p align="center">
|
||||
<img src="docs/ascend.jpg" alt="Logo" width="100%">
|
||||
</p>
|
||||
|
||||
|
@ -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",
|
||||
|
9
crates/ascend/src/components/button.rs
Normal file
9
crates/ascend/src/components/button.rs
Normal file
@ -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! {
|
||||
<button on:click=onclick type="button" class="text-black bg-orange-300 hover:bg-orange-400 focus:ring-4 focus:ring-orange-500 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none">{ text }</button>
|
||||
}
|
||||
}
|
@ -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! {
|
||||
<div class="flex">
|
||||
// Left gradient chunk
|
||||
<div class="flex-grow">
|
||||
@ -53,6 +56,17 @@ pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<div class="bg-orange-300 text-black border-b-2 border-b-orange-400">
|
||||
<div class="container mx-auto" >
|
||||
<Header items />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,11 +98,7 @@ pub fn Header(items: HeaderItems) -> impl IntoView {
|
||||
#[component]
|
||||
fn Items(items: Vec<HeaderItem>) -> impl IntoView {
|
||||
let items = items.into_iter().map(|item| view! { <Item item /> }).collect_view();
|
||||
view! {
|
||||
<div class="flex gap-4">
|
||||
{ items }
|
||||
</div>
|
||||
}
|
||||
view! { <div class="flex gap-4">{items}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
@ -5,6 +5,7 @@ pub mod pages {
|
||||
pub mod wall;
|
||||
}
|
||||
pub mod components {
|
||||
pub mod button;
|
||||
pub mod header;
|
||||
}
|
||||
|
||||
|
@ -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::<ImportFromMiniMoonboard>::new());
|
||||
let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new());
|
||||
|
||||
let onclick = move |_mouse_event| {
|
||||
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard {});
|
||||
};
|
||||
|
||||
view! {
|
||||
// <p>"Import problems from"</p>
|
||||
// <button on:click=import_from_mini_moonboard>"Mini Moonboard"</button>
|
||||
<p>"Import problems from"</p>
|
||||
<button on:click=onclick>"Mini Moonboard"</button>
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,26 +63,24 @@ async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
||||
// use crate::server::state::State;
|
||||
// let state = expect_context::<State>();
|
||||
|
||||
// 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::<Config>();
|
||||
let state = expect_context::<State>();
|
||||
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(())
|
||||
}
|
||||
|
@ -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! {
|
||||
<div class="min-w-screen min-h-screen bg-slate-900">
|
||||
<StyledHeader items=header_items />
|
||||
<StyledHeader items=header_items />
|
||||
|
||||
<div class="container mx-auto mt-2">
|
||||
<div class="mx-auto m-2">
|
||||
<Await future=load let:data>
|
||||
<Ready data=data.deref().to_owned() />
|
||||
</Await>
|
||||
@ -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! {
|
||||
<div class=move || { grid_classes.clone() }>{cells}</div>
|
||||
<button on:click=move |_| problem_fetcher.mark_dirty()>"Random problem"</button>
|
||||
<div class="grid grid-cols sm:grid-cols-2 gap-8 grid-cols-[auto,1fr]">
|
||||
// Render the wall
|
||||
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div>
|
||||
|
||||
<div>
|
||||
<Button onclick=move |_| problem_fetcher.mark_dirty() text="Next problem ➤" />
|
||||
</ div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<PersistentState>,
|
||||
}
|
||||
|
||||
#[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<models::Problem>,
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||
// 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<State, Error> {
|
||||
Ok(State { persistent })
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub(crate) async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>, 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::<HoldPosition, HoldRole>::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<dyn std::error::Error>> {
|
||||
// State file moved to datastore/private
|
||||
fn load_config(cli: Cli) -> Result<Config, Error> {
|
||||
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<dyn std::error::Error>),
|
||||
|
||||
Confik(confik::Error),
|
||||
}
|
||||
|
33
crates/ascend/src/server/cli.rs
Normal file
33
crates/ascend/src/server/cli.rs
Normal file
@ -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() {
|
||||
<Cli as clap::CommandFactory>::command().debug_assert()
|
||||
}
|
7
crates/ascend/src/server/config.rs
Normal file
7
crates/ascend/src/server/config.rs
Normal file
@ -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,
|
||||
}
|
20
crates/ascend/src/server/migrations.rs
Normal file
20
crates/ascend/src/server/migrations.rs
Normal file
@ -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();
|
||||
}
|
||||
}
|
54
crates/ascend/src/server/operations.rs
Normal file
54
crates/ascend/src/server/operations.rs
Normal file
@ -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::<HoldPosition, HoldRole>::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),
|
||||
}
|
31
crates/ascend/src/server/state.rs
Normal file
31
crates/ascend/src/server/state.rs
Normal file
@ -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<PersistentState>,
|
||||
}
|
||||
|
||||
#[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<models::Problem>,
|
||||
}
|
@ -148,6 +148,7 @@
|
||||
];
|
||||
|
||||
env.RUST_LOG = "info,ascend=trace";
|
||||
env.MOONBOARD_PROBLEMS = "moonboard-problems";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user