From b87e7863e5ba278c4e83dffd581b5191960140a1 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Sun, 1 Nov 2026 08:07:12 +0800 Subject: [PATCH] doc: `comline doc`, the package's documentation Markdown pages from the official renderer (linked in like the official generators), or the documentation model as JSON. `--version` documents a built version from its frozen IR and `//!` headers, `--changelog-from` adds a changelog, and comline.toml's `[doc]` table sets the defaults: `out`, `format`, `renderer` (an external renderer, run like an external generator through the same sandbox, limits and path checks), `timeout`, `links` and `changelog_from`. Pins core 076cae0, generation 7b51264, comline-rust bef9a74 and comline-typescript 47b54cc. Generated code is unchanged. --- Cargo.lock | 24 ++- Cargo.toml | 11 +- docs/cli.md | 55 ++++++ src/cli.rs | 41 +++++ src/commands/doc.rs | 173 ++++++++++++++++++ src/commands/mod.rs | 1 + src/external.rs | 66 ++++++- src/main.rs | 14 ++ tests/cli/doc.rs | 146 +++++++++++++++ tests/cli/external.rs | 60 ++++++ tests/cli/main.rs | 1 + tests/fixtures/generators/Cargo.lock | 21 ++- tests/fixtures/generators/Cargo.toml | 5 +- tests/fixtures/generators/src/bin/hostile.rs | 3 +- tests/fixtures/generators/src/bin/renderer.rs | 10 + 15 files changed, 612 insertions(+), 19 deletions(-) create mode 100644 src/commands/doc.rs create mode 100644 tests/cli/doc.rs create mode 100644 tests/fixtures/generators/src/bin/renderer.rs diff --git a/Cargo.lock b/Cargo.lock index ebb6c6a..221b5b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,6 +540,7 @@ dependencies = [ "comline-codegen-rust", "comline-codegen-typescript", "comline-core", + "comline-doc-markdown", "comline-runtime-conformance", "eyre", "indicatif", @@ -562,7 +563,7 @@ dependencies = [ [[package]] name = "comline-codegen" version = "0.1.0" -source = "git+https://github.com/ComlineProject/generation?rev=0601f004302ebb93e6804732419ccf459902e84a#0601f004302ebb93e6804732419ccf459902e84a" +source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06" dependencies = [ "comline-core", "eyre", @@ -576,7 +577,7 @@ dependencies = [ [[package]] name = "comline-codegen-rust" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-rust?rev=6e8715ac126ab78316dd6a2712165d716351fab1#6e8715ac126ab78316dd6a2712165d716351fab1" +source = "git+https://github.com/ComlineProject/comline-rust?rev=bef9a74c028d7a44b4dc13ed677d300e061b2147#bef9a74c028d7a44b4dc13ed677d300e061b2147" dependencies = [ "comline-codegen", "comline-core", @@ -586,7 +587,7 @@ dependencies = [ [[package]] name = "comline-codegen-typescript" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-typescript?rev=470dc5bad2f7e36e1cc2f44e3e57fde864a42315#470dc5bad2f7e36e1cc2f44e3e57fde864a42315" +source = "git+https://github.com/ComlineProject/comline-typescript?rev=47b54cc75b05bc52586d431d26ed4def316cf0c4#47b54cc75b05bc52586d431d26ed4def316cf0c4" dependencies = [ "comline-codegen", "comline-core", @@ -596,7 +597,7 @@ dependencies = [ [[package]] name = "comline-core" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64" +source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f" dependencies = [ "ariadne", "blake3", @@ -612,6 +613,7 @@ dependencies = [ "regex", "rust-sitter", "rust-sitter-tool", + "schemars", "semver", "serde", "serde_derive", @@ -624,7 +626,19 @@ dependencies = [ [[package]] name = "comline-core-stdlib" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64" +source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f" + +[[package]] +name = "comline-doc-markdown" +version = "0.1.0" +source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06" +dependencies = [ + "comline-codegen", + "comline-core", + "eyre", + "serde", + "serde_json", +] [[package]] name = "comline-runtime" diff --git a/Cargo.toml b/Cargo.toml index c6214f0..7c0494c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,15 +32,18 @@ eyre = "0.6.8" # needs real I/O (git fetch, filesystem), so it's off by default for # `comline-core`'s wasm32 consumers (the playground editor, the language # server); the CLI is the one consumer that actually runs `comline build`. -comline-core = { git = "https://github.com/ComlineProject/core", rev = "47066583002dbca27975827106f9e34761d0dc64", features = ["deps"] } +comline-core = { git = "https://github.com/ComlineProject/core", rev = "076cae0c3a0639b0c083b61a94f9d4913e853b3f", features = ["deps"] } # All three pinned to revs that agree on one `comline-codegen`, and bumped # together: comline-rust and comline-typescript's codegen crates consume # `comline-codegen`'s types, so the rev below must be byte-identical to the # one *they* pin - Cargo only unifies a git dependency across identical rev # strings, and two copies of `GenRequest` are two incompatible types. -comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "0601f004302ebb93e6804732419ccf459902e84a" } -comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "6e8715ac126ab78316dd6a2712165d716351fab1", optional = true } -comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "470dc5bad2f7e36e1cc2f44e3e57fde864a42315", optional = true } +comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" } +# The official Markdown documentation renderer (`comline doc`), linked in +# like the official generators. Same generation rev as `comline-codegen`. +comline-doc-markdown = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" } +comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "bef9a74c028d7a44b4dc13ed677d300e061b2147", optional = true } +comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "47b54cc75b05bc52586d431d26ed4def316cf0c4", optional = true } # `comline conformance`: the conformance runner (corpus loading, judging, # profile proofs) and its case format, a public spec. Pinned by git rev. comline-runtime-conformance = { git = "https://github.com/ComlineProject/runtime", rev = "6f8f73a247f0aa4ac8b7041b2fdc4b02bce0e0d6" } diff --git a/docs/cli.md b/docs/cli.md index ace90a1..9d3cbaa 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -20,6 +20,7 @@ This is the long-form reference. For a two-minute tour see the - [`build`](#comline-build) - [`generate`](#comline-generate) - [`diff`](#comline-diff) + - [`doc`](#comline-doc) - [`conformance`](#comline-conformance) - [`clean`](#comline-clean) - [`reset`](#comline-reset) @@ -445,6 +446,60 @@ comline diff 0.0.1 HEAD Fails with exit `2` if the project has never been built, and exit `1` if an argument matches no stored version (the error lists the versions that exist). +### `comline doc` + +``` +comline doc [--out ] [--version ] [--changelog-from ] [--format markdown|json] +``` + +Write the package's documentation. Nothing is written to `.comline/`. + +- **Pages** (`markdown`, the default, rendered by the official renderer linked + into `comline`): `index.md` (the package, its `//!` header, its modules), one + page per module (`types::user` is `types/user.md`) with its `//!` header and + every unit with its `///` docs, fields, arguments and each protocol's wire + summary, `changelog.md` with `--changelog-from`, and `nav.yml`, a `nav` for + mkdocs or zensical. +- **`json`** writes the documentation model itself, `model.json`, in the shape + of core's `schemas/doc-model.json`. + +| Flag | Default | Meaning | +|---|---|---| +| `--out ` | `[doc] out`, else `docs/api` | where to write, relative to the project | +| `--version ` | the working tree | document a built version (a version string, a commit hash, or `HEAD`) from its frozen IR, `//!` headers included | +| `--changelog-from ` | `[doc] changelog_from` | add what changed from built version `` to the documented one | +| `--format ` | `[doc] format`, else `markdown` | `markdown`, `json`, or another id the `[doc] renderer` offers | + +A built version keeps no source, so with `--version` its protocols have no wire +summary. A version built before headers were stored has none. + +`comline.toml`'s `[doc]` table: + +```toml +[doc] +out = "docs/api" +format = "markdown" +renderer = "renderers/my-renderer.wasm" # optional: an external renderer +timeout = 60 # seconds per call to `renderer` +links = { std = "https://comlineproject.github.io/docs/std/" } +changelog_from = "0.1.0" +``` + +- `renderer` is run exactly like an external `generator`: a `.wasm` under + wasmtime with no filesystem, environment or network, a native executable with + an empty environment, the same limits and path checks. It must list the + format in its `Describe` answer's `renderers`. +- `links` maps a dependency (or `std`) to the base URL of its documentation. A + type from it links to `//#`; a dependency not listed is + plain text. + +```bash +comline doc +comline doc --out site/api --changelog-from 0.1.0 +comline doc --version 0.2.0 +comline doc --format json +``` + ### `comline conformance` ``` diff --git a/src/cli.rs b/src/cli.rs index be085a9..671ad0f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -100,6 +100,47 @@ Examples: json: bool, }, + /// Write the package's documentation: Markdown pages, or the model as JSON + #[command(long_about = "\ +Write the package's documentation: an index page, one page per module (its \ +`//!` header, every unit with its `///` docs, fields, arguments and each \ +protocol's wire summary), a changelog page with `--changelog-from`, and an \ +mkdocs/zensical `nav.yml`. Read-only: nothing is written to `.comline/`. + +The working tree is documented unless `--version` names a built version (a \ +version string, a commit hash or `HEAD`), which is documented from its frozen \ +IR, `//!` headers included. A built version keeps no source, so its protocols \ +have no wire summary. + +`--format json` writes the documentation model itself (`model.json`), for \ +other tools. `comline.toml`'s `[doc]` table sets the defaults: `out` \ +(`docs/api`), `format` (`markdown`), `renderer` (an external renderer, run \ +like an external generator), `links` (dependency docs URLs) and \ +`changelog_from`. + +Examples: + comline doc + comline doc --out site/api --changelog-from 0.1.0 + comline doc --version 0.2.0 + comline doc --format json")] + Doc { + /// Output directory; default from `[doc] out`, else `docs/api` + #[arg(long, value_name = "DIR")] + out: Option, + + /// Document this built version instead of the working tree + #[arg(long = "version", value_name = "VERSION")] + doc_version: Option, + + /// Add a changelog from this built version to the documented one + #[arg(long, value_name = "VERSION")] + changelog_from: Option, + + /// `markdown` (pages) or `json` (the model); default from `[doc] format` + #[arg(long, value_name = "FORMAT")] + format: Option, + }, + /// Validate the project and write generated code for each configured target #[command(long_about = "\ Validate the project (no version bump, no `.comline/` write), then write \ diff --git a/src/commands/doc.rs b/src/commands/doc.rs new file mode 100644 index 0000000..ae40a6f --- /dev/null +++ b/src/commands/doc.rs @@ -0,0 +1,173 @@ +//! `comline doc` — the package's documentation (core#145). +//! +//! Core builds the documentation model (`comline_core::doc`): from the +//! compiled working tree, or from a built version's frozen IR and `//!` +//! headers with `--version`. `--changelog-from` adds what changed since an +//! earlier built version. `--format json` writes the model itself; any other +//! format is a renderer's id, `markdown` being the official one, linked in +//! like the official generators. `[doc] renderer` in `comline.toml` names an +//! external renderer instead, run exactly like an external generator +//! ([`crate::external`]: the same sandbox, limits and path checks). +//! +//! Like `check`, nothing is written to `.comline/`. + +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use comline_codegen::config::{ComlineToml, DEFAULT_DOC_FORMAT, DEFAULT_DOC_OUT}; +use comline_codegen::protocol::{FileOut, RenderOptions, RenderRequest}; +use comline_codegen::Registry; +use comline_core::doc::{self, DocModel, DOC_MODEL_VERSION}; +use comline_core::package::build::cas::objects::Tree; +use comline_core::package::build::cas::ObjectStore; +use comline_core::schema::ir::frozen::unit::FrozenUnit; +use miette::{miette, IntoDiagnostic, Result, WrapErr}; + +use crate::commands::{core_err, ensure_project}; +use crate::{external, history, ui}; + +/// The command line's say, each overriding its `[doc]` key. +#[derive(Debug, Default)] +pub struct Flags { + pub out: Option, + pub version: Option, + pub changelog_from: Option, + pub format: Option, +} + +/// The renderers linked into this binary. +pub(crate) fn renderer_registry() -> Registry { + let mut registry = Registry::new(); + comline_doc_markdown::register(&mut registry); + registry +} + +pub fn run(work_dir: &Path, flags: Flags) -> Result<()> { + ensure_project(work_dir)?; + let cfg = ComlineToml::load(work_dir)?.doc; + + let out = match (&flags.out, &cfg.out) { + (Some(out), _) => work_dir.join(out), + (None, Some(out)) => work_dir.join(out), + (None, None) => work_dir.join(DEFAULT_DOC_OUT), + }; + let format = flags + .format + .or(cfg.format.clone()) + .unwrap_or_else(|| DEFAULT_DOC_FORMAT.to_string()); + let changelog_from = flags.changelog_from.or(cfg.changelog_from.clone()); + + let (mut model, schemas) = match &flags.version { + Some(spec) => frozen_model(work_dir, spec)?, + None => working_tree_model(work_dir)?, + }; + + if let Some(from) = &changelog_from { + let history = history::load(work_dir)?; + let commit = history::resolve(&history, from)?; + let store = ObjectStore::new(work_dir); + let old = history::load_schemas(&store, commit)?; + let to = model + .package + .version + .clone() + .unwrap_or_else(|| "working tree".to_string()); + model + .changelog + .push(doc::changelog(&commit.version, &old, &to, &schemas)); + } + + let files = if format == "json" { + let json = serde_json::to_string_pretty(&model).into_diagnostic()?; + vec![FileOut { + path: "model.json".into(), + contents: format!("{json}\n").into_bytes(), + }] + } else { + let request = RenderRequest { + renderer: format.clone(), + model_version: DOC_MODEL_VERSION, + model: serde_json::to_string(&model).into_diagnostic()?, + options: RenderOptions { + links: cfg.links.clone(), + }, + }; + match &cfg.renderer { + Some(path) => { + let timeout = cfg + .timeout + .map(Duration::from_secs) + .unwrap_or(external::DEFAULT_TIMEOUT); + let renderer = external::External::open(&work_dir.join(path), timeout)?; + let description = renderer.describe()?; + renderer.render(&description, request)? + } + None => external::builtin_render(&renderer_registry(), request)?, + } + }; + + for file in &files { + let path = out.join(file.path_buf()); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create `{}`", parent.display()))?; + } + std::fs::write(&path, &file.contents) + .into_diagnostic() + .wrap_err_with(|| format!("failed to write `{}`", path.display()))?; + } + ui::success(format!( + "Documented {} into {} ({} file(s))", + match &model.package.version { + Some(v) => format!("{} {v}", model.package.name), + None => model.package.name.clone(), + }, + out.display(), + files.len() + )); + Ok(()) +} + +/// The working tree's model, and its frozen schemas for a changelog. +fn working_tree_model(work_dir: &Path) -> Result<(DocModel, Vec>)> { + let spinner = ui::spinner("compiling schemas"); + let outcome = comline_core::package::build::compile_package(work_dir); + ui::finish_spinner(spinner); + let context = outcome.map_err(|e| core_err("validation failed", e))?; + Ok((doc::model(&context, None), doc::frozen_schemas(&context))) +} + +/// A built version's model, from its frozen IR and headers. +fn frozen_model(work_dir: &Path, spec: &str) -> Result<(DocModel, Vec>)> { + let history = history::load(work_dir)?; + let commit = history::resolve(&history, spec)?; + let store = ObjectStore::new(work_dir); + let schemas = history::load_schemas(&store, commit)?; + let config = history::load_config(&store, commit)?; + // A built version's config doesn't always freeze the package's name; + // the working tree's is the same unless the package was renamed. + let name = comline_core::package::config::ir::frozen::namespace(&config) + .map(str::to_string) + .or_else(|| { + comline_core::package::build::compile_package_without_fetching(work_dir) + .ok() + .map(|context| context.config.name.value.to_string()) + }) + .or_else(|| { + work_dir + .canonicalize() + .ok()? + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + }) + .ok_or_else(|| miette!("version `{spec}` has no package name"))?; + let root = store + .read(&commit.tree) + .and_then(|bytes| Tree::from_bytes(&bytes)) + .map_err(|e| miette!("failed to read version `{spec}`: {e:#}"))?; + let headers = doc::read_docs(&store, &root) + .map_err(|e| miette!("failed to read version `{spec}`'s headers: {e:#}"))?; + let model = doc::model_frozen(&name, Some(commit.version.clone()), &schemas, headers); + Ok((model, schemas)) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a2b43c4..9e93298 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,6 +9,7 @@ pub mod clean; pub mod completions; pub mod conformance; pub mod diff; +pub mod doc; pub mod generate; pub mod inspect; pub mod new; diff --git a/src/external.rs b/src/external.rs index 63e6232..be1a3cb 100644 --- a/src/external.rs +++ b/src/external.rs @@ -26,8 +26,8 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use comline_codegen::protocol::{ - self, Description, FileOut, GenerateRequest, LanguageInfo, Request, Response, IR_FORMAT, - PROTOCOL_VERSION, + self, Description, FileOut, GenerateRequest, LanguageInfo, RenderRequest, Request, Response, + IR_FORMAT, PROTOCOL_VERSION, }; use comline_codegen::versions::LangVersion; use comline_codegen::Registry; @@ -133,7 +133,7 @@ impl External { self.name )) } - Response::Generate(_) => { + Response::Generate(_) | Response::Render(_) => { return Err(miette!( "generator `{}` answered `Describe` with generated files", self.name @@ -169,6 +169,61 @@ impl External { let response = self.call(&Request::generate(request))?; files(&format!("generator `{}`", self.name), response) } + + /// Renders documentation with the renderer `request` names, which its + /// description must list. Checked like [`External::generate`]'s files. + pub fn render( + &self, + description: &Description, + request: RenderRequest, + ) -> Result> { + let who = format!("renderer `{}`", self.name); + check_renderer(&who, description, &request)?; + let response = self.call(&Request::render(request))?; + rendered(&who, response) + } +} + +/// Refuses `request` before it's sent when `description` doesn't offer its +/// renderer at its model version. +fn check_renderer(who: &str, description: &Description, request: &RenderRequest) -> Result<()> { + let Some(info) = description + .renderers + .iter() + .find(|r| r.id == request.renderer) + else { + return Err(miette!( + "{who} doesn't render `{}`; it offers {}", + request.renderer, + list(description.renderers.iter().map(|r| format!("`{}`", r.id))) + )); + }; + if !info.model_versions.contains(&request.model_version) { + return Err(miette!( + "{who} reads documentation model {}, and this comline writes model {}", + list(info.model_versions.iter()), + request.model_version + )); + } + Ok(()) +} + +/// A built-in renderer's answer to `request`, through the protocol in +/// process, like [`builtin`]. +pub fn builtin_render(registry: &Registry, request: RenderRequest) -> Result> { + let who = format!("`{}` renderer", request.renderer); + check_renderer(&who, &protocol::describe(registry), &request)?; + let bytes = protocol::encode(&Request::render(request)).map_err(|e| miette!("{e}"))?; + rendered(&who, protocol::respond(registry, &bytes)) +} + +/// [`files`] for a `Render` answer. +fn rendered(who: &str, response: Response) -> Result> { + match response { + Response::Render(generated) => files(who, Response::Generate(generated)), + Response::Generate(_) => Err(miette!("{who} answered `Render` with generated code")), + other => files(who, other), + } } /// `items` as `a, b, c`, or `none`. @@ -261,6 +316,11 @@ fn files(who: &str, response: Response) -> Result> { Response::Describe(_) => { return Err(miette!("{who} answered `Generate` with a description")) } + Response::Render(_) => { + return Err(miette!( + "{who} answered `Generate` with rendered documentation" + )) + } }; for diagnostic in &generated.diagnostics { crate::ui::warn(format!("{who}: {diagnostic}")); diff --git a/src/main.rs b/src/main.rs index e18c213..aa0e8a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,6 +119,20 @@ fn run(cli: Cli) -> Result<()> { Commands::Build { watch } => commands::build::run(&work_dir, watch), Commands::Check => commands::check::run(&work_dir), Commands::Inspect { json } => commands::inspect::run(&work_dir, json), + Commands::Doc { + out, + doc_version, + changelog_from, + format, + } => commands::doc::run( + &work_dir, + commands::doc::Flags { + out, + version: doc_version, + changelog_from, + format, + }, + ), Commands::Generate { target, out, diff --git a/tests/cli/doc.rs b/tests/cli/doc.rs new file mode 100644 index 0000000..4f74d8f --- /dev/null +++ b/tests/cli/doc.rs @@ -0,0 +1,146 @@ +//! `comline doc` + +use std::fs; +use std::path::Path; + +use predicates::prelude::*; + +use crate::util::*; + +fn package(dir: &Path, header: &str, schema: &str) { + fs::create_dir_all(dir.join("src")).unwrap(); + fs::write( + dir.join("config.idp"), + format!("{header}congregation shop\nspecification_version = 1\n"), + ) + .unwrap(); + fs::write(dir.join("src/api.ids"), schema).unwrap(); +} + +const API: &str = "//! The shop's API.\n\n/// One order.\nstruct Order {\n /// What it costs.\n total: u64\n}\n\n\ + /// Orders things.\nprotocol Orders {\n /// Place one.\n /// @order: what to place\n \ + function place(order: Order) -> bool;\n}\n"; + +fn read(path: &Path) -> String { + fs::read_to_string(path).unwrap_or_else(|e| panic!("{}: {e}", path.display())) +} + +#[test] +fn writes_markdown_pages_for_the_working_tree() { + let temp = tempfile::tempdir().unwrap(); + let shop = temp.path().join("shop"); + package(&shop, "//! A shop.\n\n", API); + + comline_cmd() + .current_dir(&shop) + .arg("doc") + .assert() + .success() + .stderr(predicate::str::contains("Documented shop")); + + let out = shop.join("docs/api"); + let index = read(&out.join("index.md")); + assert!(index.starts_with("# shop\n\nA shop.\n"), "{index}"); + assert!(index.contains("| [`api`](api.md) | The shop's API. |")); + let api = read(&out.join("api.md")); + assert!( + api.contains("| `total` | `u64` | What it costs. |"), + "{api}" + ); + assert!(api.contains("| `order` | [`Order`](#order) | what to place |")); + assert!(api.contains("**On the wire**")); + assert!(read(&out.join("nav.yml")).contains(" - \"api\": api.md")); + assert!(!out.join("changelog.md").exists()); +} + +#[test] +fn documents_a_built_version_with_its_frozen_headers_and_a_changelog() { + let temp = tempfile::tempdir().unwrap(); + let shop = temp.path().join("shop"); + package(&shop, "//! A shop.\n\n", API); + comline_cmd() + .current_dir(&shop) + .arg("build") + .assert() + .success(); + + // Change the working tree: the built version must not read it. + package( + &shop, + "", + &API.replace("//! The shop's API.\n\n", "").replace( + " total: u64\n", + " total: u64\n optional note: string\n", + ), + ); + comline_cmd() + .current_dir(&shop) + .arg("build") + .assert() + .success(); + + comline_cmd() + .current_dir(&shop) + .args(["doc", "--version", "0.1.0-dev.1", "--out", "v1"]) + .assert() + .success(); + let index = read(&shop.join("v1/index.md")); + assert!( + index.starts_with("# shop 0.1.0-dev.1\n\nA shop.\n"), + "{index}" + ); + let api = read(&shop.join("v1/api.md")); + assert!(api.starts_with("# `api`\n\nThe shop's API.\n"), "{api}"); + assert!(!api.contains("note")); + + comline_cmd() + .current_dir(&shop) + .args(["doc", "--changelog-from", "0.1.0-dev.1", "--out", "now"]) + .assert() + .success(); + let changelog = read(&shop.join("now/changelog.md")); + assert!( + changelog.contains("## 0.1.0-dev.1 to working tree"), + "{changelog}" + ); + assert!(changelog.contains("field name: `note`"), "{changelog}"); +} + +#[test] +fn json_writes_the_model_and_doc_settings_come_from_comline_toml() { + let temp = tempfile::tempdir().unwrap(); + let shop = temp.path().join("shop"); + package(&shop, "", API); + fs::write( + shop.join("comline.toml"), + "[doc]\nout = \"site\"\nformat = \"json\"\n", + ) + .unwrap(); + + comline_cmd() + .current_dir(&shop) + .arg("doc") + .assert() + .success(); + let model: serde_json::Value = + serde_json::from_str(&read(&shop.join("site/model.json"))).unwrap(); + assert_eq!(model["model_version"], 1); + assert_eq!(model["package"]["name"], "shop"); + assert_eq!(model["modules"][0]["docs"], "The shop's API."); +} + +#[test] +fn an_unknown_format_names_what_is_offered() { + let temp = tempfile::tempdir().unwrap(); + let shop = temp.path().join("shop"); + package(&shop, "", API); + + comline_cmd() + .current_dir(&shop) + .args(["doc", "--format", "html"]) + .assert() + .failure() + .stderr(predicate::str::contains( + "doesn't render `html`; it offers `markdown`", + )); +} diff --git a/tests/cli/external.rs b/tests/cli/external.rs index c7ca8c8..ad2739f 100644 --- a/tests/cli/external.rs +++ b/tests/cli/external.rs @@ -286,3 +286,63 @@ fn a_language_the_generator_does_not_offer_is_refused() { "generator `{file}` handles `rust` from `9.0.0` on, not `1.85.0`" ))); } + +/// `[doc] renderer`: the official Markdown renderer run as an external one +/// writes what the built-in writes. +fn renderer_matches_builtin(kind: Kind) { + let temp = tempfile::tempdir().unwrap(); + let project = fixture_project(temp.path()); + comline_cmd() + .current_dir(&project) + .args(["doc", "--out", "builtin"]) + .assert() + .success(); + let file = install(&project, kind, "renderer", "comline-doc-markdown"); + fs::write( + project.join("comline.toml"), + format!("[doc]\nrenderer = \"{file}\"\nout = \"external\"\n"), + ) + .unwrap(); + comline_cmd() + .current_dir(&project) + .arg("doc") + .assert() + .success(); + let external = tree(&project.join("external")); + assert!(!external.is_empty()); + assert!( + external == tree(&project.join("builtin")), + "{kind:?}: the external renderer's pages differ from the built-in's" + ); +} + +#[cfg(feature = "gen-wasm")] +#[test] +fn a_wasm_renderer_writes_what_the_builtin_does() { + renderer_matches_builtin(Kind::Wasm); +} + +#[test] +fn a_native_renderer_writes_what_the_builtin_does() { + renderer_matches_builtin(Kind::Native); +} + +#[test] +fn a_generator_that_offers_no_renderer_is_refused_as_one() { + let temp = tempfile::tempdir().unwrap(); + let project = fixture_project(temp.path()); + let file = install(&project, Kind::Native, "real", "comline-gen-rust"); + fs::write( + project.join("comline.toml"), + format!("[doc]\nrenderer = \"{file}\"\n"), + ) + .unwrap(); + comline_cmd() + .current_dir(&project) + .arg("doc") + .assert() + .failure() + .stderr(predicate::str::contains( + "doesn't render `markdown`; it offers none", + )); +} diff --git a/tests/cli/main.rs b/tests/cli/main.rs index c00249d..8f5fc10 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -13,6 +13,7 @@ mod check; mod clean; mod conformance; mod diff; +mod doc; mod end_to_end; mod external; mod generate; diff --git a/tests/fixtures/generators/Cargo.lock b/tests/fixtures/generators/Cargo.lock index 05a057e..64dfc52 100644 --- a/tests/fixtures/generators/Cargo.lock +++ b/tests/fixtures/generators/Cargo.lock @@ -73,7 +73,7 @@ dependencies = [ [[package]] name = "comline-codegen" version = "0.1.0" -source = "git+https://github.com/ComlineProject/generation?rev=0601f004302ebb93e6804732419ccf459902e84a#0601f004302ebb93e6804732419ccf459902e84a" +source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06" dependencies = [ "comline-core", "eyre", @@ -87,7 +87,7 @@ dependencies = [ [[package]] name = "comline-codegen-rust" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-rust?rev=6e8715ac126ab78316dd6a2712165d716351fab1#6e8715ac126ab78316dd6a2712165d716351fab1" +source = "git+https://github.com/ComlineProject/comline-rust?rev=bef9a74c028d7a44b4dc13ed677d300e061b2147#bef9a74c028d7a44b4dc13ed677d300e061b2147" dependencies = [ "comline-codegen", "comline-core", @@ -97,7 +97,7 @@ dependencies = [ [[package]] name = "comline-core" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64" +source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f" dependencies = [ "blake3", "ciborium", @@ -114,7 +114,19 @@ dependencies = [ [[package]] name = "comline-core-stdlib" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=47066583002dbca27975827106f9e34761d0dc64#47066583002dbca27975827106f9e34761d0dc64" +source = "git+https://github.com/ComlineProject/core?rev=076cae0c3a0639b0c083b61a94f9d4913e853b3f#076cae0c3a0639b0c083b61a94f9d4913e853b3f" + +[[package]] +name = "comline-doc-markdown" +version = "0.1.0" +source = "git+https://github.com/ComlineProject/generation?rev=7b51264b216ef4c5e2afbe36897a5b35dafe7e06#7b51264b216ef4c5e2afbe36897a5b35dafe7e06" +dependencies = [ + "comline-codegen", + "comline-core", + "eyre", + "serde", + "serde_json", +] [[package]] name = "comline-test-generators" @@ -122,6 +134,7 @@ version = "0.0.0" dependencies = [ "comline-codegen", "comline-codegen-rust", + "comline-doc-markdown", ] [[package]] diff --git a/tests/fixtures/generators/Cargo.toml b/tests/fixtures/generators/Cargo.toml index bda1f1b..f92c3de 100644 --- a/tests/fixtures/generators/Cargo.toml +++ b/tests/fixtures/generators/Cargo.toml @@ -12,5 +12,6 @@ publish = false [dependencies] # The same revs the CLI pins, so the real generator's output is comparable # byte for byte with the CLI's built-in one. -comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "0601f004302ebb93e6804732419ccf459902e84a" } -comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "6e8715ac126ab78316dd6a2712165d716351fab1" } +comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" } +comline-doc-markdown = { git = "https://github.com/ComlineProject/generation", rev = "7b51264b216ef4c5e2afbe36897a5b35dafe7e06" } +comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "bef9a74c028d7a44b4dc13ed677d300e061b2147" } diff --git a/tests/fixtures/generators/src/bin/hostile.rs b/tests/fixtures/generators/src/bin/hostile.rs index 8c39c80..8ed611b 100644 --- a/tests/fixtures/generators/src/bin/hostile.rs +++ b/tests/fixtures/generators/src/bin/hostile.rs @@ -20,7 +20,7 @@ fn main() { let request: Request = decode(&input).unwrap(); let response = match request.body { RequestBody::Describe => describe(&mode), - RequestBody::Generate(_) => generate(&mode), + RequestBody::Generate(_) | RequestBody::Render(_) => generate(&mode), }; let mut out = std::io::stdout().lock(); out.write_all(&encode(&response).unwrap()).unwrap(); @@ -47,6 +47,7 @@ fn describe(mode: &str) -> Response { }], protocols: (protocol, protocol), ir_formats: vec![IR_FORMAT], + renderers: vec![], }) } diff --git a/tests/fixtures/generators/src/bin/renderer.rs b/tests/fixtures/generators/src/bin/renderer.rs new file mode 100644 index 0000000..0c09379 --- /dev/null +++ b/tests/fixtures/generators/src/bin/renderer.rs @@ -0,0 +1,10 @@ +//! The official Markdown renderer as an external one: what generation's +//! `comline-doc-markdown` binary is. + +use std::process::ExitCode; + +fn main() -> ExitCode { + let mut registry = comline_codegen::Registry::new(); + comline_doc_markdown::register(&mut registry); + comline_codegen::protocol::serve_stdio(®istry) +}