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