use crate::error::{Result, RototoError};
#[derive(Clone, Copy, Debug, serde::Serialize)]
pub struct DocPage {
pub id: &'static str,
pub title: &'static str,
pub list_title: &'static str,
pub markdown: &'static str,
}
#[derive(Clone, Copy, Debug, serde::Serialize)]
pub struct DocNavSection {
pub title: &'static str,
pub pages: &'static [&'static str],
}
#[derive(Debug)]
pub enum DocPageLookup {
Found(&'static DocPage),
Ambiguous {
query: String,
pages: Vec<&'static DocPage>,
},
NotFound {
query: String,
pages: Vec<&'static DocPage>,
},
}
pub const DOCS: &[DocPage] = &[
DocPage {
id: "index",
title: "rototo introduction",
list_title: "Introduction: runtime configuration control plane, workspace model, and refresh loop",
markdown: include_str!("../docs/src/index.md"),
},
DocPage {
id: "why-rototo",
title: "Why rototo",
list_title: "Why rototo: runtime configuration failure modes and the Git-backed control-plane model",
markdown: include_str!("../docs/src/concepts/why-rototo.md"),
},
DocPage {
id: "rototo-model",
title: "The rototo Model",
list_title: "Model: workspaces, environments, qualifiers, variables, schemas, values, and resolution",
markdown: include_str!("../docs/src/concepts/rototo-model.md"),
},
DocPage {
id: "quickstart",
title: "Quickstart",
list_title: "Quickstart: create a local workspace, lint it, and resolve one variable",
markdown: include_str!("../docs/src/tutorials/quickstart.md"),
},
DocPage {
id: "production-workflow",
title: "Production Workflow",
list_title: "Production workflow: Git review, schemas, tests, app loading, refresh, and observability",
markdown: include_str!("../docs/src/tutorials/production-workflow.md"),
},
DocPage {
id: "how-to-add-a-new-runtime-config-value",
title: "How to Add a New Runtime Config Value",
list_title: "Add a runtime config value: define values, defaults, rules, and validation",
markdown: include_str!("../docs/src/how-to/how-to-add-a-new-runtime-config-value.md"),
},
DocPage {
id: "how-to-change-a-config-value-for-one-environment",
title: "How to Change a Config Value for One Environment",
list_title: "Change one environment: update an environment block without changing other deployments",
markdown: include_str!(
"../docs/src/how-to/how-to-change-a-config-value-for-one-environment.md"
),
},
DocPage {
id: "how-to-add-a-new-context-field",
title: "How to Add a New Context Field",
list_title: "Add a context field: extend request facts, schema validation, and qualifier usage",
markdown: include_str!("../docs/src/how-to/how-to-add-a-new-context-field.md"),
},
DocPage {
id: "how-to-select-a-value-for-a-runtime-condition",
title: "How to Select a Value for a Runtime Condition",
list_title: "Select by runtime condition: create a qualifier-backed rule for one variable",
markdown: include_str!(
"../docs/src/how-to/how-to-select-a-value-for-a-runtime-condition.md"
),
},
DocPage {
id: "how-to-move-large-values-out-of-toml",
title: "How to Move Large Values Out of TOML",
list_title: "Move large values out of TOML: split variable values into directory-backed files",
markdown: include_str!("../docs/src/how-to/how-to-move-large-values-out-of-toml.md"),
},
DocPage {
id: "how-to-test-a-config-change-before-merge",
title: "How to Test a Config Change Before Merge",
list_title: "Test before merge: lint and resolve representative cases for a config change",
markdown: include_str!("../docs/src/how-to/how-to-test-a-config-change-before-merge.md"),
},
DocPage {
id: "how-to-enforce-a-config-policy",
title: "How to Enforce a Config Policy",
list_title: "Enforce config policy: add workspace-specific Lua lint for variables and values",
markdown: include_str!("../docs/src/how-to/how-to-enforce-a-config-policy.md"),
},
DocPage {
id: "how-to-load-config-from-a-git-repo-in-an-app",
title: "How to Load Config from a Git Repo in an App",
list_title: "Load from Git in an app: point the SDK at a workspace source URI",
markdown: include_str!(
"../docs/src/how-to/how-to-load-config-from-a-git-repo-in-an-app.md"
),
},
DocPage {
id: "how-to-keep-config-fresh-in-a-running-app",
title: "How to Keep Config Fresh in a Running App",
list_title: "Keep config fresh: refresh a running service while preserving last-known-good state",
markdown: include_str!("../docs/src/how-to/how-to-keep-config-fresh-in-a-running-app.md"),
},
DocPage {
id: "how-to-investigate-why-a-value-was-selected",
title: "How to Investigate Why a Value Was Selected",
list_title: "Investigate a selection: trace why a variable value was chosen at runtime",
markdown: include_str!("../docs/src/how-to/how-to-investigate-why-a-value-was-selected.md"),
},
DocPage {
id: "how-to-diagnose-a-failing-workspace",
title: "How to Diagnose a Failing Workspace",
list_title: "Diagnose lint failures: map stable diagnostics back to invalid workspace files",
markdown: include_str!("../docs/src/how-to/how-to-diagnose-a-failing-workspace.md"),
},
DocPage {
id: "example-environment-specific-limits",
title: "Example: Keep Deployment-Lane Limits Out of Application Code",
list_title: "Example: keep deployment-lane limits out of application code",
markdown: include_str!("../docs/src/examples/example-environment-specific-limits.md"),
},
DocPage {
id: "example-reviewed-account-class",
title: "Example: Select Behavior for a Reviewed Account Class",
list_title: "Example: select behavior for a reviewed account class",
markdown: include_str!("../docs/src/examples/example-reviewed-account-class.md"),
},
DocPage {
id: "example-llm-agent-configuration",
title: "Example: Control Structured LLM Agent Config Safely",
list_title: "Example: control structured LLM agent config with schema validation",
markdown: include_str!("../docs/src/examples/example-llm-agent-configuration.md"),
},
DocPage {
id: "example-tenant-specific-runtime-config",
title: "Example: Manage Tenant Exceptions Without App Branches",
list_title: "Example: manage tenant exceptions without application branches",
markdown: include_str!("../docs/src/examples/example-tenant-specific-runtime-config.md"),
},
DocPage {
id: "example-incident-banner",
title: "Example: Ship an Operational Override Without Redeploying",
list_title: "Example: ship an operational override without redeploying an app",
markdown: include_str!("../docs/src/examples/example-incident-banner.md"),
},
DocPage {
id: "example-bucketed-rollout",
title: "Example: Run a Stable Percentage Rollout from Config",
list_title: "Example: run a stable percentage rollout from workspace config",
markdown: include_str!("../docs/src/examples/example-bucketed-rollout.md"),
},
DocPage {
id: "workspace-manifest-reference",
title: "Workspace Manifest Reference",
list_title: "Reference: rototo-workspace.toml environments, schema version, and context schema",
markdown: include_str!("../docs/src/reference/workspace-manifest-reference.md"),
},
DocPage {
id: "qualifier-reference",
title: "Qualifier File Reference",
list_title: "Reference: qualifier TOML shape, predicates, references, and lint rules",
markdown: include_str!("../docs/src/reference/qualifier-reference.md"),
},
DocPage {
id: "variable-reference",
title: "Variable File Reference",
list_title: "Reference: variable TOML shape, values, env rules, schemas, and custom lint",
markdown: include_str!("../docs/src/reference/variable-reference.md"),
},
DocPage {
id: "predicate-reference",
title: "Predicate Reference",
list_title: "Reference: predicate fields, operators, value shapes, and bucket rules",
markdown: include_str!("../docs/src/reference/predicate-reference.md"),
},
DocPage {
id: "context-reference",
title: "Context Reference",
list_title: "Reference: runtime context contract and schema-backed request facts",
markdown: include_str!("../docs/src/reference/context-reference.md"),
},
DocPage {
id: "environment-reference",
title: "Environment Reference",
list_title: "Reference: environment declarations, defaults, and resolution fallback behavior",
markdown: include_str!("../docs/src/reference/environment-reference.md"),
},
DocPage {
id: "value-types-reference",
title: "Value Types Reference",
list_title: "Reference: primitive value types and schema-backed JSON values",
markdown: include_str!("../docs/src/reference/value-types-reference.md"),
},
DocPage {
id: "source-uri-reference",
title: "Source URI Reference",
list_title: "Reference: local, file, Git, SSH, HTTPS archive, ref, subdir, and auth forms",
markdown: include_str!("../docs/src/reference/source-uri-reference.md"),
},
DocPage {
id: "cli-reference",
title: "rototo CLI reference",
list_title: "CLI reference: commands, flags, context input forms, JSON output, and color behavior",
markdown: include_str!("../docs/src/api/cli-reference.md"),
},
DocPage {
id: "rust-sdk-reference",
title: "rototo Rust SDK",
list_title: "Rust SDK: async workspace loading, linting, reading, and resolution APIs",
markdown: include_str!("../docs/src/api/rust-sdk-reference.md"),
},
DocPage {
id: "diagnostic-reference",
title: "Diagnostic reference",
list_title: "Diagnostic reference: stable codes, rules, sources, severity, and help text",
markdown: include_str!("../docs/src/api/diagnostic-reference.md"),
},
DocPage {
id: "json-output-reference",
title: "JSON Output Reference",
list_title: "JSON output reference: machine-readable shapes emitted by CLI commands",
markdown: include_str!("../docs/src/reference/json-output-reference.md"),
},
];
pub const DOC_NAV_SECTIONS: &[DocNavSection] = &[
DocNavSection {
title: "Start",
pages: &["index"],
},
DocNavSection {
title: "Concepts",
pages: &["why-rototo", "rototo-model"],
},
DocNavSection {
title: "Tutorials",
pages: &["quickstart", "production-workflow"],
},
DocNavSection {
title: "How-to: Authoring",
pages: &[
"how-to-add-a-new-runtime-config-value",
"how-to-change-a-config-value-for-one-environment",
"how-to-add-a-new-context-field",
"how-to-select-a-value-for-a-runtime-condition",
"how-to-move-large-values-out-of-toml",
],
},
DocNavSection {
title: "How-to: Validation",
pages: &[
"how-to-test-a-config-change-before-merge",
"how-to-enforce-a-config-policy",
],
},
DocNavSection {
title: "How-to: Application",
pages: &[
"how-to-load-config-from-a-git-repo-in-an-app",
"how-to-keep-config-fresh-in-a-running-app",
],
},
DocNavSection {
title: "How-to: Operations",
pages: &[
"how-to-investigate-why-a-value-was-selected",
"how-to-diagnose-a-failing-workspace",
],
},
DocNavSection {
title: "Examples",
pages: &[
"example-environment-specific-limits",
"example-reviewed-account-class",
"example-llm-agent-configuration",
"example-tenant-specific-runtime-config",
"example-incident-banner",
"example-bucketed-rollout",
],
},
DocNavSection {
title: "Reference",
pages: &[
"workspace-manifest-reference",
"qualifier-reference",
"variable-reference",
"predicate-reference",
"context-reference",
"environment-reference",
"value-types-reference",
"source-uri-reference",
],
},
DocNavSection {
title: "API",
pages: &[
"cli-reference",
"rust-sdk-reference",
"diagnostic-reference",
"json-output-reference",
],
},
];
pub fn get_page(id: &str) -> Result<&'static DocPage> {
match lookup_page(id) {
DocPageLookup::Found(page) => Ok(page),
DocPageLookup::Ambiguous { query, pages } => Err(RototoError::new(format!(
"documentation page `{query}` matched multiple pages: {}",
page_ids(&pages)
))),
DocPageLookup::NotFound { query, pages } => Err(RototoError::new(format!(
"unknown documentation page `{query}`; available pages: {}",
page_ids(&pages)
))),
}
}
pub fn lookup_page(query: &str) -> DocPageLookup {
let query = normalize_page_query(query);
let slug_query = page_id_alias(&slug_query(&query));
let text_query = text_query(&query);
if let Some(page) = DOCS.iter().find(|page| page.id == slug_query) {
return DocPageLookup::Found(page);
}
if let Some(page) = DOCS
.iter()
.find(|page| text_matches_exact(page.title, &text_query))
{
return DocPageLookup::Found(page);
}
let matches = best_page_matches(&slug_query, &text_query);
match matches.len() {
0 => DocPageLookup::NotFound {
query,
pages: DOCS.iter().collect(),
},
1 => DocPageLookup::Found(matches[0]),
_ => DocPageLookup::Ambiguous {
query,
pages: matches,
},
}
}
fn normalize_page_query(query: &str) -> String {
let query = query.trim();
let query = query.strip_suffix(".html").unwrap_or(query);
let query = query.strip_suffix("...").unwrap_or(query);
match query {
"" | "/" => "index".to_owned(),
_ => query.to_ascii_lowercase(),
}
}
fn slug_query(query: &str) -> String {
query
.trim_matches('/')
.replace([' ', '_'], "-")
.trim_matches('-')
.to_owned()
}
fn page_id_alias(id: &str) -> String {
match id {
"model" => "rototo-model",
"cli" => "cli-reference",
"sdk" => "rust-sdk-reference",
"diagnostics" => "diagnostic-reference",
_ => id,
}
.to_owned()
}
fn text_query(query: &str) -> String {
query.replace(['-', '_'], " ").trim().to_owned()
}
fn best_page_matches(slug_query: &str, text_query: &str) -> Vec<&'static DocPage> {
let mut best_score = usize::MAX;
let mut matches = Vec::new();
for page in DOCS {
let Some(score) = page_match_score(page, slug_query, text_query) else {
continue;
};
if score < best_score {
best_score = score;
matches.clear();
}
if score == best_score {
matches.push(page);
}
}
matches
}
fn page_match_score(page: &DocPage, slug_query: &str, text_query: &str) -> Option<usize> {
let title = page.title.to_ascii_lowercase();
let list_title = page.list_title.to_ascii_lowercase();
if page.id.starts_with(slug_query) {
Some(0)
} else if page.id.contains(slug_query) {
Some(1)
} else if title.starts_with(text_query) || list_title.starts_with(text_query) {
Some(2)
} else if title.contains(text_query) || list_title.contains(text_query) {
Some(3)
} else {
None
}
}
fn text_matches_exact(text: &str, query: &str) -> bool {
text.to_ascii_lowercase() == query
}
fn page_ids(pages: &[&DocPage]) -> String {
pages
.iter()
.map(|page| page.id)
.collect::<Vec<_>>()
.join(", ")
}