From af1ed398512f006d7bd520a29b8f874941caeeff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Thu, 23 Jul 2026 22:15:39 +0200 Subject: [PATCH] feat: add auto-generated config TOML reference This adds an automated script that generates a Guide page about the configuration by using JsonSchema of the config structures in config.rs. In addition to that, there's a new test added to cot-test that checks whether the markdown file in the repository is up to date. This has two huge advantages: 1. There is a full specification of the config file now, making it much easier to find out what the correct values are. 2. The specification never gets out of date, because the CI pipeline forces the guide page to be updated. The reason we store the markdown file in the repository instead of generating it on the fly is mainly so that any changes to the documentation can be easily reviewed. The workflow is therefore as following: 1. A change is made to the config structures in config.rs. 2. The change author regenerates the guide with `just generate-config-docs`. 3. The changes are reviewed when a PR is made. 4. Whenever the generated result is not up to our standards, either the docs or the generation script is modified. There are some limitations of the page generation script, such as: * The script only takes the first paragraph of the rustdoc, so the links need to be inlined - referencing links defined later in the doc will not work. * Some types are not supported - e.g. `[cache.timeout]` displays the type of the value is just `string`, even though it needs to be a specific string that represents a time duration. The config page "cheats" a little bit to be more readable, e.g. it reduces the padding of the content. See cot-rs/cot-site#111 for the details. Fixes #479 --- Cargo.lock | 3 + cot-test/Cargo.toml | 16 + cot-test/src/bin/generate_config_docs.rs | 20 + cot-test/src/config_reference.rs | 489 +++++++++++++++++++++++ cot-test/src/lib.rs | 3 + cot-test/tests/config_reference.rs | 25 ++ cot/Cargo.toml | 2 + cot/src/config.rs | 101 +++-- cot/src/email/transport/smtp.rs | 1 + docs/configuration.md | 218 ++++++++++ docs/site/Cargo.lock | 66 +-- docs/site/Cargo.toml | 4 +- docs/site/src/main.rs | 1 + justfile | 4 + 14 files changed, 870 insertions(+), 83 deletions(-) create mode 100644 cot-test/src/bin/generate_config_docs.rs create mode 100644 cot-test/src/config_reference.rs create mode 100644 cot-test/tests/config_reference.rs create mode 100644 docs/configuration.md diff --git a/Cargo.lock b/Cargo.lock index 65e4e2fa5..e61f4fdff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1058,6 +1058,8 @@ dependencies = [ "cot-cli", "glob", "libtest-mimic", + "schemars", + "serde_json", "thiserror 2.0.20", ] @@ -3814,6 +3816,7 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ + "indexmap", "itoa", "memchr", "serde", diff --git a/cot-test/Cargo.toml b/cot-test/Cargo.toml index e8ceebcbf..2be4d383a 100644 --- a/cot-test/Cargo.toml +++ b/cot-test/Cargo.toml @@ -18,8 +18,14 @@ cot-cli = { workspace = true, features = ["test_utils"] } cot.workspace = true glob.workspace = true libtest-mimic.workspace = true +schemars = { workspace = true, features = ["std"], optional = true } +serde_json = { workspace = true, optional = true } thiserror.workspace = true +[features] +# Enables generating the configuration file reference (docs/configuration.md). +config-docs = ["cot/full", "cot/_internal_config-docs", "dep:schemars", "dep:serde_json"] + [lints] workspace = true @@ -27,3 +33,13 @@ workspace = true name = "doc_code_blocks" path = "tests/doc_code_blocks.rs" harness = false + +[[test]] +name = "config_reference" +path = "tests/config_reference.rs" +required-features = ["config-docs"] + +[[bin]] +name = "generate_config_docs" +path = "src/bin/generate_config_docs.rs" +required-features = ["config-docs"] diff --git a/cot-test/src/bin/generate_config_docs.rs b/cot-test/src/bin/generate_config_docs.rs new file mode 100644 index 000000000..66b120006 --- /dev/null +++ b/cot-test/src/bin/generate_config_docs.rs @@ -0,0 +1,20 @@ +//! Regenerates `docs/configuration.md` from `cot::config::ProjectConfig`'s type +//! definition. +//! +//! Run via `just generate-config-docs`. + +use std::path::PathBuf; + +fn main() { + let content = cot_test::config_reference::generate_config_reference(); + + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let docs_path = manifest_dir + .parent() + .expect("failed to get workspace path") + .join("docs") + .join("configuration.md"); + + std::fs::write(&docs_path, content).expect("failed to write docs/configuration.md"); + println!("Wrote {}", docs_path.display()); +} diff --git a/cot-test/src/config_reference.rs b/cot-test/src/config_reference.rs new file mode 100644 index 000000000..a123d1d30 --- /dev/null +++ b/cot-test/src/config_reference.rs @@ -0,0 +1,489 @@ +//! Generates the configuration file reference (`docs/configuration.md`) from +//! `cot::config::ProjectConfig`'s JSON schema (via `schemars`). + +use std::fmt::Write as _; + +use serde_json::{Map, Value}; + +/// Generates the Markdown configuration reference. +/// +/// # Panics +/// +/// Panics if `cot::config::ProjectConfig`'s JSON schema doesn't have the shape +/// this generator expects. +#[must_use] +pub fn generate_config_reference() -> String { + let schema = schemars::schema_for!(cot::config::ProjectConfig); + let root = schema + .as_value() + .as_object() + .expect("root schema must be a JSON object"); + let defs: Map = root + .get("$defs") + .and_then(Value::as_object) + .cloned() + .unwrap_or_default(); + let properties = root + .get("properties") + .and_then(Value::as_object) + .expect("ProjectConfig schema must declare properties"); + + let mut md = String::new(); + md.push_str("---\ntitle: Configuration\n---\n\n"); + md.push_str( + "\n\n", + ); + if let Some(desc) = root.get("description").and_then(Value::as_str) { + md.push_str(&first_paragraph(desc)); + md.push_str("\n\n"); + } + md.push_str( + "Cot projects are configured via a TOML file (typically `config/dev.toml` and \ + `config/prod.toml`, loaded with\n\ + [`ProjectConfig::from_toml`](https://docs.rs/cot/latest/cot/config/struct.ProjectConfig.html#method.from_toml)).\n\ + This page lists every table and key that `ProjectConfig` understands.\n\n", + ); + md.push_str( + "Any top-level table not listed below is preserved as-is and made available to your \ + application through `ProjectConfig::extra`, for app-specific configuration.\n\n", + ); + + md.push_str("## Top-level keys\n\n"); + let mut default_toml = String::new(); + render_fields(&[], 1, properties, &defs, &mut md, &mut default_toml); + + md.push_str("## Full default configuration\n\n"); + md.push_str( + "This is a complete example with every key set explicitly to its default value. \ + Fields without a well-defined default (like `secret_key`) are shown as `\"...\"` \ + and must be set explicitly:\n\n", + ); + // `ignore`: this includes "..." placeholders, so it's not a valid, + // parseable config on its own + md.push_str("```toml,ignore\n"); + md.push_str(default_toml.trim()); + md.push('\n'); + md.push_str("```\n"); + + md +} + +enum PendingChild<'a> { + Table(Vec, &'a Map), + Tagged(Vec, &'a Vec, Option<&'a Value>), +} + +/// Renders a field table for the given `properties` into `md`, followed by a +/// section (and, recursively, its own field table) for every property that +/// is a nested table. +fn render_fields( + path: &[String], + level: usize, + properties: &Map, + defs: &Map, + md: &mut String, + default_toml: &mut String, +) { + md.push_str("| Key | Type | Default | Description |\n|---|---|---|---|\n"); + let mut own_toml = String::new(); + let mut pending: Vec> = Vec::new(); + + for (key, prop_value) in properties { + let prop = prop_value + .as_object() + .expect("property schema must be an object"); + let description = escape_table_cell(&first_paragraph( + prop.get("description") + .and_then(Value::as_str) + .unwrap_or(""), + )); + let default_val = prop.get("default"); + let resolved = deref(prop_value, defs); + + match classify(resolved) { + Kind::Table(props) => { + let child_path = extend(path, key); + let anchor = heading_anchor(&child_path); + let _ = writeln!( + md, + "| `{key}` | table | [*(see below)*](#{anchor}) | {description} |" + ); + pending.push(PendingChild::Table(child_path, props)); + } + Kind::TaggedTable(variants) => { + let child_path = extend(path, key); + let anchor = heading_anchor(&child_path); + let default_cell = tagged_default_cell(default_val, &anchor); + let _ = writeln!(md, "| `{key}` | table | {default_cell} | {description} |"); + pending.push(PendingChild::Tagged(child_path, variants, default_val)); + } + Kind::LeafEnum(variants) => { + let ty = leaf_enum_type_name(variants); + let _ = writeln!( + md, + "| `{key}` | {ty} | {} | {description} |", + default_cell(default_val, ScalarKind::String) + ); + own_toml.push_str(&default_line(key, default_val, ScalarKind::String)); + } + Kind::Scalar(kind) => { + let ty = scalar_type_name(kind); + let _ = writeln!( + md, + "| `{key}` | {ty} | {} | {description} |", + default_cell(default_val, kind) + ); + own_toml.push_str(&default_line(key, default_val, kind)); + } + } + } + md.push('\n'); + + if !own_toml.is_empty() { + if !path.is_empty() { + let _ = writeln!(default_toml, "\n[{}]", path.join(".")); + } + default_toml.push_str(&own_toml); + } + + for child in pending { + match child { + PendingChild::Table(child_path, props) => { + render_object(&child_path, level + 1, props, defs, md, default_toml); + } + PendingChild::Tagged(child_path, variants, default_val) => { + render_tagged( + &child_path, + level + 1, + variants, + default_val, + defs, + md, + default_toml, + ); + } + } + } +} + +/// Renders a `## [path]` (or deeper) section for a plain nested table. +fn render_object( + path: &[String], + level: usize, + properties: &Map, + defs: &Map, + md: &mut String, + default_toml: &mut String, +) { + let _ = writeln!(md, "{} `[{}]`\n", heading_hashes(level), path.join(".")); + render_fields(path, level, properties, defs, md, default_toml); +} + +/// Renders a `## [path]` section for an internally-tagged enum (selected via a +/// `type` key), with one sub-section per variant. +fn render_tagged( + path: &[String], + level: usize, + variants: &[Value], + default_val: Option<&Value>, + defs: &Map, + md: &mut String, + default_toml: &mut String, +) { + let _ = writeln!(md, "{} `[{}]`\n", heading_hashes(level), path.join(".")); + md.push_str("Select the variant with the `type` key:\n\n"); + + let default_type = default_val + .and_then(Value::as_object) + .and_then(|o| o.get("type")) + .and_then(Value::as_str); + + for variant in variants { + let variant = variant + .as_object() + .expect("tagged enum variant schema must be an object"); + let type_const = variant + .get("properties") + .and_then(|p| p.get("type")) + .and_then(|t| t.get("const")) + .and_then(Value::as_str) + .expect("tagged enum variant must declare a `type` const"); + let default_marker = if default_type == Some(type_const) { + " (default)" + } else { + "" + }; + let _ = writeln!( + md, + "{} `type = \"{type_const}\"`{default_marker}\n", + heading_hashes(level + 1) + ); + if let Some(desc) = variant.get("description").and_then(Value::as_str) { + md.push_str(&first_paragraph(desc)); + md.push_str("\n\n"); + } + + let other_props: Vec<(&String, &Value)> = variant + .get("properties") + .and_then(Value::as_object) + .into_iter() + .flatten() + .filter(|(k, _)| k.as_str() != "type") + .collect(); + if other_props.is_empty() { + continue; + } + md.push_str("| Key | Type | Default | Description |\n|---|---|---|---|\n"); + for (key, prop_value) in other_props { + let prop = prop_value + .as_object() + .expect("property schema must be an object"); + let description = escape_table_cell(&first_paragraph( + prop.get("description") + .and_then(Value::as_str) + .unwrap_or(""), + )); + let default_val = prop.get("default"); + let resolved = deref(prop_value, defs); + match classify(resolved) { + Kind::Scalar(kind) => { + let ty = scalar_type_name(kind); + let _ = writeln!( + md, + "| `{key}` | {ty} | {} | {description} |", + default_cell(default_val, kind) + ); + } + Kind::LeafEnum(variants) => { + let ty = leaf_enum_type_name(variants); + let _ = writeln!( + md, + "| `{key}` | {ty} | {} | {description} |", + default_cell(default_val, ScalarKind::String) + ); + } + Kind::Table(_) | Kind::TaggedTable(_) => { + unimplemented!( + "tagged-enum variant field `{key}` is a nested table, which this generator doesn't handle yet" + ); + } + } + } + md.push('\n'); + } + + let mut own_toml = String::new(); + if let Some(Value::Object(default_obj)) = default_val { + for (key, value) in default_obj { + if let Some(line) = json_scalar_to_toml_line(key, value) { + own_toml.push_str(&line); + } + } + } + if !own_toml.is_empty() { + let _ = writeln!(default_toml, "\n[{}]", path.join(".")); + default_toml.push_str(&own_toml); + } +} + +enum Kind<'a> { + Table(&'a Map), + TaggedTable(&'a Vec), + LeafEnum(&'a Vec), + Scalar(ScalarKind), +} + +#[derive(Clone, Copy)] +enum ScalarKind { + Bool, + Integer, + String, + Array, +} + +/// Classifies an already-dereferenced schema node. +fn classify(resolved: &Value) -> Kind<'_> { + let Some(obj) = resolved.as_object() else { + return Kind::Scalar(ScalarKind::String); + }; + if let Some(Value::Array(one_of)) = obj.get("oneOf") { + let tagged = !one_of.is_empty() && one_of.iter().all(|v| v.get("properties").is_some()); + return if tagged { + Kind::TaggedTable(one_of) + } else { + Kind::LeafEnum(one_of) + }; + } + if let Some(Value::Object(props)) = obj.get("properties") { + return Kind::Table(props); + } + let ty_str = match obj.get("type") { + Some(Value::String(s)) => s.as_str(), + Some(Value::Array(arr)) => arr + .iter() + .filter_map(Value::as_str) + .find(|s| *s != "null") + .unwrap_or("string"), + _ => "string", + }; + Kind::Scalar(match ty_str { + "boolean" => ScalarKind::Bool, + "integer" | "number" => ScalarKind::Integer, + "array" => ScalarKind::Array, + _ => ScalarKind::String, + }) +} + +/// Follows a `$ref` (one level - this schema never nests them further) or picks +/// the non-null branch of an `anyOf` (produced by `Option` fields), +/// returning the schema node that actually describes the field's shape. +fn deref<'a>(prop: &'a Value, defs: &'a Map) -> &'a Value { + let Some(obj) = prop.as_object() else { + return prop; + }; + if let Some(Value::String(r)) = obj.get("$ref") { + let name = r.rsplit('/').next().unwrap_or(r); + if let Some(target) = defs.get(name) { + return target; + } + } + if let Some(Value::Array(any_of)) = obj.get("anyOf") { + for branch in any_of { + if branch.get("type").and_then(Value::as_str) != Some("null") { + return deref(branch, defs); + } + } + } + prop +} + +fn scalar_type_name(kind: ScalarKind) -> &'static str { + match kind { + ScalarKind::Bool => "boolean", + ScalarKind::Integer => "integer", + ScalarKind::String => "string", + ScalarKind::Array => "array of strings", + } +} + +fn leaf_enum_type_name(variants: &[Value]) -> String { + let values: Vec = variants + .iter() + .filter_map(|v| v.get("const").and_then(Value::as_str)) + .map(|s| format!("`\"{s}\"`")) + .collect(); + values.join(", ") +} + +fn default_cell(default_val: Option<&Value>, kind: ScalarKind) -> String { + match json_scalar_repr(default_val, kind) { + Some(s) => format!("`{s}`"), + None => "—".to_string(), + } +} + +/// Renders the Default cell for a tagged-enum table field: the concrete +/// default variant's `type` tag, linked to its subsection, e.g. +/// `` [`type = "none"`](#auth_backend) ``. Falls back to a generic linked +/// "see below" if the field has no representable default (i.e. it's +/// required). +fn tagged_default_cell(default_val: Option<&Value>, anchor: &str) -> String { + let default_type = default_val + .and_then(Value::as_object) + .and_then(|o| o.get("type")) + .and_then(Value::as_str); + match default_type { + Some(t) => format!("[`type = \"{t}\"`](#{anchor})"), + None => format!("[*(see below)*](#{anchor})"), + } +} + +/// Computes the anchor fragment that cot-site's Markdown renderer generates for +/// a `` `[path]` `` heading. +fn heading_anchor(path: &[String]) -> String { + // every path segment here is a lowercase Rust identifier (letters, digits, + // underscores only, no spaces), so the only characters the algorithm + // actually strips are the heading's own brackets and the dots joining + // the segments - i.e. it reduces to concatenating the segments as-is. + path.concat() +} + +/// Renders a field's line for the "full default configuration" TOML example. +/// +/// Fields with a representable default (see [`json_scalar_repr`]) get that +/// default; fields without one (e.g. `secret_key`, which is required and has +/// no sensible default) get a `"..."` placeholder instead, so every key the +/// config accepts still shows up in the example rather than being silently +/// dropped from it. +fn default_line(key: &str, default_val: Option<&Value>, kind: ScalarKind) -> String { + let value = json_scalar_repr(default_val, kind).unwrap_or_else(|| "\"...\"".to_string()); + format!("{key} = {value}\n") +} + +/// Renders a schema `default` value as a TOML-literal string, but only when it +/// actually matches the field's declared scalar type. +fn json_scalar_repr(default_val: Option<&Value>, kind: ScalarKind) -> Option { + match (kind, default_val?) { + (ScalarKind::Bool, Value::Bool(b)) => Some(b.to_string()), + (ScalarKind::Integer, Value::Number(n)) => Some(n.to_string()), + (ScalarKind::String, Value::String(s)) => Some(toml_quote(s)), + (ScalarKind::Array, Value::Array(items)) => { + let mut rendered = Vec::with_capacity(items.len()); + for item in items { + match item { + Value::String(s) => rendered.push(toml_quote(s)), + _ => return None, + } + } + Some(format!("[{}]", rendered.join(", "))) + } + _ => None, + } +} + +fn json_scalar_to_toml_line(key: &str, value: &Value) -> Option { + match value { + Value::Bool(b) => Some(format!("{key} = {b}\n")), + Value::Number(n) => Some(format!("{key} = {n}\n")), + Value::String(s) => Some(format!("{key} = {}\n", toml_quote(s))), + _ => None, + } +} + +fn toml_quote(s: &str) -> String { + format!("{s:?}") +} + +fn heading_hashes(level: usize) -> String { + "#".repeat(level.clamp(2, 6)) +} + +fn extend(path: &[String], key: &str) -> Vec { + let mut v = path.to_vec(); + v.push(key.to_string()); + v +} + +fn escape_table_cell(s: &str) -> String { + s.replace('|', "\\|") +} + +/// Extracts the first paragraph of a rustdoc description. +fn first_paragraph(desc: &str) -> String { + let mut lines = Vec::new(); + for line in desc.lines() { + let trimmed = line.trim(); + if trimmed.starts_with('#') || trimmed.starts_with("```") { + break; + } + if trimmed.is_empty() { + if lines.is_empty() { + continue; + } + break; + } + lines.push(trimmed); + } + lines.join(" ") +} diff --git a/cot-test/src/lib.rs b/cot-test/src/lib.rs index 7159b69b0..02019acd8 100644 --- a/cot-test/src/lib.rs +++ b/cot-test/src/lib.rs @@ -8,6 +8,9 @@ use cot_cli::new_project::{CotSource, new_project}; use libtest_mimic::Failed; use thiserror::Error; +#[cfg(feature = "config-docs")] +pub mod config_reference; + pub const COMMON_IMPORTS: &[&str] = &[ "cot::db::*", "cot::request::extractors::*", diff --git a/cot-test/tests/config_reference.rs b/cot-test/tests/config_reference.rs new file mode 100644 index 000000000..a3f2e5d40 --- /dev/null +++ b/cot-test/tests/config_reference.rs @@ -0,0 +1,25 @@ +//! Verifies that `docs/configuration.md` is up to date with +//! `cot::config::ProjectConfig`'s current type definition. + +use std::path::PathBuf; + +#[test] +fn config_reference_is_up_to_date() { + let generated = cot_test::config_reference::generate_config_reference(); + + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let docs_path = manifest_dir + .parent() + .expect("failed to get workspace path") + .join("docs") + .join("configuration.md"); + let committed = std::fs::read_to_string(&docs_path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", docs_path.display())) + .replace("\r\n", "\n"); // normalize line endings + + assert_eq!( + generated, committed, + "docs/configuration.md is out of date with cot::config::ProjectConfig; \ + run `just generate-config-docs` and commit the result" + ); +} diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 8d8e0030d..c8a4a944f 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -119,6 +119,8 @@ swagger-ui = ["openapi", "dep:swagger-ui-redist"] live-reload = ["dep:tower-livereload"] cache = ["json"] test = [] +# Internal feature used to generate the configuration file reference (docs/configuration.md). +_internal_config-docs = ["dep:schemars", "schemars/std", "schemars/preserve_order", "full"] [lib] bench = false diff --git a/cot/src/config.rs b/cot/src/config.rs index fa6e7ddb6..a6226bf5e 100644 --- a/cot/src/config.rs +++ b/cot/src/config.rs @@ -35,6 +35,7 @@ use crate::utils::chrono::DateTimeWithOffsetAdapter; /// This is all the project-specific configuration data that can (and makes /// sense to) be expressed in a TOML configuration file. #[derive(Debug, Clone, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] @@ -150,7 +151,7 @@ pub struct ProjectConfig { /// # Ok::<(), cot::Error>(()) /// ``` pub auth_backend: AuthBackendConfig, - /// Configuration related to the database. + /// Database configuration. /// /// # Examples /// @@ -172,7 +173,7 @@ pub struct ProjectConfig { /// ``` #[cfg(feature = "db")] pub database: DatabaseConfig, - /// Configuration related to the cache. + /// Cache subsystem configuration. /// /// # Examples /// @@ -204,7 +205,7 @@ pub struct ProjectConfig { /// ``` #[cfg(feature = "cache")] pub cache: CacheConfig, - /// Configuration related to the static files. + /// Static files configuration. /// /// # Examples /// @@ -234,7 +235,7 @@ pub struct ProjectConfig { /// # Ok::<(), cot::Error>(()) /// ``` pub static_files: StaticFilesConfig, - /// Configuration related to the middlewares. + /// Middleware configuration. /// /// # Examples /// @@ -252,7 +253,7 @@ pub struct ProjectConfig { /// # Ok::<(), cot::Error>(()) /// ``` pub middlewares: MiddlewareConfig, - /// Configuration related to the email backend. + /// Email backend configuration. /// /// # Examples /// @@ -304,6 +305,7 @@ pub struct ProjectConfig { /// # Ok::<(), Box>(()) /// ``` #[serde(flatten)] + #[cfg_attr(feature = "_internal_config-docs", schemars(skip))] pub extra: toml::Table, } @@ -430,6 +432,7 @@ impl ProjectConfigBuilder { /// let config = AuthBackendConfig::Database; /// ``` #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(tag = "type", rename_all = "snake_case")] #[non_exhaustive] pub enum AuthBackendConfig { @@ -461,6 +464,7 @@ pub enum AuthBackendConfig { /// ``` #[cfg(feature = "db")] #[derive(Debug, Default, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] @@ -605,6 +609,7 @@ const MAX_RETRIES_DEFAULT: u32 = 3; #[cfg(feature = "cache")] #[derive(Debug, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] @@ -674,6 +679,8 @@ pub struct CacheConfig { /// timeout = "2h" /// ``` #[serde(with = "crate::serializers::cache_timeout")] + #[cfg_attr(feature = "_internal_config-docs", schemars(with = "String"))] + // TODO: Option is wrong pub timeout: Timeout, /// Prefix for cache keys. @@ -792,6 +799,7 @@ impl CacheConfig { #[cfg(feature = "cache")] #[derive(Debug, Default, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] /// Configuration for the cache store backend. @@ -903,6 +911,7 @@ const fn is_default_redis_pool_size(size: &usize) -> bool { /// assert_eq!(mem, CacheStoreTypeConfig::Memory); /// ``` #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(tag = "type", rename_all = "snake_case")] #[non_exhaustive] #[cfg(feature = "cache")] @@ -927,6 +936,8 @@ pub enum CacheStoreTypeConfig { /// must be specified, and additional Redis-specific options can be /// configured. Redis { + /// The URL of the Redis server. + /// /// # Examples /// /// ``` @@ -937,7 +948,6 @@ pub enum CacheStoreTypeConfig { /// pool_size: 20, /// }; /// ``` - /// The URL of the Redis server. url: CacheUrl, /// Connection pool size for Redis connections. @@ -954,6 +964,8 @@ pub enum CacheStoreTypeConfig { /// This stores cache data in files on the local filesystem. The path to /// the directory where the cache files will be stored must be specified. File { + /// The path to the directory where cache files will be stored. + /// /// # Examples /// /// ``` @@ -965,7 +977,6 @@ pub enum CacheStoreTypeConfig { /// path: PathBuf::from("/tmp/cache"), /// }; /// ``` - /// The path to the directory where cache files will be stored. path: PathBuf, }, } @@ -1013,18 +1024,21 @@ pub enum CacheStoreTypeConfig { /// .build(); /// ``` #[derive(Debug, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] pub struct StaticFilesConfig { /// The URL prefix for the static files to be served at (which should - /// typically end with a slash). The default is `/static/`. + /// typically end with a slash). /// /// This prefix is used to determine which requests should be handled by the /// static files middleware. For example, if set to `/assets/`, then /// requests to `/assets/style.css` will be served from the static files /// directory. /// + /// The default is `/static/`. + /// /// # Examples /// /// ``` @@ -1104,6 +1118,7 @@ pub struct StaticFilesConfig { /// # Ok::<(), cot::Error>(()) /// ``` #[serde(with = "crate::serializers::humantime")] + #[cfg_attr(feature = "_internal_config-docs", schemars(with = "Option"))] #[builder(setter(strip_option), default)] pub cache_timeout: Option, } @@ -1112,6 +1127,7 @@ pub struct StaticFilesConfig { /// /// This is used as part of the [`StaticFilesConfig`] struct. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum StaticFilesPathRewriteMode { @@ -1193,6 +1209,7 @@ impl StaticFilesConfig { /// .build(); /// ``` #[derive(Debug, Default, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] @@ -1254,6 +1271,7 @@ impl MiddlewareConfigBuilder { /// let config = LiveReloadMiddlewareConfig::builder().enabled(true).build(); /// ``` #[derive(Debug, Default, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] @@ -1333,6 +1351,7 @@ impl LiveReloadMiddlewareConfigBuilder { /// }; /// ``` #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", tag = "type")] pub enum SessionStoreTypeConfig { /// In-memory session storage. @@ -1413,6 +1432,7 @@ pub enum SessionStoreTypeConfig { /// ``` #[derive(Debug, Default, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] pub struct SessionStoreConfig { @@ -1489,6 +1509,7 @@ impl SessionStoreConfigBuilder { /// /// [`SameSite`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#controlling_third-party_cookies_with_samesite #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum SameSite { @@ -1599,14 +1620,15 @@ impl From for tower_sessions::Expiry { /// let config = SessionMiddlewareConfig::builder().secure(false).build(); /// ``` #[derive(Debug, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] pub struct SessionMiddlewareConfig { - /// The [`Secure`] of the cookie determines whether the session middleware - /// is secure. + /// The + /// [`Secure`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies) + /// of the cookie determines whether the session middleware is secure. /// - /// [`Secure`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies /// # Examples /// /// ``` @@ -1615,10 +1637,9 @@ pub struct SessionMiddlewareConfig { /// let config = SessionMiddlewareConfig::builder().secure(false).build(); /// ``` pub secure: bool, - /// The [`HttpOnly`] of the cookie used for the session. It is set to `true` - /// by default. - /// - /// [`HttpOnly`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies + /// The + /// [`HttpOnly`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies) + /// of the cookie used for the session. It is set to `true` by default. /// /// # Examples /// @@ -1628,10 +1649,11 @@ pub struct SessionMiddlewareConfig { /// let config = SessionMiddlewareConfig::builder().http_only(true).build(); /// ``` pub http_only: bool, - /// The [`SameSite`] attribute of the cookie used for the session. - /// The default value is [`SameSite::Strict`] + /// The + /// [`SameSite`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#controlling_third-party_cookies_with_samesite) + /// attribute of the cookie used for the session. /// - /// [`SameSite`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#controlling_third-party_cookies_with_samesite + /// The default value is [`SameSite::Strict`]. /// /// # Examples /// @@ -1644,10 +1666,11 @@ pub struct SessionMiddlewareConfig { /// ``` pub same_site: SameSite, - /// The [`Domain`] attribute of the cookie used for the session. When not - /// explicitly configured, it is set to `None` by default. + /// The + /// [`Domain`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#define_where_cookies_are_sent) + /// attribute of the cookie used for the session. /// - /// [`Domain`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#define_where_cookies_are_sent + /// When not explicitly configured, it is set to [`None`] by default. /// /// # Examples /// @@ -1660,10 +1683,11 @@ pub struct SessionMiddlewareConfig { /// ``` #[builder(setter(strip_option), default)] pub domain: Option, - /// The [`Path`] attribute of the cookie used for the session. It is set to - /// `/` by default. + /// The + /// [`Path`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#define_where_cookies_are_sent) + /// attribute of the cookie used for the session. /// - /// [`Path`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#define_where_cookies_are_sent + /// It is set to `/` by default. /// /// # Examples /// @@ -1677,8 +1701,9 @@ pub struct SessionMiddlewareConfig { /// .build(); /// ``` pub path: String, - /// The name of the cookie used for the session. It is set to "id" by - /// default. + /// The name of the cookie used for the session. + /// + /// It is set to "id" by default. /// /// # Examples /// @@ -1692,7 +1717,9 @@ pub struct SessionMiddlewareConfig { pub name: String, /// Whether the unmodified session should be saved on read or not. /// If set to `true`, the session will be saved even if it was not modified. + /// /// It is set to `false` by default. + /// /// # Examples /// /// ``` @@ -1701,7 +1728,9 @@ pub struct SessionMiddlewareConfig { /// let config = SessionMiddlewareConfig::builder().always_save(true).build(); /// ``` pub always_save: bool, - /// The [`Expiry`] behavior for session cookies. + /// The + /// [`Expiry`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#removal_defining_the_lifetime_of_a_cookie) + /// behavior for session cookies. /// /// This controls when the session cookie expires and how long it remains /// valid. The expiry behavior affects how the cookie's `max-age` and @@ -1727,8 +1756,6 @@ pub struct SessionMiddlewareConfig { /// - For `AtDateTime`: Use a valid RFC 3339/ISO 8601 formatted timestamp /// (e.g., `"2025-12-31T23:59:59+00:00"`). /// - /// [`Expiry`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#removal_defining_the_lifetime_of_a_cookie - /// /// # Examples /// /// ``` @@ -1777,6 +1804,7 @@ pub struct SessionMiddlewareConfig { /// ); /// ``` #[serde(with = "crate::serializers::session_expiry_time")] + #[cfg_attr(feature = "_internal_config-docs", schemars(with = "String"))] pub expiry: Expiry, /// What session store to use. @@ -1863,10 +1891,12 @@ impl Default for SessionMiddlewareConfig { /// The default backend if not specified is `console`. #[cfg(feature = "email")] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(tag = "type", rename_all = "snake_case")] #[non_exhaustive] pub enum EmailTransportTypeConfig { - /// Console email transport backend. + /// Console email transport backend that prints the contents to the standard + /// output. /// /// This is a convenient transport backend for development and testing that /// simply prints the email contents to the console instead of actually @@ -1920,6 +1950,7 @@ pub enum EmailTransportTypeConfig { /// ``` url: EmailUrl, /// The authentication mechanism to use. + /// /// Supported mechanisms are `plain`, `login`, and `xoauth2`. /// /// # TOML Configuration @@ -1940,6 +1971,7 @@ pub enum EmailTransportTypeConfig { /// configuration. #[cfg(feature = "email")] #[derive(Debug, Default, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] pub struct EmailTransportConfig { @@ -2018,6 +2050,7 @@ impl EmailTransportConfigBuilder { /// ``` #[cfg(feature = "email")] #[derive(Debug, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] pub struct EmailConfig { @@ -2132,7 +2165,9 @@ impl Default for EmailConfig { /// ``` #[repr(transparent)] #[derive(Clone, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(from = "String")] +#[cfg_attr(feature = "_internal_config-docs", schemars(with = "String"))] pub struct SecretKey(SecureBytes); impl Serialize for SecretKey { @@ -2239,7 +2274,9 @@ impl From<&str> for SecretKey { /// let url = DatabaseUrl::from("postgres://user:password@localhost:5432/database"); /// ``` #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(transparent)] +#[cfg_attr(feature = "_internal_config-docs", schemars(with = "String"))] #[cfg(feature = "db")] pub struct DatabaseUrl(url::Url); @@ -2348,7 +2385,9 @@ impl std::str::FromStr for CacheType { /// let url = CacheUrl::from("redis://user:password@localhost:6379/0"); /// ``` #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(transparent)] +#[cfg_attr(feature = "_internal_config-docs", schemars(with = "String"))] #[cfg(feature = "cache")] pub struct CacheUrl(url::Url); @@ -2457,7 +2496,9 @@ impl std::fmt::Display for CacheUrl { /// let url = EmailUrl::from("smtp://user:pass@hostname:587"); /// ``` #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(transparent)] +#[cfg_attr(feature = "_internal_config-docs", schemars(with = "String"))] #[cfg(feature = "email")] pub struct EmailUrl(url::Url); diff --git a/cot/src/email/transport/smtp.rs b/cot/src/email/transport/smtp.rs index 9fadbd454..f9a2299ed 100644 --- a/cot/src/email/transport/smtp.rs +++ b/cot/src/email/transport/smtp.rs @@ -70,6 +70,7 @@ impl From for TransportError { /// /// The default is `Plain`. #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "_internal_config-docs", derive(schemars::JsonSchema))] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum Mechanism { diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 000000000..31759397d --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,218 @@ +--- +title: Configuration +--- + + + +The configuration for a project. + +Cot projects are configured via a TOML file (typically `config/dev.toml` and `config/prod.toml`, loaded with +[`ProjectConfig::from_toml`](https://docs.rs/cot/latest/cot/config/struct.ProjectConfig.html#method.from_toml)). +This page lists every table and key that `ProjectConfig` understands. + +Any top-level table not listed below is preserved as-is and made available to your application through `ProjectConfig::extra`, for app-specific configuration. + +## Top-level keys + +| Key | Type | Default | Description | +|---|---|---|---| +| `debug` | boolean | `true` | Debug mode flag. | +| `register_panic_hook` | boolean | `true` | Whether to register a panic hook. | +| `secret_key` | string | — | The secret key used for signing cookies and other sensitive data. This is a cryptographic key, should be kept secret, and should be set to a random and unique value for each project. | +| `fallback_secret_keys` | array of strings | `[]` | Fallback secret keys that can be used to verify old sessions. | +| `auth_backend` | table | [`type = "none"`](#auth_backend) | The authentication backend to use. | +| `database` | table | [*(see below)*](#database) | Database configuration. | +| `cache` | table | [*(see below)*](#cache) | Cache subsystem configuration. | +| `static_files` | table | [*(see below)*](#static_files) | Static files configuration. | +| `middlewares` | table | [*(see below)*](#middlewares) | Middleware configuration. | +| `email` | table | [*(see below)*](#email) | Email backend configuration. | + +## `[auth_backend]` + +Select the variant with the `type` key: + +### `type = "none"` (default) + +No authentication backend. + +### `type = "database"` + +Database authentication backend. + +## `[database]` + +| Key | Type | Default | Description | +|---|---|---|---| +| `url` | string | — | The URL of the database, possibly with username, password, and other options. | + +## `[cache]` + +| Key | Type | Default | Description | +|---|---|---|---| +| `max_retries` | integer | `3` | Maximum number of retries for cache operations. | +| `timeout` | string | `"5m"` | Timeout for cache operations. | +| `prefix` | string | — | Prefix for cache keys. | +| `store` | table | [`type = "memory"`](#cachestore) | The cache store configuration. | + +### `[cache.store]` + +Select the variant with the `type` key: + +#### `type = "memory"` (default) + +In-memory cache store. + +#### `type = "redis"` + +Redis cache store. This stores cache data in a Redis instance. The URL to the Redis server must be specified, and additional Redis-specific options can be configured. + +| Key | Type | Default | Description | +|---|---|---|---| +| `url` | string | — | The URL of the Redis server. | +| `pool_size` | integer | — | Connection pool size for Redis connections. This controls how many connections to maintain in the connection pool. When not specified, a default pool size of `10` is used. | + +#### `type = "file"` + +File-based cache store. This stores cache data in files on the local filesystem. The path to the directory where the cache files will be stored must be specified. + +| Key | Type | Default | Description | +|---|---|---|---| +| `path` | string | — | The path to the directory where cache files will be stored. | + +## `[static_files]` + +| Key | Type | Default | Description | +|---|---|---|---| +| `url` | string | `"/static/"` | The URL prefix for the static files to be served at (which should typically end with a slash). | +| `rewrite` | `"none"`, `"query_param"` | `"none"` | The URL rewriting mode for the static files. This is useful to allow long-lived caching of static files, while still allowing to invalidate the cache when the file changes. | +| `cache_timeout` | string | — | The duration for which static files should be cached by browsers. | + +## `[middlewares]` + +| Key | Type | Default | Description | +|---|---|---|---| +| `live_reload` | table | [*(see below)*](#middlewareslive_reload) | The configuration for the live reload middleware. | +| `session` | table | [*(see below)*](#middlewaressession) | The configuration for the session middleware. | + +### `[middlewares.live_reload]` + +| Key | Type | Default | Description | +|---|---|---|---| +| `enabled` | boolean | `false` | Whether the live reload middleware is enabled. | + +### `[middlewares.session]` + +| Key | Type | Default | Description | +|---|---|---|---| +| `secure` | boolean | `true` | The [`Secure`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies) of the cookie determines whether the session middleware is secure. | +| `http_only` | boolean | `true` | The [`HttpOnly`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies) of the cookie used for the session. It is set to `true` by default. | +| `same_site` | `"strict"`, `"lax"`, `"none"` | `"strict"` | The [`SameSite`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#controlling_third-party_cookies_with_samesite) attribute of the cookie used for the session. | +| `domain` | string | — | The [`Domain`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#define_where_cookies_are_sent) attribute of the cookie used for the session. | +| `path` | string | `"/"` | The [`Path`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#define_where_cookies_are_sent) attribute of the cookie used for the session. | +| `name` | string | `"id"` | The name of the cookie used for the session. | +| `always_save` | boolean | `false` | Whether the unmodified session should be saved on read or not. If set to `true`, the session will be saved even if it was not modified. | +| `expiry` | string | — | The [`Expiry`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#removal_defining_the_lifetime_of_a_cookie) behavior for session cookies. | +| `store` | table | [`type = "memory"`](#middlewaressessionstore) | What session store to use. | + +#### `[middlewares.session.store]` + +Select the variant with the `type` key: + +##### `type = "memory"` (default) + +In-memory session storage. + +##### `type = "database"` + +Database-backed session storage. + +##### `type = "file"` + +File-based session storage. + +| Key | Type | Default | Description | +|---|---|---|---| +| `path` | string | — | The path to the directory where session files will be stored. | + +##### `type = "cache"` + +Cache-based session storage. + +| Key | Type | Default | Description | +|---|---|---|---| +| `uri` | string | — | The URI to the cache service. | + +## `[email]` + +| Key | Type | Default | Description | +|---|---|---|---| +| `transport` | table | [`type = "console"`](#emailtransport) | The type of email transport backend to use. | + +### `[email.transport]` + +Select the variant with the `type` key: + +#### `type = "console"` (default) + +Console email transport backend that prints the contents to the standard output. + +#### `type = "smtp"` + +SMTP email transport backend. + +| Key | Type | Default | Description | +|---|---|---|---| +| `url` | string | — | The SMTP connection URL. | +| `mechanism` | `"plain"`, `"login"`, `"xoauth2"` | — | The authentication mechanism to use. | + +## Full default configuration + +This is a complete example with every key set explicitly to its default value. Fields without a well-defined default (like `secret_key`) are shown as `"..."` and must be set explicitly: + +```toml,ignore +debug = true +register_panic_hook = true +secret_key = "..." +fallback_secret_keys = [] + +[auth_backend] +type = "none" + +[database] +url = "..." + +[cache] +max_retries = 3 +timeout = "5m" +prefix = "..." + +[cache.store] +type = "memory" + +[static_files] +url = "/static/" +rewrite = "none" +cache_timeout = "..." + +[middlewares.live_reload] +enabled = false + +[middlewares.session] +secure = true +http_only = true +same_site = "strict" +domain = "..." +path = "/" +name = "id" +always_save = false +expiry = "..." + +[middlewares.session.store] +type = "memory" + +[email.transport] +type = "console" +``` diff --git a/docs/site/Cargo.lock b/docs/site/Cargo.lock index 5bd6010a9..875d8e346 100644 --- a/docs/site/Cargo.lock +++ b/docs/site/Cargo.lock @@ -864,9 +864,9 @@ checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" [[package]] name = "comrak" -version = "0.54.0" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d5910408554659ed848ff469e67ec83b30f179e72cec286cfdae64d1616f466" +checksum = "daa3d1ea6b01ce72405fa3f826768f51c7a44917527c89b37856ffdd17ee57e4" dependencies = [ "bon", "caseless", @@ -1013,10 +1013,10 @@ checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" [[package]] name = "cot" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4885a939f3f5911649f77328b1059a42b347d4159c460b8e91e624507fb49211" +source = "git+https://github.com/cot-rs/cot.git?rev=1638ac877352d22ce98d1b7f2f6e75d13fc519e8#1638ac877352d22ce98d1b7f2f6e75d13fc519e8" dependencies = [ "ahash 0.8.12", + "anstyle", "askama", "async-trait", "axum", @@ -1047,6 +1047,7 @@ dependencies = [ "pin-project-lite", "securer-string", "serde", + "serde_json", "subtle", "thiserror 2.0.20", "time", @@ -1062,7 +1063,7 @@ dependencies = [ [[package]] name = "cot-site" version = "0.1.0" -source = "git+https://github.com/cot-rs/cot-site.git?rev=6c68a1b02d8aaba99eca6828101d77b55a0894f5#6c68a1b02d8aaba99eca6828101d77b55a0894f5" +source = "git+https://github.com/cot-rs/cot-site.git?rev=b67e46aaa6901913768ffd80a8737a79cba39db1#b67e46aaa6901913768ffd80a8737a79cba39db1" dependencies = [ "askama", "async-trait", @@ -1083,7 +1084,7 @@ dependencies = [ [[package]] name = "cot-site-common" version = "0.1.0" -source = "git+https://github.com/cot-rs/cot-site.git?rev=6c68a1b02d8aaba99eca6828101d77b55a0894f5#6c68a1b02d8aaba99eca6828101d77b55a0894f5" +source = "git+https://github.com/cot-rs/cot-site.git?rev=b67e46aaa6901913768ffd80a8737a79cba39db1#b67e46aaa6901913768ffd80a8737a79cba39db1" dependencies = [ "comrak", "semver", @@ -1094,7 +1095,7 @@ dependencies = [ [[package]] name = "cot-site-macros" version = "0.1.0" -source = "git+https://github.com/cot-rs/cot-site.git?rev=6c68a1b02d8aaba99eca6828101d77b55a0894f5#6c68a1b02d8aaba99eca6828101d77b55a0894f5" +source = "git+https://github.com/cot-rs/cot-site.git?rev=b67e46aaa6901913768ffd80a8737a79cba39db1#b67e46aaa6901913768ffd80a8737a79cba39db1" dependencies = [ "comrak", "cot-site-common", @@ -1109,21 +1110,19 @@ dependencies = [ [[package]] name = "cot_codegen" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc699bdab0ce9c3c5b78fa498071d07762ba451f0c8e27ea3c11e9b77f4722ca" +source = "git+https://github.com/cot-rs/cot.git?rev=1638ac877352d22ce98d1b7f2f6e75d13fc519e8#1638ac877352d22ce98d1b7f2f6e75d13fc519e8" dependencies = [ - "darling 0.23.0", + "darling 0.24.1", "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] name = "cot_core" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b20b72eb437a91f2876377a14edb358b1ba7cac016a90fcb0cc424ea5833bc" +source = "git+https://github.com/cot-rs/cot.git?rev=1638ac877352d22ce98d1b7f2f6e75d13fc519e8#1638ac877352d22ce98d1b7f2f6e75d13fc519e8" dependencies = [ "askama", "axum", @@ -1151,17 +1150,16 @@ dependencies = [ [[package]] name = "cot_macros" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7321f4664bea3ba6c2138343fa88e9b9c3e8e1d7ad68ff8ddad7d4c7ec771435" +source = "git+https://github.com/cot-rs/cot.git?rev=1638ac877352d22ce98d1b7f2f6e75d13fc519e8#1638ac877352d22ce98d1b7f2f6e75d13fc519e8" dependencies = [ "askama_derive", "cot_codegen", - "darling 0.23.0", + "darling 0.24.1", "heck 0.5.0", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -1300,16 +1298,6 @@ dependencies = [ "darling_macro 0.20.11", ] -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - [[package]] name = "darling" version = "0.24.1" @@ -1334,19 +1322,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", -] - [[package]] name = "darling_core" version = "0.24.1" @@ -1371,17 +1346,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.119", -] - [[package]] name = "darling_macro" version = "0.24.1" diff --git a/docs/site/Cargo.toml b/docs/site/Cargo.toml index abf471cb8..19a60beb2 100644 --- a/docs/site/Cargo.toml +++ b/docs/site/Cargo.toml @@ -7,5 +7,5 @@ edition = "2024" license = "MIT OR Apache-2.0" [dependencies] -cot = { version = "0.7", default-features = false } -cot-site = { git = "https://github.com/cot-rs/cot-site.git", rev = "6c68a1b02d8aaba99eca6828101d77b55a0894f5" } +cot = { git = "https://github.com/cot-rs/cot.git", rev = "1638ac877352d22ce98d1b7f2f6e75d13fc519e8", default-features = false } +cot-site = { git = "https://github.com/cot-rs/cot-site.git", rev = "b67e46aaa6901913768ffd80a8737a79cba39db1" } diff --git a/docs/site/src/main.rs b/docs/site/src/main.rs index 256ff5ee8..ad02bd718 100644 --- a/docs/site/src/main.rs +++ b/docs/site/src/main.rs @@ -35,6 +35,7 @@ impl Project for CotSiteProject { "Getting started", vec![ GuideItem::Page(md_page!("introduction")), + GuideItem::Page(md_page!("configuration")), GuideItem::Page(md_page!("templates")), GuideItem::Page(md_page!("forms")), GuideItem::SubCategory { diff --git a/justfile b/justfile index ab5239fa5..80e7c9c81 100644 --- a/justfile +++ b/justfile @@ -78,3 +78,7 @@ alias td := test-docs test-docs: cargo nextest run -p cot-test + +generate-config-docs: + # regenerates docs/configuration.md from `cot::config::ProjectConfig` + cargo run -p cot-test --features config-docs --bin generate_config_docs