diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1fc55f35..4a13d629 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,16 +13,24 @@ jobs: - name: Publish rwf-macros working-directory: rwf-macros + env: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} run: bash ../scripts/check_publish.sh - name: Publish rwf-ruby working-directory: rwf-ruby + env: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} run: bash ../scripts/check_publish.sh - name: Publish rwf working-directory: rwf + env: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} run: bash ../scripts/check_publish.sh - name: Publish rwf-cli working-directory: rwf-cli + env: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} run: bash ../scripts/check_publish.sh diff --git a/README.md b/README.md index 9f244764..1c4e4f0c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Static Badge](https://img.shields.io/badge/documentation-blue?style=flat)](https://levkk.github.io/rwf/) [![Latest crate](https://img.shields.io/crates/v/rwf.svg)](https://crates.io/crates/rwf) +[![Reference docs](https://img.shields.io/docsrs/rwf)](https://docs.rs/rwf/latest/rwf/) Rwf is a comprehensive framework for building web applications in Rust. Written using the classic MVC pattern (model-view-controller), Rwf comes standard with everything you need to easily build fast and secure web apps. diff --git a/examples/turbo/src/controllers/mod.rs b/examples/turbo/src/controllers/mod.rs index d4f9d756..a8305636 100644 --- a/examples/turbo/src/controllers/mod.rs +++ b/examples/turbo/src/controllers/mod.rs @@ -1,5 +1,6 @@ pub mod chat; pub mod signup; -pub use chat::{typing::TypingController, ChatController}; -pub use signup::{LogoutController, SignupController}; +pub use chat::typing::*; +pub use chat::*; +pub use signup::*; diff --git a/examples/turbo/src/main.rs b/examples/turbo/src/main.rs index c9112fae..9866f5b4 100644 --- a/examples/turbo/src/main.rs +++ b/examples/turbo/src/main.rs @@ -36,6 +36,8 @@ async fn main() -> Result<(), Error> { // Configure logging. Logger::init(); + // Run migrations on app start. + // Not mandatory, but helpful for this demo. Migrations::migrate().await?; Server::new(vec![ diff --git a/rwf-cli/src/deploy.rs b/rwf-cli/src/deploy.rs index f4b6611c..35ae198e 100644 --- a/rwf-cli/src/deploy.rs +++ b/rwf-cli/src/deploy.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use flate2::write::GzEncoder; use flate2::Compression; -use rwf::config::ConfigFile; +use rwf::config::Config; use tokio::process::Command; use crate::logging::*; @@ -59,7 +59,7 @@ pub async fn package( if let Some(config) = config { if config.is_file() { - if let Err(_) = ConfigFile::load(&config) { + if let Err(_) = Config::load(&config) { warning(format!( "{} doesn't seem to be be valid Rwf config file, but we'll use it anyway", config.display() diff --git a/rwf/src/config.rs b/rwf/src/config.rs index ef3bf9c6..8ac4868b 100644 --- a/rwf/src/config.rs +++ b/rwf/src/config.rs @@ -17,8 +17,9 @@ use serde::{Deserialize, Serialize}; use std::fs::read_to_string; use thiserror::Error; -static CONFIG: OnceCell = OnceCell::new(); +static CONFIG: OnceCell = OnceCell::new(); +/// Configuration error. #[derive(Error, Debug)] pub enum Error { #[error("config: {0}")] @@ -40,26 +41,36 @@ pub enum Error { NoConfig, } -pub fn get_config() -> &'static ConfigFile { - CONFIG.get_or_init(|| ConfigFile::load_default()) +/// Get application configuration. +/// +/// Safe to call from anywhere. +pub fn get_config() -> &'static Config { + CONFIG.get_or_init(|| Config::load_default()) } +/// Rwf configuration file. Can be deserialized +/// from a TOML file, although any format supported by +/// `serde` is possible. #[derive(Serialize, Deserialize, Clone)] -pub struct ConfigFile { +pub struct Config { + /// Where the configuration file is located. #[serde(skip)] pub path: Option, + /// General settings. Most settings are here. #[serde(default = "General::default")] pub general: General, + /// Database connection settings. #[serde(default = "DatabaseConfig::default")] pub database: DatabaseConfig, + /// WebSocket connections settings. #[serde(default = "WebsocketConfig::default")] pub websocket: WebsocketConfig, } -impl Default for ConfigFile { +impl Default for Config { fn default() -> Self { Self { path: None, @@ -72,7 +83,16 @@ impl Default for ConfigFile { } } -impl ConfigFile { +impl Config { + /// Get the configuration. + /// + /// Safe to call from anywhere. Loads the + /// config if it's not loaded yet. + pub fn get() -> &'static Self { + get_config() + } + + /// Load configuration file from default location(s). pub fn load_default() -> Self { for path in ["rwf.toml", "Rwf.toml", "Rum.toml"] { let path = Path::new(path); @@ -84,7 +104,8 @@ impl ConfigFile { Self::default() } - pub fn load(path: impl AsRef + Copy) -> Result { + /// Load configuration file from a specific path. + pub fn load(path: impl AsRef + Copy) -> Result { let file = read_to_string(path)?; let mut config: Self = toml::from_str(&file)?; config.path = Some(path.as_ref().to_owned()); @@ -117,6 +138,7 @@ impl ConfigFile { Ok(self) } + /// Log some information about the configuration file. pub fn log_info(&self) { if let Some(ref path) = self.path { info!("Configuration file \"{}\" loaded", path.display()); @@ -126,34 +148,53 @@ impl ConfigFile { } } +/// General configuration. Most configuration settings +/// are here. #[derive(Serialize, Deserialize, Clone)] pub struct General { + /// On what address to run the HTTP server. Default: 0.0.0.0 (all interfaces). + #[serde(default = "General::default_host")] + pub host: String, + /// On what port to run the HTTP server. Default: 8000. + #[serde(default = "General::default_port")] + pub port: u16, #[serde(default = "General::default_secret_key")] secret_key: String, + /// AES-128 encryption key. Derived from the secret key. Used for encrypting cookies, sessions, and arbitrary user data. #[serde(skip)] pub aes_key: Key>, #[serde(skip)] pub secure_id_key: Key>, + /// Enable logging all queries executed by the ORM. #[serde(default = "General::default_log_queries")] pub log_queries: bool, + /// Enable caching templates at runtime. #[serde(default = "General::default_cache_templates")] pub cache_templates: bool, + /// Record HTTP requests made to the server in the database. #[serde(default = "General::default_track_requests")] pub track_requests: bool, + /// Enable CSRF attack protection. #[serde(default = "General::default_csrf_protection")] pub csrf_protection: bool, #[serde(default = "General::default_cookie_max_age")] cookie_max_age: usize, #[serde(default = "General::default_session_duration")] session_duration: usize, + /// The terminal where Rwf is running is TTY. #[serde(default = "General::default_tty")] pub tty: bool, + /// Maximum size allowed for an HTTP header. #[serde(default = "General::default_header_max_size")] pub header_max_size: usize, + /// Maximum size allowed for an HTTP request. #[serde(default = "General::default_max_request_size")] pub max_request_size: usize, + /// Global authentication handler. Used by default + /// in all controllers. #[serde(skip)] pub default_auth: AuthHandler, + /// Global middleware set. Used by default in all controllers. #[serde(skip)] pub default_middleware: MiddlewareSet, } @@ -161,6 +202,8 @@ pub struct General { impl Default for General { fn default() -> Self { Self { + host: General::default_host(), + port: General::default_port(), secret_key: General::default_secret_key(), aes_key: Key::>::default(), secure_id_key: Key::>::default(), @@ -188,6 +231,17 @@ fn true_from_env(name: &str) -> bool { } impl General { + fn default_host() -> String { + String::from("0.0.0.0") + } + + fn default_port() -> u16 { + 8000 + } + + /// Extract the secret key from configuration. + /// It should be provided as a base64 string + /// encoding 256 bits of entropy. pub fn secret_key(&self) -> Result, Error> { use base64::{engine::general_purpose, Engine as _}; let bytes = general_purpose::STANDARD.decode(&self.secret_key)?; @@ -249,10 +303,14 @@ impl General { Duration::days(30).whole_milliseconds() as usize } + /// Default `MaxAge` attribute to be set on + /// all cookies. pub fn cookie_max_age(&self) -> Duration { Duration::milliseconds(self.cookie_max_age as i64) } + /// Authenticated session duration. When the session + /// expires, user must re-authenticate. pub fn session_duration(&self) -> Duration { Duration::milliseconds(self.session_duration as i64) } @@ -274,12 +332,23 @@ impl General { } } +/// WebSocket connections configuration. #[derive(Serialize, Deserialize, Clone)] pub struct WebsocketConfig { + /// How long to wait for a ping to receive a pong. + /// Configured in milliseconds. + /// Use [`WebsocketConfig::ping_timeout`] to get a + /// valid [`time::Duration`]. #[serde(default = "WebsocketConfig::default_ping_timeout")] pub ping_timeout: usize, + /// How often to send pings. + /// Configured in milliseconds. + /// Use [`WebsocketConfig::ping_interval`] to get a + /// valid [`time::Duration`]. #[serde(default = "WebsocketConfig::default_ping_interval")] pub ping_interval: usize, + /// Allow this many unanswered pings before + /// closing the connection. #[serde(default = "WebsocketConfig::default_disconnect_count")] pub ping_disconnect_count: usize, } @@ -299,6 +368,7 @@ impl WebsocketConfig { Duration::seconds(5).whole_milliseconds() as usize } + /// How long to wait for a ping to receive a pong. pub fn ping_timeout(&self) -> Duration { Duration::milliseconds(self.ping_timeout as i64) } @@ -307,6 +377,7 @@ impl WebsocketConfig { Duration::seconds(60).whole_milliseconds() as usize } + /// How often to send pings. pub fn ping_interval(&self) -> Duration { Duration::milliseconds(self.ping_interval as i64) } @@ -316,15 +387,26 @@ impl WebsocketConfig { } } +/// Database connection configuration. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct DatabaseConfig { url: Option, name: Option, user: Option, + /// How long to wait before an idle connection is + /// automatically closed by the pool. + /// Configured in milliseconds. + /// Use [`DatabaseConfig::idle_timeout`] to get a valid [`Duration`] struct. #[serde(default = "DatabaseConfig::default_idle_timeout")] pub idle_timeout: usize, + /// Maximum amount of time to wait for a connection + /// from the pool. + /// Configured in milliseconds. + /// Use [`DatabaseConfig::checkout_timeout`] to get a valid [`Duration`] struct. #[serde(default = "DatabaseConfig::default_checkout_timeout")] pub checkout_timeout: usize, + /// Maximum number of database connections + /// in the pool. #[serde(default = "DatabaseConfig::default_pool_size")] pub pool_size: usize, } @@ -347,6 +429,8 @@ impl DatabaseConfig { 3600 * 1000 } + /// How long to wait before an idle connection is + /// automatically closed by the pool. pub fn idle_timeout(&self) -> Duration { Duration::milliseconds(self.idle_timeout as i64) } @@ -355,6 +439,8 @@ impl DatabaseConfig { 5 * 1000 } + /// Maximum amount of time to wait for a connection + /// from the pool. pub fn checkout_timeout(&self) -> Duration { Duration::milliseconds(self.checkout_timeout as i64) } @@ -363,6 +449,9 @@ impl DatabaseConfig { 10 } + /// Convert the connection config to a valid + /// database URL as described by the + /// Twelve Factor Application. pub fn database_url(&self) -> String { match self.url { Some(ref url) => url.clone(), @@ -413,7 +502,7 @@ name = "test" let mut file = File::create(path).unwrap(); file.write_all(config.as_bytes()).unwrap(); - let config = ConfigFile::load_default(); + let config = Config::load_default(); assert_eq!(config.path, Some(PathBuf::from(config_path))); } } diff --git a/rwf/src/lib.rs b/rwf/src/lib.rs index a83eba20..2b1786f7 100644 --- a/rwf/src/lib.rs +++ b/rwf/src/lib.rs @@ -1,6 +1,9 @@ //! Rwf is a comprehensive framework for building web applications in Rust. Written using the classic MVC pattern //! (model-view-controller), Rwf comes standard with everything you need to easily build fast and secure web apps. //! +//! This documentation serves primarily as a reference for methods and types provided by this +//! and [`rwf_macros`] crates. For user guides, refer to the [documentation here](https://levkk.github.io/rwf/). +//! //! # Getting started //! //! Rwf is a Rust library built on top of Tokio, and can be added to any binary or library Rust project: @@ -77,12 +80,6 @@ //! } //! ``` //! -//! # Documentation -//! -//! Rwf docs are primarily [located here](https://levkk.github.io/rwf/). While maintaining two sets of documentation -//! can be challenging, we'll make every effort to maintain both versions. Most public methods have some documentation -//! and examples. -//! pub mod analytics; pub mod colors; pub mod comms; diff --git a/rwf/src/prelude.rs b/rwf/src/prelude.rs index c56f451e..69b728f3 100644 --- a/rwf/src/prelude.rs +++ b/rwf/src/prelude.rs @@ -7,7 +7,7 @@ //! use rwf::prelude::*; //! ``` pub use crate::comms::Comms; -pub use crate::config::ConfigFile; +pub use crate::config::Config; pub use crate::controller::{auth::SessionAuth, AuthHandler}; pub use crate::controller::{ Authentication, Controller, Error, ModelController, PageController, RestController, SessionId, diff --git a/scripts/check_publish.sh b/scripts/check_publish.sh index 570f2cc0..4900cd74 100644 --- a/scripts/check_publish.sh +++ b/scripts/check_publish.sh @@ -24,4 +24,8 @@ else echo "All checks passed, publishing crate" fi +if [[ ! -z "${CRATES_IO_TOKEN}" ]]; then + cargo login "${CRATES_IO_TOKEN}" +fi + cargo publish