crux_cli/codegen/
mod.rs

1mod filter;
2mod formatter;
3pub mod generate;
4mod indexed;
5mod item;
6mod node;
7mod serde;
8mod serde_generate;
9
10use std::{
11    collections::{BTreeMap, HashMap},
12    fs::{self, File},
13    io::Read,
14    path::PathBuf,
15    process::Command,
16};
17
18use anyhow::{anyhow, bail, Context, Result};
19use guppy::{graph::PackageGraph, MetadataCommand};
20use log::debug;
21use rustdoc_types::Crate;
22
23use crate::args::CodegenArgs;
24use filter::Filter;
25use formatter::Formatter;
26use node::ItemNode;
27use serde_generate::format::ContainerFormat;
28
29pub type Registry = BTreeMap<String, ContainerFormat>;
30
31pub fn codegen(args: &CodegenArgs) -> Result<()> {
32    let mut cmd = MetadataCommand::new();
33    let package_graph = PackageGraph::from_command(&mut cmd)?;
34
35    let manifest_paths: BTreeMap<&str, &str> = package_graph
36        .packages()
37        .map(|package| (package.name(), package.manifest_path().as_str()))
38        .collect();
39
40    let Ok(lib) = package_graph.workspace().member_by_path(&args.lib) else {
41        bail!("Could not find workspace package with path {}", args.lib)
42    };
43
44    let lib_name = lib.name();
45
46    let registry = run(lib_name, |name| load_crate(name, &manifest_paths))?;
47
48    // switch from vendored types to `serde-reflection` types
49    let registry: serde_reflection::Registry =
50        serde_json::from_slice(&serde_json::to_vec(&registry)?)?;
51
52    fs::create_dir_all(&args.output)?;
53
54    generate::java(&registry, &args.java_package, args.output.join("java"))
55        .context("Generating types for Java")?;
56
57    generate::swift(&registry, &args.swift_package, args.output.join("swift"))
58        .context("Generating tyeps for Swift")?;
59
60    generate::typescript(
61        &registry,
62        &args.typescript_package,
63        &lib.version().to_string(),
64        args.output.join("typescript"),
65    )
66    .context("Generating types for TypeScript")?;
67    Ok(())
68}
69
70fn run<F>(crate_name: &str, load: F) -> Result<Registry>
71where
72    F: Fn(&str) -> Result<Crate>,
73{
74    let mut previous: HashMap<String, Crate> = HashMap::new();
75
76    let shared_lib = load(crate_name)?;
77
78    let mut filter = Filter::default();
79    filter.process(crate_name, &shared_lib)?;
80
81    previous.insert(crate_name.to_string(), shared_lib);
82
83    let mut next: Vec<String> = filter.get_crates();
84
85    while let Some(crate_name) = next.pop() {
86        if previous.contains_key(&crate_name) {
87            continue;
88        }
89        let crate_ = load(&crate_name)?;
90
91        filter.process(&crate_name, &crate_)?;
92
93        next = filter.get_crates();
94        previous.insert(crate_name, crate_);
95    }
96
97    Ok(format(filter.edge))
98}
99
100fn format(edges: Vec<(ItemNode, ItemNode)>) -> Registry {
101    let mut formatter = Formatter::default();
102    formatter.edge = edges;
103    formatter.run();
104    debug!("{}", formatter.scc_times_summary());
105
106    formatter.container.into_iter().collect()
107}
108
109fn load_crate(name: &str, manifest_paths: &BTreeMap<&str, &str>) -> Result<Crate> {
110    // TODO: ensure that the user has installed the core rustdoc JSON files
111    // e.g. `rustup component add --toolchain nightly rust-docs-json`
112
113    let json_path = if let "core" | "alloc" | "std" = name {
114        rustdoc_json_path()?.join(format!("{name}.json"))
115    } else {
116        let manifest_path = manifest_paths
117            .get(name)
118            .ok_or_else(|| anyhow!("unknown crate {}", name))?;
119        rustdoc_json::Builder::default()
120            .toolchain("nightly")
121            .document_private_items(true)
122            .manifest_path(manifest_path)
123            .build()?
124    };
125    debug!("from {}", json_path.to_string_lossy());
126
127    let buf = &mut Vec::new();
128    File::open(json_path)?.read_to_end(buf)?;
129    let crate_ = serde_json::from_slice(buf)?;
130
131    Ok(crate_)
132}
133
134fn rustdoc_json_path() -> Result<PathBuf> {
135    let output = Command::new("rustup")
136        .arg("which")
137        .args(["--toolchain", "nightly"])
138        .arg("rustc")
139        .output()?;
140    let rustc_path = std::str::from_utf8(&output.stdout)?.trim();
141    let json_path = PathBuf::from(rustc_path)
142        .parent()
143        .ok_or_else(|| anyhow!("could not get parent of {}", rustc_path))?
144        .parent()
145        .ok_or_else(|| anyhow!("could not get grandparent of {}", rustc_path))?
146        .join("share/doc/rust/json");
147
148    Ok(json_path)
149}
150
151pub fn collect<'a, N, T>(input: T) -> impl Iterator<Item = Vec<(&'a N,)>>
152where
153    N: 'a + Clone,
154    T: Iterator<Item = (&'a N,)>,
155{
156    std::iter::once(input.collect::<Vec<_>>())
157}
158
159#[cfg(test)]
160mod tests;