diff options
Diffstat (limited to 'mingling_ci/src/markdown/project.rs')
| -rw-r--r-- | mingling_ci/src/markdown/project.rs | 347 |
1 files changed, 0 insertions, 347 deletions
diff --git a/mingling_ci/src/markdown/project.rs b/mingling_ci/src/markdown/project.rs deleted file mode 100644 index d781b7e..0000000 --- a/mingling_ci/src/markdown/project.rs +++ /dev/null @@ -1,347 +0,0 @@ -//! Model of a testable rust code block extracted from markdown: its dependency -//! configuration (features + deps) and the code itself. - -use std::fmt::Write as _; -use std::path::Path; - -/// A single testable `rust` code block, modeled as a test project. -pub(crate) struct MarkdownTestProject { - pub features: Vec<String>, - pub deps: Vec<(String, String)>, - pub code: String, - pub is_build_time: bool, - pub has_main: bool, - pub has_gen_program: bool, - pub source_file: String, - pub line: usize, -} - -impl MarkdownTestProject { - /// FNV-1a 64-bit hash over the dependency configuration (features + deps). - /// - /// Blocks with the same hash share one temporary crate and avoid redundant - /// recompilation. The input is sorted so the hash is stable. - #[must_use] - pub fn compute_hash(&self) -> String { - let mut features: Vec<&str> = self.features.iter().map(String::as_str).collect(); - features.sort_unstable(); - let mut dep_names: Vec<&str> = self.deps.iter().map(|(n, _)| n.as_str()).collect(); - dep_names.sort_unstable(); - let mut dep_versions: Vec<&str> = self.deps.iter().map(|(_, v)| v.as_str()).collect(); - dep_versions.sort_unstable(); - let mut deps: Vec<String> = self.deps.iter().map(|(n, v)| format!("{n}={v}")).collect(); - deps.sort(); - - let canonical = format!( - "{}\n{}\n{}\n{}", - features.join(","), - dep_names.join(","), - dep_versions.join(","), - deps.join(",") - ); - - // FNV-1a 64-bit — stable across runs (no random seed). - let mut hash: u64 = 0xcbf2_9ce4_8422_2325; - for &byte in canonical.as_bytes() { - hash ^= u64::from(byte); - hash = hash.wrapping_mul(0x0000_0100_0000_01b3); - } - format!("{hash:016x}") - } -} - -/// Parses all fenced `rust` blocks from markdown content. -/// -/// Blocks marked `// NOT VERIFIED` are skipped. -pub(crate) fn parse_markdown(content: &str, source_file: &str) -> Vec<MarkdownTestProject> { - let mut projects = Vec::new(); - let lines: Vec<&str> = content.lines().collect(); - let mut i = 0; - while i < lines.len() { - if lines[i].trim() == "```rust" { - if let Some(proj) = parse_block(&lines, i, source_file) { - projects.push(proj); - } - while i < lines.len() && lines[i].trim() != "```" { - i += 1; - } - } - i += 1; - } - projects -} - -/// Parses a single code block starting at a `rust` fence line. -fn parse_block(lines: &[&str], start: usize, source_file: &str) -> Option<MarkdownTestProject> { - let mut code_lines: Vec<String> = Vec::new(); - let mut features: Vec<String> = Vec::new(); - let mut not_verified = false; - let mut deps: Vec<(String, String)> = Vec::new(); - let mut has_main = false; - let mut has_gen_program = false; - let mut is_build_time = false; - - let mut idx = start + 1; - let mut in_header = true; - - while idx < lines.len() { - let raw_line = lines[idx]; - let trimmed = raw_line.trim(); - - if trimmed == "```" { - break; - } - - // `@@@` lines: hidden in the rendered docs (filtered by a docsify - // plugin) but must still compile. - if let Some(stripped) = trimmed.strip_prefix("@@@") { - in_header = false; - let code = stripped.trim_start(); - if code.contains("fn main") { - has_main = true; - } - if code.contains("gen_program!") { - has_gen_program = true; - } - code_lines.push(code.to_string()); - idx += 1; - continue; - } - - if in_header && trimmed == "// NOT VERIFIED" { - not_verified = true; - idx += 1; - continue; - } - if in_header && trimmed == "// BUILD TIME" { - is_build_time = true; - idx += 1; - continue; - } - if in_header && trimmed.starts_with("// ") { - if let Some(feat_str) = trimmed.strip_prefix("// Features:") { - let feat_str = feat_str.trim(); - if feat_str.starts_with('[') && feat_str.ends_with(']') { - let inner = &feat_str[1..feat_str.len() - 1]; - if !inner.is_empty() { - features = inner - .split(',') - .map(|s| s.trim().trim_matches('"').to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } - } - idx += 1; - continue; - } - if trimmed == "// Dependencies:" { - idx += 1; - while idx < lines.len() { - let next = lines[idx].trim(); - if next == "```" { - break; - } - if let Some(dep_line) = next.strip_prefix("// ") { - if let Some((name, ver)) = dep_line.split_once(" = ") { - deps.push(( - name.trim().to_string(), - ver.trim().trim_matches('"').to_string(), - )); - } - idx += 1; - } else { - break; - } - } - continue; - } - } - - in_header = false; - if raw_line.contains("fn main") { - has_main = true; - } - if raw_line.contains("gen_program!") { - has_gen_program = true; - } - code_lines.push(raw_line.to_string()); - idx += 1; - } - - if code_lines.is_empty() || not_verified { - return None; - } - - Some(MarkdownTestProject { - features, - deps, - code: code_lines.join("\n"), - is_build_time, - has_main, - has_gen_program, - source_file: source_file.to_string(), - line: start + 1, - }) -} - -/// Builds the extra `[dependencies]` entries declared by a block's -/// `// Dependencies:` header comments. -/// -/// Markdown blocks declare companion crates like this: -/// -/// ```text -/// // Dependencies: -/// // serde = "1" -/// // clap = "4" -/// // tokio = { version = "1", features = ["full"] } -/// ``` -/// -/// Each `name = value` pair becomes one dependency of the generated test -/// crate (in addition to `mingling` itself), so doc blocks can freely use -/// external crates without repeating the whole manifest. -/// -/// # Special case: serde / clap -/// -/// Doc blocks pervasively derive serialization and argument parsing: -/// structural-renderer examples use `#[derive(Serialize)]`, the clap examples -/// use `#[derive(Parser)]` — and those derives live behind the `derive` -/// feature of `serde` / `clap`. Requiring every block to spell out -/// `// serde = { version = "1", features = ["derive"] }` would be -/// boilerplate repeated dozens of times, so the two crates automatically get -/// `features = ["derive"]` appended. -/// -/// Version values starting with `{` are inline tables (e.g. `tokio` with a -/// `features` list above) and are passed through verbatim — they already -/// carry their own features and must not be rewritten. -fn build_extra_deps(proj: &MarkdownTestProject) -> String { - let mut extra_deps = String::new(); - for (name, version) in &proj.deps { - if version.starts_with('{') { - // Inline table (path/features/…): the block already expressed its - // full dependency, so emit it unchanged. - let _ = writeln!(extra_deps, "{name} = {version}"); - } else if name == "serde" || name == "clap" { - // serde/clap derive: `#[derive(Serialize, Deserialize)]` and - // `#[derive(Parser)]` are used everywhere in the docs; auto-enable - // the `derive` feature to keep blocks terse. - let _ = writeln!( - extra_deps, - "{name} = {{ version = \"{version}\", features = [\"derive\"] }}" - ); - } else { - // Plain `name = "version"`. - let _ = writeln!(extra_deps, "{name} = \"{version}\""); - } - } - extra_deps -} - -/// Generates the `Cargo.toml` for a project. -/// -/// `manifest_path` is used to compute the relative path to the `mingling` crate. -pub(crate) fn generate_cargo_toml(proj: &MarkdownTestProject, manifest_path: &Path) -> String { - let features_str = if proj.features.is_empty() { - String::new() - } else { - let feats: Vec<String> = proj.features.iter().map(|f| format!("\"{f}\"")).collect(); - format!("features = [{}]", feats.join(", ")) - }; - - let extra_deps = build_extra_deps(proj); - - let mingling_path = find_mingling_relative_path(manifest_path); - let deps_section = if proj.features.is_empty() { - format!("[dependencies]\nmingling = {{ path = \"{mingling_path}\" }}\n{extra_deps}") - } else { - format!( - "[dependencies]\nmingling = {{ path = \"{mingling_path}\", {features_str} }}\n{extra_deps}" - ) - }; - - // Build-time projects mirror the features into [build-dependencies] so - // build.rs sees the same feature set. - let build_deps_section = if proj.is_build_time { - let feats: Vec<String> = proj.features.iter().map(|f| format!("\"{f}\"")).collect(); - let build_feats = if feats.is_empty() { - String::new() - } else { - format!("features = [{}]", feats.join(", ")) - }; - format!( - "\n[build-dependencies]\nmingling = {{ path = \"{mingling_path}\", {build_feats} }}\n" - ) - } else { - String::new() - }; - - format!( - r#"[package] - name = "test-doc" - version = "0.0.0" - edition = "2024" - -{deps_section}{build_deps_section} -[workspace] -"# - ) -} - -/// Computes the relative path from a manifest's parent directory to `mingling`. -/// -/// The process current directory is expected to be the project root. -fn find_mingling_relative_path(manifest_path: &Path) -> String { - let manifest_dir = manifest_path - .parent() - .expect("manifest path has no parent directory"); - let cwd = std::env::current_dir().expect("failed to get current directory"); - - let relative_to_root = manifest_dir.strip_prefix(&cwd).unwrap_or(manifest_dir); - let depth = relative_to_root.components().count(); - - let mut result = String::new(); - for _ in 0..depth { - result.push_str("../"); - } - result.push_str("mingling"); - result -} - -/// Generates `main.rs` for a project. -/// -/// Automatically prepends `use mingling::prelude::*;` and appends `fn main() {}` -/// and `gen_program!()` when the block does not provide them. -pub(crate) fn generate_main_rs(proj: &MarkdownTestProject) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - - if !proj.code.contains("use mingling::prelude::*;") { - output.push_str("#[allow(unused_imports)]\nuse mingling::prelude::*;\n\n"); - } - output.push_str(&proj.code); - output.push('\n'); - - if !proj.has_main { - output.push_str("\nfn main() {}\n"); - } - if !proj.has_gen_program { - output.push_str("\nmingling::macros::gen_program!();\n"); - } - output -} - -/// Generates `build.rs` for a build-time project: the code wrapped in -/// `fn main() { }` unless the block already provides one. -pub(crate) fn generate_build_rs(proj: &MarkdownTestProject) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - if proj.has_main { - output.push_str(&proj.code); - } else { - output.push_str("fn main() {\n"); - for line in proj.code.lines() { - output.push_str(" "); - output.push_str(line); - output.push('\n'); - } - output.push_str("}\n"); - } - output -} |
