// main.rs — Axum server entry point // // This file is compiled ONLY when the "ssr" feature is enabled. // `#[cfg(feature = "ssr")]` works like `#ifdef` in C: // the guarded code does not exist in the WASM bundle. // // Run the server: // cargo run --features ssr // // Run with verbose logs: // RUST_LOG=debug cargo run --features ssr #[cfg(feature = "ssr")] #[tokio::main] // `#[tokio::main]` turns the synchronous `fn main()` into an async function // managed by the Tokio runtime. Without it, Rust cannot execute `async` code. async fn main() { use axum::Router; use leptos::config::get_configuration; use leptos::prelude::provide_context; use leptos::view; use leptos_axum::{ generate_route_list, handle_server_fns_with_context, LeptosRoutes, }; use rust_ipam::{ app::{App, Shell}, server::{ config::AppConfig, db::{create_pool, run_migrations}, routes::not_found_handler, state::AppState, }, }; use tower_http::services::ServeDir; // Initialize structured logging. // tracing::info!(), tracing::warn!(), etc. produce no output without this. // RUST_LOG=debug cargo run --features ssr → enables debug-level logs tracing_subscriber::fmt() .with_env_filter( std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), ) .init(); tracing::info!("Starting Rust IPAM server..."); // Load configuration from environment variables / .env file. // The server cannot start without knowing which database to connect to, // so we abort immediately on any configuration error. let app_config = AppConfig::from_env() .expect("Configuration error — check your .env file"); tracing::info!("Database: {} ({})", app_config.backend, app_config.database_url); // Connect to the database and apply any pending migrations. // The server cannot serve data without a working database, so we abort on error. let pool = create_pool(&app_config) .await .expect("Failed to connect to database"); run_migrations(&pool, &app_config.backend) .await .expect("Database migration failed"); tracing::info!("Database ready."); // `Some("Cargo.toml")` tells Leptos to read the [package.metadata.leptos] // section from Cargo.toml (file paths, output names, server address...). let conf = get_configuration(Some("Cargo.toml")) .expect("Failed to load Leptos configuration from Cargo.toml"); let leptos_options = conf.leptos_options; let addr = leptos_options.site_addr; // Combine Leptos options and the database pool into a single shared state. // `AppState` implements `FromRef for LeptosOptions` so Leptos // can still extract just what it needs from the full state. let state = AppState { leptos_options: leptos_options.clone(), db: pool }; // Walk all `` components inside `App` to build the list of URLs // that Leptos SSR must handle. let routes = generate_route_list(App); // Clone the pool so we can inject it into two different contexts: // 1. `leptos_routes_with_context` — SSR rendering + server functions called during SSR // 2. `handle_server_fns_with_context` — server functions called from the WASM client let pool_for_routes = state.db.clone(); let pool_for_fns = state.db.clone(); // Build the Axum router using the builder pattern (method chaining). // // `Router::::new()` explicitly tells Rust the state type is `AppState`. // Without this annotation, type inference would default to `LeptosOptions` // (inferred from `leptos_routes`) and then reject `.with_state(state: AppState)`. let app = Router::::new() // Serve static files compiled by trunk (WASM, JS...). // Trunk places them in target/site/pkg/ as configured in [package.metadata.leptos]. .nest_service("/pkg", ServeDir::new("target/site/pkg")) // Handle server function HTTP calls from the WASM client. // // `#[server]` functions register themselves at "/api/". // `handle_server_fns_with_context` runs the server function body and injects // `additional_context` into the Leptos context before execution, // so server functions can call `use_context::()` to get the pool. .route( "/api/*fn_name", axum::routing::post({ let pool = pool_for_fns; move |req| { let pool = pool.clone(); handle_server_fns_with_context( move || provide_context(pool.clone()), req, ) } }), ) // Mount all Leptos routes into Axum. // `leptos_routes_with_context` injects the pool into the Leptos context // for every SSR render — needed for server functions called during SSR // (e.g. when a `Resource` pre-fetches data on the server). .leptos_routes_with_context( &state, routes, { move || provide_context(pool_for_routes.clone()) }, { let leptos_options = state.leptos_options.clone(); move || view! { } }, ) .fallback(not_found_handler) // Share AppState (Leptos options + DB pool) with all handlers. .with_state(state); let listener = tokio::net::TcpListener::bind(&addr) .await .expect(&format!("Failed to bind to address {}", addr)); tracing::info!("Server listening on http://{}", addr); axum::serve(listener, app) .await .expect("Fatal server error"); } // This empty block is required so the compiler finds a `fn main()` // when building in WASM mode (where the "ssr" feature is not enabled). // In WASM, the real entry point is `hydrate()` in lib.rs. #[cfg(not(feature = "ssr"))] fn main() {}