Plain-Language Automated Function Documentation
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Save plan document and link in nav.adoc |
Done (pre-ADR-030) |
2 |
Add |
Done (pre-ADR-030) |
3 |
Implement OpenAPI fetcher and AsciiDoc generator |
Done (pre-ADR-030) |
4 |
Generate initial documentation output |
Done (pre-ADR-030) |
5 |
Add generated pages to Antora nav |
Done (pre-ADR-030) |
6 |
Verification and documentation updates |
Not started |
Issue: #97
Branch: feature/plain-language-docs
Context
45 CFR 1355.53(a)(2) requires CCWIS to provide plain-language documentation of all automated functions. This ensures non-technical stakeholders (caseworkers, supervisors, administrators) can understand what the system does without reading code.
All 8 backend services already have comprehensive OpenAPI annotations via [utoipa::path(…)] attributes on every endpoint. Each endpoint has a summary (short description) and response documentation. The [derive(utoipa::ToSchema)] on request/response structs provides parameter documentation. All services expose their OpenAPI spec at /api-doc/openapi.json via utoipa-swagger-ui.
Rather than maintaining separate documentation that drifts from code, this plan generates plain-language AsciiDoc pages directly from the existing OpenAPI specs. The generator fetches live specs from running services, parses them, and outputs human-readable pages organized by service and tag.
Scope
In scope:
-
cargo xtask api-docscommand that generates AsciiDoc from OpenAPI specs -
One generated page per service (8 pages total)
-
Endpoint name, plain description, parameters, request/response examples
-
Output to
docs/modules/ROOT/pages/api/directory -
Antora nav entries under User Guide section
Out of scope:
-
Live API explorer in the web UI (separate feature)
-
Training materials or workflow guides
-
Automatic CI regeneration (manual
cargo xtask api-docsfor now)
Design
OpenAPI Spec Access
Every CRAIG service exposes its OpenAPI 3.0 spec at /api-doc/openapi.json. The craig-api shared crate wires this up via utoipa_swagger_ui::SwaggerUi in ApiServer::router():
// crates/craig-api/src/lib.rs
router = router.merge(SwaggerUi::new("/swagger-ui").url("/api-doc/openapi.json", doc));
Services and their ports:
| Service | URL |
|---|---|
craig-rules |
|
craig-cases |
|
craig-placement |
|
craig-exchange |
|
craig-financial |
|
craig-reporting |
|
craig-security |
|
craig-intake |
Existing Annotation Pattern
Endpoints use #[utoipa::path(…)] with summary, tag, params, request_body, and responses:
// services/craig-cases/src/api/cases.rs
#[utoipa::path(
post,
path = "/v1/cases/cases",
tag = "Cases",
summary = "Open a case",
request_body = CreateCaseRequest,
responses(
(status = 200, description = "Case created", body = store::models::Case),
(status = 401, description = "Unauthorized", body = craig_common::error::ProblemDetails),
),
)]
Request structs use #[derive(utoipa::ToSchema)] with /// doc comments on fields:
#[derive(Deserialize, utoipa::ToSchema)]
pub struct CreateCaseRequest {
/// Investigation that substantiated the case (optional for direct filing).
pub investigation_id: Option<Uuid>,
pub admin_unit: String,
pub assigned_worker: String,
}
Output Format
Each generated page follows this structure:
= Craig Cases API Reference
:description: Plain-language documentation for the Cases service API
== Cases
=== Open a Case
**POST** `/v1/cases/cases`
Opens a new case in the system, typically after an investigation has been substantiated.
.Request Fields
[cols="1,1,2"]
|===
| Field | Type | Description
| investigation_id | UUID (optional) | Investigation that substantiated the case
| admin_unit | string | Administrative unit (county) handling the case
| assigned_worker | string | Worker assigned to the case
|===
.Responses
* **200** — Case created successfully
* **401** — Authentication required
---
Generator Architecture
The generator is a pure Rust module in xtask that:
-
Fetches OpenAPI JSON from each service via
reqwest -
Deserializes into
utoipa::openapi::OpenApi(utoipa re-exports the spec types) -
Iterates paths grouped by tag
-
Renders AsciiDoc using string formatting (no template engine needed for this)
Steps
Step 1: Save Plan and Link in nav.adoc
Files: docs/modules/ROOT/pages/plans/plain-language-docs.adoc, docs/modules/ROOT/nav.adoc
This plan file replaces the existing stub. Already linked in nav.adoc under Planned.
Step 2: Add api-docs Command to xtask
Files: xtask/src/cmd/mod.rs, xtask/src/cmd/api_docs.rs, xtask/src/main.rs, xtask/Cargo.toml
Add a new xtask subcommand:
// xtask/src/cmd/mod.rs — add to Command enum:
/// Generate plain-language API documentation from OpenAPI specs
ApiDocs(api_docs::ApiDocsArgs),
// xtask/src/main.rs — add match arm:
Command::ApiDocs(args) => cmd::api_docs::run(args),
Define ApiDocsArgs:
// xtask/src/cmd/api_docs.rs
use clap::Args;
#[derive(Args)]
pub struct ApiDocsArgs {
/// Base URL for services (default: http://localhost)
#[arg(long, default_value = "http://localhost")]
pub base_url: String,
/// Output directory (default: docs/modules/ROOT/pages/api)
#[arg(long, default_value = "docs/modules/ROOT/pages/api")]
pub output_dir: String,
}
Add dependencies to xtask/Cargo.toml:
-
reqwest = { version = "…", features = ["rustls-tls", "json"], default-features = false } -
serde_json = "…"(likely already present) -
tokio = { version = "…", features = ["rt-multi-thread", "macros"] }(for async runtime)
Step 3: Implement OpenAPI Fetcher and AsciiDoc Generator
Files: xtask/src/cmd/api_docs.rs
use anyhow::{Context, Result};
use serde_json::Value;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
const SERVICES: &[(&str, u16, &str)] = &[
("craig-rules", 8001, "Rules Engine"),
("craig-cases", 8002, "Case Management"),
("craig-placement", 8003, "Placement"),
("craig-exchange", 8004, "Data Exchange & ICPC"),
("craig-financial", 8005, "Financial & Claims"),
("craig-reporting", 8006, "Reporting & Data Quality"),
("craig-security", 8007, "Security & Compliance"),
("craig-intake", 8008, "Public Intake"),
];
pub fn run(args: ApiDocsArgs) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { generate_docs(&args).await })
}
async fn generate_docs(args: &ApiDocsArgs) -> Result<()> {
let client = reqwest::Client::new();
let output_dir = Path::new(&args.output_dir);
fs::create_dir_all(output_dir)?;
for (name, port, display_name) in SERVICES {
let url = format!("{}:{}/api-doc/openapi.json", args.base_url, port);
println!("Fetching {name} from {url}...");
let spec: Value = client
.get(&url)
.send()
.await
.with_context(|| format!("failed to fetch {name} — is devstack running?"))?
.json()
.await
.with_context(|| format!("failed to parse {name} OpenAPI JSON"))?;
let adoc = render_service_page(&spec, name, display_name);
let filename = output_dir.join(format!("{name}.adoc"));
fs::write(&filename, &adoc)?;
println!(" Wrote {}", filename.display());
}
// Generate index page
let index = render_index_page();
fs::write(output_dir.join("index.adoc"), &index)?;
println!("Done. Generated {} service pages + index.", SERVICES.len());
Ok(())
}
The render_service_page function should:
-
Extract
info.titleandinfo.descriptionfrom the spec -
Group paths by tag (from
paths.{path}.{method}.tags[0]) -
For each path+method: extract
summary,description,parameters,requestBodyschema,responses -
Render parameter tables from
requestBody.content.application/json.schema— resolve$refpointers tocomponents.schemas -
Use plain language: "POST" becomes "Create", "GET" becomes "Retrieve", "PUT" becomes "Update", "DELETE" becomes "Remove"
Key rendering rules:
* Group endpoints by tag with == {Tag Name} heading
* Each endpoint gets === {Summary} heading
* Show HTTP method and path in bold
* Render request fields as a table: field name, type, required/optional, description
* Render response codes as a bullet list
* Resolve $ref references to inline the schema field descriptions
* Skip internal fields (id, created_at, updated_at, active) from request tables — they are server-managed
* Convert snake_case field names to readable labels in descriptions where doc comments are absent
Step 4: Generate Initial Documentation
Run cargo xtask api-docs with devstack running. This produces:
docs/modules/ROOT/pages/api/ index.adoc # Links to all service pages craig-rules.adoc # Rules Engine endpoints craig-cases.adoc # Case Management endpoints craig-placement.adoc # Placement endpoints craig-exchange.adoc # Data Exchange endpoints craig-financial.adoc # Financial endpoints craig-reporting.adoc # Reporting endpoints craig-security.adoc # Security endpoints craig-intake.adoc # Public Intake endpoints
Review the generated output for: * Completeness — every endpoint present * Readability — descriptions make sense to non-technical users * Accuracy — parameter tables match actual API contracts
If any endpoint has insufficient summary/description in its #[utoipa::path(…)], update the annotation in the source code and regenerate.
Step 5: Add to Antora Navigation
Files: docs/modules/ROOT/nav.adoc
Add under User Guide section:
* User Guide
** xref:guide/caseworker.adoc[Caseworker Guide]
** xref:guide/supervisor.adoc[Supervisor Guide]
** xref:guide/admin.adoc[Administrator Guide]
** xref:guide/public-reporting.adoc[Public Reporting Guide]
** API Reference
*** xref:api/index.adoc[Overview]
*** xref:api/craig-rules.adoc[Rules Engine]
*** xref:api/craig-cases.adoc[Case Management]
*** xref:api/craig-placement.adoc[Placement]
*** xref:api/craig-exchange.adoc[Data Exchange]
*** xref:api/craig-financial.adoc[Financial]
*** xref:api/craig-reporting.adoc[Reporting]
*** xref:api/craig-security.adoc[Security]
*** xref:api/craig-intake.adoc[Public Intake]
Step 6: Verification and Documentation Updates
Verify:
-
cargo xtask api-docscompletes without errors against running devstack -
All 8 service pages are generated with correct endpoint counts
-
cargo xtask check-docspasses (no broken xrefs in nav.adoc) -
Each page is human-readable — a non-developer can understand what each endpoint does
-
Parameter tables are accurate (compare against actual request structs)
Files Touched
| File | Change |
|---|---|
|
New file: OpenAPI fetcher + AsciiDoc generator |
|
Add |
|
Add |
|
Add |
|
Generated output (9 files: index + 8 services) |
|
Add API Reference sub-section under User Guide |
Verification
-
cargo nextest run --workspace --lib— unit tests pass -
cargo xtask dev reload— devstack running -
cargo xtask api-docs— generates all 9 files without error -
cargo xtask check-docs— no broken nav xrefs -
Manual review of at least 3 generated pages for readability
Documentation Updates
-
CHANGELOG.adoc— entry under== Unreleased -
.claude/docs/coding-conventions.md— addapi-docsto xtask subcommand list -
Generated API docs are the deliverable itself — no separate doc update needed